zsh loses precision when miltiplying two floats of significantly different magnitude

Viewed 88

I'm trying to do some floating-point math in a zsh script. I'm seeing the following behavior:

$ (( a = 1.23456789 * 0.00000001 )); printf "a = %g\n" $a
a = 1.23e-08

$ (( a = 1.23456789 * 0.00000001 )); printf "a = %e\n" $a
a = 1.230000e-08

$ (( a = 1.23456789 * 0.0000001 )); printf "a = %e\n" $a
a = 1.235000e-07

I expect not to loose the 1st number's mantissa precision when I merely multiply it by a number whose mantissa is 1 (or at least very close to 1, if the true binary representation is considered). In other words, I'd expect to get a = 1.23456789e-08 or maybe some truncated mantissa, but not zeros after 1.23 / 1.235.

I'm running the following version:

$ zsh --version
zsh 5.8 (x86_64-apple-darwin20.0)

Am I missing something? Or is it an issue in zsh? I'm new to zsh, and I don't have a lot of experience in shell programming in general, so any help is appreciated. Thanks!

2 Answers

It appears that (( x = 1.0 )), when x is not defined, will cause Zsh to declare the variable as -F: a double precision floating point which is formatted to fixed-point with 10 decimal digits on output:

% unset x; (( x = 0.12345678901234567 )); declare -p x
typeset -F x=0.1234567890

% unset x; x=$((0.12345678901234567)); declare -p x
typeset x=0.12345678901234566

I don't know why it works this way, but if you manually declare your variable as a string first, this won't happen, and you'll get the full value:

% unset a; typeset a; (( a = 1.23456789 * 0.00000001 )); printf "a = %g\n" $a
a = 1.23457e-08

The difference comes from the way of how you pass the value of a to printf. If you write it as

(( a = 1.23456789 * 0.00000001 )); printf "a = %e\n" $((a))

$ (( a = 1.23456789 * 0.0000001 )); printf "a = %e\n" $((a))

the problem does not occur. This is described here, where it says:

floating point numbers can be declared with the float builtin; there are two types, differing only in their output format, as described for the typeset builtin. The output format can be bypassed by using arithmetic substitution instead of the parameter substitution, i.e. ‘${float}’ uses the defined format, but ‘$((float))’ uses a generic floating point format

Related