Arithmetic on values with memory size units

A little self promotion: we wrote a library called libbytesize to do these calculations in C and Python and it also has a commandline tool called bscalc

$ bscalc "5 * (100 GiB + 80 MiB) + 2 * (300 GiB + 15 GiB + 800 MiB)"
1215425413120 B
1186938880.00 KiB
   1159120.00 MiB
      1131.95 GiB
         1.11 TiB

The library is packaged in most distributions, unfortunately the tool isn't. It's in Fedora in libbytesize-tools and SuSE in bscalc package, but not in Debian/Ubuntu.


In zsh, you could define a math function like:

() {
  typeset -gA bsuffix
  local n=1 ni=1 s
  for s (k m g t p e) {
    (( n *= 1000 )); (( ni *= 1024 ))
    (( bsuffix[$s] = bsuffix[${s}ib] = bsuffix[${s}io] = ni ))
    (( bsuffix[${s}b] = bsuffix[${s}o] = n ))
  }
}
b() {
  set -o localoptions -o extendedglob
  local s=${(M)1%(#i)(${(j:|:k)~bsuffix})}
  (( ${1%$s} * ${bsuffix[$s:l]-1} ))
}

functions -Ms b

Then you'd be able to use b(1G), b(1mB) in any zsh arithmetic expression, like in (( .... )), $(( ... )), $array[...], etc, or in zcalc:

$ <<< $((b(86k) + b(320mb) + b(1.7gio)))
2145449164.8

$ autoload zcalc
$ zcalc
1> b(86k) + b(320mb) + b(1.7gio)
2.14545e+09
2> :sci 15
2145449164.8

$ echo $(( b(infeo) ))
Inf   

(note that we make no difference between b and B (or o / O), the match it case insensitive. It's not interpreted as bit vs byte).

Another approach could be to have the b() function take the whole expression as argument, and replace all the suffixes with * $bsuffix[<suffix>]

b() {
  set -o localoptions -o extendedglob
  local s=${(M)1%(#i)(${(j:|:k)~bsuffix})}
  (( ${1//(#bi)([0-9.][[:blank:]]#)(${(j:|:k)~bsuffix})/$match[1] * $bsuffix[$match[2]:l] } ))
}

And then:

$ echo $(( b(1m + 1Mb) ))
2048576

There's the problem of e/E (exa) though which puts a spanner in the works in that 1e-3GB would not be interpreted as 0.001 * 1000000000 but as 1 * 1152921504606846976 - 3 * 1000000000.

In any shell with support for floating point arithmetic (ksh93, zsh, yash), you could always define:

  K=1024  M=$((K * K))  G=$((M * K))  T=$((G * K))  P=$((T * K))  E=$((P * K))
KiB=$K  MiB=$M        GiB=$G        TiB=$T        PiB=$P        EiB=$E
 KB=1000 MB=$((KB*KB)) GB=$((MB*KB)) TB=$((GB*KB)) PB=$((TB*KB)) EB=$((PB*KB))

Or to golf it:

K=1024 EiB=$((E=K*(P=PiB=K*(T=TiB=K*(G=GiB=K*(M=MiB=K*K))))))
KB=1000 EB=$((EB=KB*(PB=KB*(TB=KB*(GB=KB*(MB=KB*KB))))))

And write $(( 1.1*GB + 5*K ))

to add the suffixes on output, you could use GNU numfmt:

$ human() numfmt --field=- --to=iec --suffix=iB
$ echo $(( b(1m + 1Mb) )) | human
2.0MiB

There is Bcal.

$ bcal -m "(5kib+2mib)/2"
1051136 B
$ bcal -m "(5kb+2mb)/2"
1002500 B

The -m flag is for brief output. Removing it outputs verbosely with base 2 (KiB, MiB, GiB, TiB) and base 10 (kB, MB, GB, TB) results.

It does not understand 86k or 320m or 1.7g, after all those are not proper byte units. In that case, you could use Sed to add the b after each letter and then pipe it to bcal:

$ cat file
1.7g+320m+86k
$ sed 's/[gmk]/&b/g' file | bcal -m
bcal> 1.7gb+320mb+86kb
2020086000 B

You can also use it in interactive mode.