bcコマンドのscaleの落とし穴【シェルで小数計算する話】


## 前置き
`bash` で数値計算をしたい場合、整数の計算であれば例えば次のようにできる。
```bash
$ a=3
$ echo $(( a + 2 ))
5
```
これで事足りることも多い。だけど、例えば割り算の場合は小数点以下が切り捨てられるので、期待通りの結果が得られない。
```bash
$ a=3
$ echo $(( a / 2 ))
1
```
くどいようだが切り捨てであり、四捨五入ではない。

## Hello, `bc` コマンド
そこで便利なのが`bc`コマンド。`man bc`によれば「任意精度の計算言語」とのこと。

:::note
Ubuntu Linux 20.04.5 LTS には最初からインストールされていた。
インストールされていない場合は `apt install bc` などで入ると思う。
その他、環境については次の通り：
```bash
$ bc --version
bc 1.07.1
Copyright 1991-1994, 1997, 1998, 2000, 2004, 2006, 2008, 2012-2017 Free Software Foundation, Inc.

$ uname -srvmpo
Linux 5.15.0-56-generic #62~20.04.1-Ubuntu SMP Tue Nov 22 21:24:20 UTC 2022 x86_64 x86_64 GNU/Linux```
:::

個人的に一番使うのは次のように数式を標準入力に与えて結果を出力させるというもの。その際、小数点以下の有効桁数を`scale=2`のように指定する。
```bash
$ echo "scale=2; 200/10000*100" | bc
2.00
```
`bc`を初めて知ったときは「小数計算ができるようになった喜び」や「任意精度への過剰な期待」からここで終わってしまいがちだけど（はい私です）、ここに注意点がある。

## 期待しない結果

順に見ていく。
```bash
$ echo "scale=2; 100/10000*100" | bc
1.00
```
これは期待通りの結果。問題ない。

では、これはどうだろう：

:::note alert
```bash
$ echo "scale=2; 199/10000*100" | bc
1.00
```
**なんと、期待する結果は`1.99`なのに、値が半分程度しかない！**
:::

## 修正方法の例

正しくは数式をこのように書く必要がある。
```bash
$ echo "scale=2; 100*199/10000" | bc
1.99
```
計算式を変形し、100を掛ける順番を最初にもってきた。

あるいは次のように変形して、演算の優先順位を変えても良い：
```bash
$ echo "scale=2; 199/(10000/100)" | bc
1.99
```

雑なやり方になるけど、`scale`を極端に大きくすれば事足りるかもしれない。この場合数式を変形する必要がないので楽ではある：
```bash
$ echo "scale=20; 199/10000*100" | bc
1.99000000000000000000
```

:::note warn
ただ、`scale`を大きくすれば良いかというとそうでもない。例えばこの結果を見てもらえれば、何が起きているかおよそ検討がつくのではないだろうか：
```bash
$ echo "scale=3; 199/10000*100" | bc 
1.900
```
:::

## `bc` の注意点

`bc` の `scale` は与えた数式の最終的な計算結果だけに適用されるわけではない。**演算の各過程でも参照され、都度桁落ちが発生しうる**。上記で見てきた例で起きていたことは次の通り：
* 誤った例： `199/10000` は本来 `0.0199` だが `scale=2` により `0.01` になった。これを `*100` した結果、`1.00`になってしまった。
* 正しい例： まず `100*199` が `199000` になり、それを `/10000` した結果、期待通り `1.99` になった。

### scale の適用タイミング

`scale`が適用されるタイミングには条件があるようで、あらゆる四則演算で常に適用されるわけではないようだ。網羅的な検証はできていないが、いくつか試した限りでは次の通り：
* 足し算と引き算は整数・小数にかかわらず任意精度。精度は数式内で最大のものになるかと思いきや、完全に無視されているようだ。
  * scaleは小数点以下の有効桁数なので整数の場合は無視されて当然
  * だが小数の加減算でも無視されるので、混乱するか、任意精度を万能だと誤認しがち：
```bash
$ echo "scale=2; 100 + 0.1" | bc
100.1

$ echo "scale=2; 10000 + 0.1234" | bc
10000.1234

$ echo "scale=2; 12345678901234567890 + 0.12345678901234567890" | bc
12345678901234567890.12345678901234567890
```
* 整数の掛け算は任意精度。これは問題ない。
* 小数の掛け算も任意精度。scaleは無視される。（足し算や引き算と同様）
```bash
$ echo "scale=2; (10000 + 0.0100) * 0.5 " | bc
5000.0050

$ echo "scale=2; 100 * 0.5" | bc
50.0
```
* 割り算では常にscaleが適用されるようだ。なお有効桁以下は切り捨て：
```bash
$ echo "scale=2; 1 / 2" | bc
.50

$ echo "scale=2; (10000 + 0.0100)" | bc
10000.0100
$ echo "scale=2; (10000 + 0.0100) / 2" | bc
5000.00    # 四捨五入されて 5000.01 になったりはしない
```
この違いはさらに演算を重ねることで影響が出てくる：
```bash
$ echo "scale=2; (10000 + 0.0100) / 2   + 0.995" | bc
5000.995

$ echo "scale=2; (10000 + 0.0100) * 0.5 + 0.995" | bc
5001.0000
```
上の2つの式は手計算すると同じ計算結果になるハズのもの。もし単位が10kmの距離計算や、10kgの重量計算、10万円の金額計算などを意味していて、かつこのあと小数点以下を切り捨てた場合、2つの数式の間に生まれる差は大きい。（そんな計算をシェルでやる人はいないか）

### 特に知っておくべき注意点

scaleが適用されるのは割り算のときだけのように思われるが、一見似たような小数計算であってもscaleが適用されたりされなかったりするので、挙動を正確に把握するのが難しい印象だ。

:::note alert
実質的なscaleが計算の途中で増減するのは厄介。
指定したscaleよりも**最終的な見た目上の有効桁数が大きいからと言って、必ずしもその精度で計算されたとは限らない**からだ（なんてこった）：
```bash
$ echo "scale=2; (100 + 0.010)" | bc
100.010   # 0.010 の精度が優先されて小数点以下3桁になっている

$ echo "scale=2; (100 + 0.010) / 2" | bc
50.00     # 手計算で割り算すると 50.005 だが小数点以下2桁で切り捨て

$ echo "scale=2; (100 + 0.010) / 2 + 0.001" | bc
50.001    # 小数点以下3桁の精度だったなら 50.006 になっていたはずだが...
```
scaleを指定した場合、信頼できるのはそのscaleまでの結果であり、見た目上の結果を鵜呑みにしてはいけない。
:::

## 数式を組む際の工夫例

### scale の再設定

上記の最後の例でいうと、指定した scale よりも大きな精度で結果が表示された場合、scaleをその精度に修正して再計算した方が良さそうだ：
```bash
$ echo "scale=3; (100 + 0.010) / 2 + 0.001" | bc
50.006
```

### 数式の変形

変数の値がとりうる範囲は事前にある程度分かる場合も少なくない。
なので可能な限り、例えば
1. 数式の中の各変数の値の大きさの範囲をできるだけ限定・把握する
1. 数式の中の各演算の優先順位を正確に把握する
1. 予期せぬ桁落ちが起きないよう数式を変形させる：
    * 計算過程の結果の数値が大きくなりやすい足し算や掛け算などを先にやる
    * 逆に数値が小さくなりやすい引き算や、分母が大きな割り算、1以下の小数を含む掛け算などは後でやる
1. 割り算は最後にやる（！？）

といった工夫をするとベター。

ここまで読んで、鋭い人はシンプルな対策に気づいたかもしれない（後述）。

## `bc -l` オプション

機能的にもタイプ数的にも便利なオプションを紹介。

`-l`オプションを使うとsin(x)の意味の`s(x)`や log(x) の意味の`l(x)`など基本的な数学関数が使えるようになる。そして**デフォルトで `scale=20` に設定されるので、`scale`の指定を省略しても良い。**

計算結果のフォーマットを気にしなければ、これを常用すると良いかもしれない。冒頭の例で数式の変形をせずとも期待通りの結果が得られる。
```bash
$ echo "199/10000*100" | bc -l
1.99000000000000000000
```

ただし、それでも前述の事情・注意点は変わらないので `-l` を盲信しないようにしたい。

というのも、コマンドラインを編集する過程で`scale`の指定が残ってしまうとそれが優先され、計算の過程で容赦なく桁落ちが発生する。これは現実的に起こりうるので`-l`への盲信は危ない：
```bash
$ echo "scale=2; 199/10000*100" | bc -l
1.00
```
20桁の有効桁で桁落ちの影響を受けることなど現実的にはほとんどないと思うが、だからこそこれには注意しよう。もしかしてscaleは常に指定しないほうがよいのでは、という気すらしてくる。

## 対策はシンプルにしたい

あくまで基本的なアイデアだが、タイプ量が増えるものの最初に十分大きな数`BIG`を掛けてやって、最後に同じ数で割ると解決する場合もある。小数部分の桁数の整形もできる。

```bash
$ BIG=100000000
$ echo "scale=2; ${BIG} * 199/10000*100 / ${BIG}" | bc 
1.99
```

ただし途中に足し算や引き算がある数式の場合はもうひと工夫いるのはお察しの通り。ケースバイケースとなる。また、

:::note warn
「本来やりたい計算式が見やすくなるよう括弧で括ってやろう」などと余計な気を回すと意味がなくなったりするので、少しは注意が必要：
```bash
$ BIG=100000000
$ echo "scale=2; ${BIG} * (199/10000*100) / ${BIG}" | bc 
1.00
```
:::

ターミナルやシェルスクリプトでちょっと計算させたいだけのときはあまり細々したことは気にしたくないので、対策はもっとシンプルにしておきたいところ。

## 結論

いまのところ、個人的にはこの方法に落ち着いている：

:::note info
* 【有効桁数】 常に `bc -l` オプションを使う。scaleは使わない。桁数の指定は諦める
* 【整形】`bc` では整形をしない。整形は `printf` に任せる。
```bash
$ echo "199/10000*100" | bc -l | xargs printf "%.2f\n"
1.99
```
:::

:::note warn
余談だが、`printf`は整形だけかと思いきや四捨五入もしてくれるorしてしまう。切り捨てる`bc`とは挙動が異なるので混乱しないようにしたい：
```bash
$ a=9.98765
$ printf "%.1f\n" $a
10.0
```
:::

<details><summary>蛇足</summary>

ちなみに小数点以下をすべて切り捨てたい場合、`${a%.*}` のようにbashのパラメータ展開を使えば簡単にできる。
だが例えば小数点第3位以下を切り捨てたい場合は結構まどろっこしい。こういうときにも`bc`は使えるが、scaleを使うことになるのでポリシー次第か：
```bash
$ a=9.98765
$ echo "scale=2; $a / 1" | bc
9.98
```
いずれにせよ、原則的に計算の途中では整形せず、最後だけにした方がよいと思う。
</details>

思わぬ計算ミスの予防、あるいは早期修正のお役に立てれば幸いです。