Three Won Missing, Yet Every Row Checks Out
한국어 원문으로 표시합니다.
목표
통화마다 다른 최소단위를 표로 만들고, 수수료 원장을 정수 최소단위로 다시 적재하고, 반올림 모드를 정책으로 못박고, 나머지 1까지 맞는 배분과 환산 순서에 따른 차이를 산출물로 남긴다.
왜 중요한가
금액을 실수로 다루면 한 줄에 1씩 어긋나고, 그 정도는 단위 시험이 잡지 못한다. 합계가 안 맞는다는 문의가 들어와도 어느 줄이 틀렸는지 짚을 수 없어서 "다시 돌리면 맞는다" 는 운영 절차가 생긴다. 여기서 정하는 것은 세 가지다. 저장 형식(정수 최소단위), 끊는 자리와 반올림 모드(정책), 나머지 배분 규칙(최대잉여법). 셋 다 기술이 정해 주지 않고 사람이 정해 문서에 적어야 하는 것이다. 채점기는 여러분이 적은 숫자끼리 비교하지 않는다. 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) 두 표로 적재하고 /root/money/store.json 에 통화별 두 합계를 적는다.
- /root/money/money_fmt.py 에 파싱과 표시를 모으고, 안내의 입력 12개를 판정해 /root/money/format_check.json 을 만든다.
- 같은 곱셈을 ROUND_HALF_UP 과 ROUND_HALF_EVEN 으로 끊어 /root/money/rounding.json 에 갈라진 줄 수와 두 합계를 적는다.
- splits.csv 의 30건을 최대잉여법으로 배분해 /root/money/alloc.json 에 적는다. 합계는 총액과 정확히 같아야 한다.
- 안내의 환율표로 통화 5종을 원화로 환산해 /root/money/fx.json 에 줄 단위와 총액 단위 두 값을 적는다.
- 앞 단계의 결과를 /root/money/report.json 하나로 묶고 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단계 두 경로: 실수 쪽은
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네 가지만 쓴다. - 7단계 환율표(원화 기준, 소수 넷째 자리): JPY 9.0421 · USD 1381.5027 · KWD 4512.3007 · BHD 3665.1420 · CLP 1.4508. 이 숫자는 이 실습의 가정입니다 — 실제 고시환율이 아니라 소수 넷째 자리라는 계약을 보이려고 지어낸 값입니다. 통화별 소수 자리(최소단위)만 ISO 4217 의 실제 값입니다.
- 흔한 실수: 파이썬 내장
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 를 안내의 두 경로로 계산해 /root/money/float_gap.json 에 rate, rows, mismatch_rows, gap_minor_total, by_ccy 를 적으세요.
두 경로의 차이는 두 군데서 옵니다. 0.125 는 이진수로 정확하지만 12.345 같은 금액은 그렇지 않고, 파이썬 내장 round() 는 0.5 를 짝수 쪽으로 붙입니다. by_ccy 는 통화마다 mismatch_rows 와 gap_minor 두 값을 담은 객체입니다.
정수 최소단위로 다시 적재하기
/root/money/money.db 에 fee(row_id, ccy, minor, exponent)와 fee_real(row_id, ccy, amount REAL) 두 표를 만들어 600행씩 넣고, /root/money/store.json 에 통화별 sum_minor, real_round_minor, equal 을 적으세요.
SQLite 의 자료형은 컬럼이 아니라 값에 붙습니다. INTEGER 로 선언해도 실수를 넣으면 REAL 로 들어가니, typeof() 로 확인해 보세요. real_round_minor 는 REAL 합계를 그 통화의 소수 자리만큼 곱해 반올림한 정수입니다. equal 이 전부 true 로 나올 수도 있습니다 — 이 규모에서는 REAL 합이 우연히 같은 값으로 반올림되기 때문이고, 그래서 이런 결함이 오래 살아남습니다. 본 대로 적으세요.
파싱과 표시를 한 문으로 모으기
/root/money/money_fmt.py 에 parse 와 fmt 를 만들고, 안내의 입력 12개를 판정해 /root/money/format_check.json 에 ok 와 rejected 로 적으세요.
ok 항목에는 in, ccy, minor, round_trip 을 담습니다. round_trip 은 최소단위를 다시 글자로 만든 값이라 원래 표기와 같아야 합니다. 거절 사유는 네 가지뿐이고, 통화 자체를 모르는 것과 소수 자리를 모르는 것은 다른 사유입니다.
반올림 모드를 정책으로 못박기
같은 곱셈을 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 입니다.
곱셈은 한 번만 하고 quantize 를 두 번 하면 됩니다. 0.125 는 이진수로 정확한 값이라 정확히 절반인 자리가 자주 나오고, 거기서 두 모드가 갈립니다. 두 합계가 같다면 한쪽이 실제로 적용되지 않은 것입니다.
나머지 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)를 적으세요.
줄마다 환산해 원 단위로 끊고 더한 값과, 먼저 다 더한 뒤 한 번만 환산한 값은 다릅니다. 둘 다 계산해 차이를 그대로 적으세요. 원화는 소수 자리가 0 이라 끊는 자리는 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 는 통화별 최소단위 합계입니다.