合計が3ウォン合わないのに、どの行も間違っていない
目標
通貨ごとに異なる最小単位を表にし、手数料元帳を整数の最小単位で再ロードし、丸めモードをポリシーとして確定し、余りの1まで合う按分と、換算の順序による差を、出力物として残します。
なぜ重要なのか
金額を実数で扱うと1行あたり1ずつずれ、その程度は単体テストでは捕まりません。合計が合わないという問い合わせが来ても、どの行が間違っているかを指せないので、「回し直せば合う」という運用手順ができます。 ここで決めるのは3つです。保存形式(整数の最小単位)、丸める桁と丸めモード(ポリシー)、余りの按分ルール(最大剰余法)。3つとも、技術が決めてくれるものではなく、人が決めて文書に書かなければならないものです。 採点ツールは、書かれた数字どうしを比較しません。ステップ1で作った元データから基準値を再計算して突き合わせます。そのため、ルールをそのとおりに守ることが重要です。
ステップ
- /root/money/gen_money.pyを作成して実行し、/root/money/currencies.json、/root/money/fees.csv(600行)、/root/money/splits.csv(30件)を作ってください。
- 加算率0.125を、実数経路と整数経路でそれぞれ計算し、/root/money/float_gap.jsonにずれた行数と差を書いてください。
- /root/money/money.dbにfee(整数の最小単位)とfee_real(REAL)の2つのテーブルとしてロードし、/root/money/store.jsonに通貨ごとの2つの合計を書いてください。
- /root/money/money_fmt.pyにパースと表示をまとめ、案内の入力12個を判定して、/root/money/format_check.jsonを作ってください。
- 同じ掛け算をROUND_HALF_UPとROUND_HALF_EVENで丸めて、/root/money/rounding.jsonに、分かれた行数と2つの合計を書いてください。
- splits.csvの30件を最大剰余法で按分して、/root/money/alloc.jsonに書いてください。合計は総額とぴったり同じでなければなりません。
- 案内の為替レート表で通貨5種をウォンに換算して、/root/money/fx.jsonに、行単位と総額単位の2つの値を書いてください。
- 前のステップの結果を/root/money/report.jsonの1つにまとめ、fees.csvのsha256もあわせて書いてください。
参考
- ステップ1の生成ルール: 通貨はKRW,JPY,USD,KWD,BHD,CLPをこの順に使い回し(row_idを6で割った余り)、row_idは0から599、最小単位の金額は
1000 + (row_id * 37) % 9001、amount_textはその金額を通貨の小数桁数で表示した文字列です。 - ステップ1の按分ルール: case_idは0から29、通貨はKRW,USD,KWDを使い回し、total_minorは
100000 + case_id * 7777、重みは1 + (case_id*3) % 9、1 + (case_id*5) % 7、1 + (case_id*7) % 5です。 - ステップ2の2つの経路: 実数側は
int(round(round(float(amount_text) * 0.125, 소수자리) * 10 ** 소수자리))、整数側はDecimal(amount_text) * Decimal("0.125")を小数桁数でROUND_HALF_UPのquantizeで丸めてから最小単位に変えた値です(プレースホルダーは小数桁数です)。 - ステップ4の入力12個:
KRW 1234、JPY 0、USD 0.05、KWD 0.001、BHD 12.345、CLP 99、USD 1234567.89、USD 1.234、KRW 12.5、XAU 1.0、ZZZ 5、USD 1,234.56。拒否理由はtoo_many_minor_digits、unknown_minor_units、unknown_currency、not_a_numberの4つだけを使います。 - ステップ7の為替レート表(ウォン基準、小数第4位): JPY 9.0421・USD 1381.5027・KWD 4512.3007・BHD 3665.1420・CLP 1.4508。この数字はこのラボの前提です。実際の公示為替レートではなく、小数第4位という契約を示すために作った値です。通貨ごとの小数桁数(最小単位)だけが、ISO 4217の実際の値です。
- よくある間違い: Python組み込みの
round()が銀行丸めであることを忘れる、円に小数点を付ける、按分で余りを捨てる、換算の順序を混ぜる。 - 確認:
python3 -c "import json;print(json.load(open('/root/money/float_gap.json'))['mismatch_rows'])"
通貨表と手数料元帳を作る
/root/money/gen_money.pyを作成して実行し、/root/money/currencies.json、/root/money/fees.csv(600行)、/root/money/splits.csv(30件)を作ってください。
通貨表の小数桁数は、理論に書かれたList Oneの値をそのまま使います。小数桁数がN.A.の通貨は数字ではないので、nullです。amount_textは最小単位の整数を文字にしたもので、小数桁数0の通貨には小数点がありません。
実数で計算した加算額は何行ずれるか
加算率0.125を案内の2つの経路で計算し、/root/money/float_gap.jsonにrate、rows、mismatch_rows、gap_minor_total、by_ccyを書いてください。
2つの経路の差は2か所から来ます。0.125は2進数で正確ですが、12.345のような金額はそうではなく、Python組み込みのround()は0.5を偶数側に寄せます。by_ccyは、通貨ごとにmismatch_rowsとgap_minorの2つの値を持つオブジェクトです。
整数の最小単位で再ロードする
/root/money/money.dbにfee(row_id, ccy, minor, exponent)とfee_real(row_id, ccy, amount REAL)の2つのテーブルを作って600行ずつ入れ、/root/money/store.jsonに通貨ごとのsum_minor、real_round_minor、equalを書いてください。
SQLiteのデータ型は、列ではなく値に付きます。INTEGERで宣言しても、実数を入れるとREALとして入るので、typeof()で確認してみてください。real_round_minorは、REALの合計をその通貨の小数桁数だけ掛けて丸めた整数です。equalがすべてtrueになることもあります。この規模では、REALの合計がたまたま同じ値に丸まるからで、だからこうした欠陥は長く生き残ります。見たとおりに書いてください。
パースと表示を1か所にまとめる
/root/money/money_fmt.pyにparseとfmtを作り、案内の入力12個を判定して、/root/money/format_check.jsonにokとrejectedで書いてください。
okの項目にはin、ccy、minor、round_tripを入れます。round_tripは最小単位をもう一度文字にした値なので、元の表記と同じでなければなりません。拒否理由は4つだけで、通貨自体を知らないことと、小数桁数を知らないことは、別の理由です。
丸めモードをポリシーとして確定する
同じ掛け算をROUND_HALF_UPとROUND_HALF_EVENで丸め、/root/money/rounding.jsonにpolicy、rate、diff_rows、half_up_total_minor、half_even_total_minorを書いてください。このラボのポリシーはROUND_HALF_UPです。
掛け算は1回だけ行い、quantizeを2回行えばよいのです。0.125は2進数で正確な値なので、ちょうど半分の桁がよく出て、そこで2つのモードが分かれます。2つの合計が同じなら、片方が実際には適用されていません。
余りの1まで合う按分
splits.csvの30件を最大剰余法で3か所に按分し、/root/money/alloc.jsonにcases、sum_ok、remainder_totalを書いてください。
取り分は切り捨てで配り、残った分だけ1ずつ追加で配ります。誰が受け取るかは、剰余(総額に重みを掛けたものを、重みの合計で割った余り)が大きい順で、同じなら若い番号が先です。remainder_toには、1を多く受け取った場所の、0から数えた番号を昇順で書きます。
換算の順序が結果を変える
案内の為替レート表で通貨5種をウォンに換算し、/root/money/fx.jsonにrate_scale、round_mode、by_ccy(rate, rows, row_then_sum_krw, sum_then_row_krw, gap_krw)を書いてください。
行ごとに換算してウォン単位に丸めて足した値と、先にすべて足してから1回だけ換算した値は違います。両方を計算して、差をそのまま書いてください。ウォンは小数桁数が0なので、丸める桁は1ウォンです。
6つのステップを1つのレポートにまとめる
/root/money/report.jsonにfees_sha256、rows、currencies、exact_total_minor、float_gap_rows、float_gap_minor、rounding_policy、half_even_diff_rows、allocation_cases、allocation_remainder_total、fx_gap_krw、verdictを書いてください。verdictはinteger-minor-unitsです。
前のステップの出力物を読んでまとめますが、ハッシュは今のfees.csvから再計算します。レポートがどのデータを根拠にしたかを指し示せなければ、あとで数字を説明できません。exact_total_minorは、通貨ごとの最小単位の合計です。