税込から税抜を求めるには切り上げ?切り下げ?→ケースバイケースで要注意な件


4月から税込み表示が義務化され、また世のエンジニアが苦労する中、逆に税込から税抜を求めるの思った以上に苦労した話です。
一般的な小売では税込から税抜を求める状況はないと思いますが、相対取引で定価が無い場合は税込価格から決まることはザラです。

具体例として10096円〜10099円の間の価格でお話ししますと
10097円は÷1.1して切り捨てた9179円が正解です。
10099円は÷1.1して切り上げた9181円が正解です。

なぜなら双方とも誤って切り上げたり、切り下げると共に9180円になり、そこから税込価格を逆算すると10098円一択となってしまい元に値と矛盾するからです。ちなみに

10098円は割り切れる整数なので、切り捨て切り上げ関係なく、9180円の一択になります。
10096円は切り捨てて9178円、切り上げて9179円としてどちらでも良いのですが、それらからの税込み価格の計算は、端数の取扱次第で10095〜10097円まで幅広く解釈することができます。

これらを踏まえ実装としては、税抜価格netが最大化する按分になるceilでまず計算してみて、そこから税込の逆算が矛盾を起こす場合のみ、floorを採用する条件分岐が必要になります。
MySQLなら以下の通りです。

mysql> SELECT
    -> taxed,
    -> case when floor(ceil(taxed/1.1)*1.1)=taxed or ceil(ceil(taxed/1.1)*1.1)=taxed then ceil(taxed/1.1) else floor(taxed/1.1) end as net
    -> FROM (
    -> SELECT 10096 AS taxed UNION
    -> SELECT 10097 AS taxed UNION
    -> SELECT 10098 AS taxed UNION
    -> SELECT 10099 AS taxed
    -> ) prices;
+-------+------+
| taxed | net  |
+-------+------+
| 10096 | 9179 |
| 10097 | 9179 |
| 10098 | 9180 |
| 10099 | 9181 |
+-------+------+
4 rows in set (0.01 sec)

MySQLは問題ありませんでしたが、実装には浮動小数点の問題があり要注意です。
端的に以下に例示すると100円の税込みは110円のはずですが、どちらも111円になります。体感的にはもはやバグです。

JavaScriptによる例

Math.ceil(100*1.1) // 111
pythonによる例
import math
math.ceil(100*1.1) # 111

このリスクに対応するため8桁以降を丸めた処理を挟んで以下のようになりました。

pythonによる例
from math import floor, ceil
{taxed: ceil(round(taxed / 1.1, 8)) if floor(round(ceil(round(taxed / 1.1, 8)) * 1.1, 8)) == taxed or ceil(round(ceil(round(taxed / 1.1, 8)) * 1.1, 8)) == taxed else floor(round(taxed / 1.1, 8)) for taxed in [10096, 10097, 10098, 10099]}
# {10096: 9179, 10097: 9179, 10098: 9180, 10099: 9181}

1万円までの全価格を走査して統計とったスクリプトを書いたので、貼っておきます。
価格(整数)の集合全体の8割は割った後に切り上げ切り下げどちらでも良いのですが、残り1割ずつは切り上げ限定か切り下げ限定の困ったちゃん価格なのが分かります。

from math import floor, ceil
import pprint
result = {}

for taxed in range(100, 10100):
    recalc = {
        "FF": floor(round(floor(round(taxed / 1.1, 8)) * 1.1, 8)),
        "CF": floor(round(ceil(round(taxed / 1.1, 8)) * 1.1, 8)),
        "FC": ceil(round(floor(round(taxed / 1.1, 8)) * 1.1, 8)),
        "CC": ceil(round(ceil(round(taxed / 1.1, 8)) * 1.1, 8)),
    }
    key = tuple(k for k, v in recalc.items() if v == taxed)
    if len(key) == 0:
        print(taxed)
        raise
    result[key] = [*result.get(key, []), taxed]
    print(taxed, key, recalc)

pprint.pprint({k: {
    "min": min(v),
    "max": max(v),
    "len": len(v), } for k, v in result.items()})

税抜き価格の計算は税率で割って丸めるだけ、そんなふうに考えていた時期が俺にもありました。。。