Splitting an amount in parts

MJ - A frequent question on the Tcl Chat is how to split the number of seconds in the number of days, minutes etc. The following procedure provides this and allows you to specifiy the divisors of the different units, making it very flexible. The largest unit should not have a divider, this indicates that this unit should not be split any further (see examples below) The lead and inter parameters indicate whether amounts that are 0 should be displayed.

For example the following {a x b y c z d} would translate to: A d is z c's which are y b's consisting of x a's.

Note that this only works for integer amounts and dividers.

 proc split_amount {amount dividers {lead 0} {inter 1}} {
   set result {}
   foreach {unit divider} $dividers {
       if {!$lead && $amount==0} break;
       if {$divider eq {}} {
              set result "$amount$unit $result"
              break
       } 
       if {$inter || !($amount%$divider == 0)} {
          set result "[expr {$amount % $divider}]$unit $result"
       }
       set amount [expr {$amount/$divider}]
   }
   return $result
 }

Example usage:

 % split_amount [clock seconds] {s 60 m 60 h 24 d 365 y}
 37y 238d 13h 2m 59s    
 % split_amount [clock seconds] {{ seconds} 60 { minutes and} 60 { hours,} 24 { days,} 365 { years,}}
 37 years, 238 days, 13 hours, 4 minutes and 7 seconds
 % split_amount 2234141 {{ gram} 1000 { kilogram and} 1000 { tonne,}}
 2 tonne, 234 kilogram and 141 gram
 % split_amount 1200 {s 60 m 60 h 24 d 365 y}
 20m 0s 
 % split_amount 1200 {s 60 m 60 h 24 d 365 y} 1 1
 0y 0d 0h 20m 0s 
 % split_amount 1200 {s 60 m 60 h 24 d 365 y} 1 0
 0y 20m 
 % split_amount 1200 {s 60 m 60 h 24 d 365 y} 0 0
 20m

MAKR 2007-08-31: Something similar, but less flexible:

 proc timemsg {seconds} {
    if {$seconds < 0} {
        return -code error "seconds should be unsigned integer"
    } elseif {$seconds < 60} {
        set num $seconds
        set unit second
    } elseif {$seconds < 3600} {
        set num [expr {($seconds%3600)/60}]
        set unit minute
    } elseif {$seconds < 86400} {
        set num [expr {($seconds%86400)/3600}]
        set unit hour
    } else {
        set num [expr {int($seconds/86400)}]
        set unit day
    }
    if {$num > 1} {
        append unit "s"
    }
    return "$num $unit"
 }
 % timemsg 1
 1 second
 % timemsg 125
 2 minutes
 % timemsg [clock seconds]
 13756 days

LEG 2015-0913:- A slightly different implementation of the first algorithm which is friendly to interp alias:

proc _split_amount_ {dividers t {_s {}} {_P {}}} {
    if {!$t} {return "0[lindex $dividers 1]"}
    foreach {_D _S _U}  $dividers {
        lassign [list [expr {$t/$_D}] [expr {$t%$_D}]] $_U $_S
        if {[set $_S]} {set _P [append $_S $_s $_S [expr {[string length $_P]?" $_P":$_P}]]}
        if {![set t [set $_U]]} {return $_P}
    }
    return "$t$_s$_U $_P"
}

t is the amount you want to split, _s is an optional string between value and unit.

_P is a dirty trick to save one line of code for initializing a local variable - don't use _P.

Some properties of the implementation:

  • If one of the intermediate values is zero it is not included in the resulting string.
  • No leading or trailing spaces are generated.
  • If the amount is zero, the returned string is the '0' followed by the smallest unit.
  • The dividers are specified in a small language with triplets which answer the question how much of a smaller unit yield the next one, think '60 s m' as: 60 seconds a minute.

And now a bunch of splitters, mostly taken from Wikipedia and not tested thoroughly:

# Elapsed time
#
interp alias {} elapsed_s {} _split_amount_ {
    60 s m   60 m h  24 h d  7 d w  4 w mon 12 mon y
}
interp alias {} elapsed_ms {} _split_amount_ {
    1000 ms s  60 s m   60 m h  24 h d  7 d w  4 w mon 12 mon y
}
interp alias {} elapsed_s_months_only {} _split_amount_ {
    60 s m   60 m h  24 h d  30 d mon 12 mon y
}
# this is the same splitting as in the first example from MJ
interp alias {} elapsed_s_years_only {} _split_amount_ {
    60 s m   60 m h  24 h d  365 d y
}

# Imperial lengths
#
interp alias {} split_inch {} _split_amount_ {
    12 in ft  3 ft yrd  1760 yrd mi
}
interp alias {} split_mil {} _split_amount_ {
    1000 mil in  12 in ft  3 ft yrd  1760 yrd mi
}
# call as: split_1/32in $amount " "
# or: split_1/32in [expr {$amount_float_inch/32.}] " "
interp alias {} split_1/32in {} _split_amount_ {
    2 1/32in 1/16in 2 1/16in 1/8in  2 1/8in 1/4in
    2 1/4in 1/2in   2 1/2in in      12 in ft
    3 ft yrd  1760 yrd mi
}

# Imperial gallon
interp alias {} split_ounces {} _split_amount_ {
    20 "imp fl oz" pint  2 pint quart  4 quart "imp gal"
}
# US liquid gallon
interp alias {} split_ounces {} _split_amount_ {
    16 "fl oz" pint  2 pint quart  4 quart "imp gal"
}


# Data amounts
#
interp alias {} split_binary_JEDEC {} _split_amount_ {
    1024 B KB     1024 KB MB    1024 GB MB
    1024 GB TiB   1024 TiB PiB  1024 PiB EiB
    1024 EiB ZiB  1024 ZiB YiB
}
interp alias {} split_binary_IEC {} _split_amount_ {
    1024 B KiB    1024 KiB MiB  1024 GiB MiB
    1024 GiB TiB  1024 TiB PiB  1024 PiB EiB
    1024 EiB ZiB  1024 ZiB YiB
}
# call as: split_bibyte $amount " "
interp alias {} split_bibyte {} _split_amount_ {
    1024 byte kibibyte      1024 kibibyte mebibyte  1024 mebibyte gibibyte
    1024 gibibyte tebibyte  1024 tebibyte pebibyte  1024 pebibyte exibyte
    1024 exibyte zebibyte   1024 zebibyte yobibyte
}
interp alias {} split_byte_decimal {} _split_amount_ {
    1000 B kB   1000 KB MB  1000 GB MB
    1000 GB TB  1000 TB PB  1000 PB EB
    1000 EB ZB  1000 ZB YB
}

# Degrees (angles)
#
interp alias {} split_arcseconds {} _split_amount_ {
    60 \" '  60 ' °
}
#"#

# Quantities as dozens
#
interp alias {} split_dozens {} _split_amount_ {
    12 pcs dz  12 dz gross  12 gross "great gross"
}

Example usage:

% split_arcseconds 34000
9° 26' 40"
% split_bibyte [expr {1024*1024*1024+4*1024}] " "
1 gibibyte 4 kibibyte