Build the Engine That Computes Value Dates
Goal
You separate the receipt instant from the calendar date and build an engine that computes the value date with a holiday calendar and a cutoff time. You handle the counterparty institution's time zone and Seoul's daylight saving stretch of 1987 and 1988 correctly, and recompute all records for wrongly assigned value dates without overwriting the ledger.
Why it matters
An instant is one, but which day that instant is on gets an answer only once you decide on a time zone. At a bank, the cutoff time and the business-day calendar attach to this too, so the date on which money actually moves becomes not a stored fact but the product of rules. If this computation is scattered across several places in the code, the definition of "yesterday's transactions" differs by system, and that divergence does not look like an outage and survives until settlement reconciliation.
Holidays and cutoff times change every year, so you put them in data, not code. That is why this lab's engine reads the calendar and settings from the SQLite it receives as --db.
The grader does not trust the wording you wrote. It runs your engine with a temporary DB holding a holiday calendar and cutoff time that it made itself and compares the judgments against the values it computed itself. An engine with the answers embedded does not pass.
Steps
- Create and run
/root/bizday/gen_bizday.pyto make/root/bizday/txn.db. It holds 300 transfer receipts, 48 migrated transactions, 6 holidays, and 3 setting lines. - Write the transactions whose UTC date and KST date split into
/root/bizday/kst_shift.csv, and three numbers into/root/bizday/bucket.txt. - Implement
is-bizday,next-bizday, andadd-bizdaysin/root/bizday/bizday.py. The calendar is read from the holiday table of--db. - Add
value-datetobizday.py. The cutoff time and time zone are read from sys_param. - Add
localizetobizday.py, and write the transactions whose date splits between Seoul and London into/root/bizday/counterparty.csv. - Write the 48 migrated transactions into
/root/bizday/legacy_offsets.csvwith local time and offset, and write three numbers into/root/bizday/dst_seoul.txt. - Do not overwrite the ledger's value_date; create a
value_datetable intxn.dbto recompute the 300 records, and write only the transactions that change into/root/bizday/redate.csv. - Write the recomputed per-value-date tally into
/root/bizday/valuedate_summary.csvand the report into/root/bizday/bizday_report.md.
Notes
- Execution contract: the engine is called as
python3 /root/bizday/bizday.py [--db 경로] <하위명령> …(the placeholders are the path and the subcommand) and outputs one line of a JSON object to standard output. The default of--dbis/root/bizday/txn.db. is-bizday --date 2026-07-06→{"date":…, "is_bizday": true|false, "reason": "bizday"|"weekend"|"holiday"}next-bizday --date 2026-07-03→{"date":…, "next_bizday": "YYYY-MM-DD"}(that day itself is not counted)add-bizdays --date 2026-07-03 --n -2→{"date":…, "n":…, "result": "YYYY-MM-DD"}(positive goes forward, negative goes backward, and 0 pushes to the next business day only when that date is not a business day)value-date --at 2026-07-02T07:30:00Z→{"at":…, "local":…, "cutoff":…, "after_cutoff": true|false, "value_date": "YYYY-MM-DD"}localize --at 1987-05-15T03:00:00Z --tz Asia/Seoul→{"at":…, "tz":…, "local":…, "local_date":…, "utc_offset": "+10:00"}- Value date rule: move the receipt instant to the local time of
sys_param.value_date_tz, add a day if that time is at or pastsys_param.value_date_cutoff, and then from that date find a business day and push forward. - Direct test:
python3 /root/bizday/bizday.py value-date --at 2026-07-02T07:30:00Z - Test with a different calendar: create a temporary DB with just two tables, holiday and sys_param, and pass it with
--db. That is what the grader does. datetime.fromisoformatreads theZsuffix as it is from Python 3.11. The Python in this image is 3.12.3.- Common mistakes: adding a fixed 9 hours, comparing the cutoff time against a UTC time, embedding holidays in code, and calling
.date()beforeastimezone(). - This lab's holiday calendar is synthetic. It is not a real institution's holidays.
Build the receipt ledger snapshot
Create and run /root/bizday/gen_bizday.py to make /root/bizday/txn.db. It holds 300 transfer receipts, 48 migrated transactions, 6 holidays, and 3 setting lines.
There are four tables. The received_utc of txn is in the shape 2026-06-29T01:02:03Z, and value_date uses the first 10 characters of received_utc as they are, per the current core system rule. That this rule is wrong is the starting point of this lab.
Separate the instant from the calendar date
Write the transactions whose UTC date and KST date differ into /root/bizday/kst_shift.csv with the header txn_id,received_utc,utc_date,kst_date, and write shift_count=, utc_days=, and kst_days= into /root/bizday/bucket.txt.
Read the instant with datetime.fromisoformat, move it with astimezone(ZoneInfo("Asia/Seoul")), and then take .date(). You must not change the order. utc_days and kst_days are the number of distinct dates that come out when bucketing by each.
Move the business-day calendar into the engine
Implement is-bizday, next-bizday, and add-bizdays in /root/bizday/bizday.py. Holidays are read from the holiday table of the DB received with --db.
A weekend is weekday() >= 5 and holidays come from the table. reason is one of bizday, weekend, and holiday. The grader runs with a temporary DB holding holidays different from the learner DB, so if you embed the calendar in code, it fails.
Requests after the cutoff go to the next business day
Add value-date to bizday.py. Read the time zone and the cutoff time from sys_param, and if the time is at or past the cutoff, add a day and then push to a business day.
Do the cutoff comparison after moving to local time. If you compare against a UTC time string, it is off by an hour in the stretch that had daylight saving time. The boundary is >= — a request that comes in exactly on the hour rolls over. The grader picks a different cutoff time on each run.
Move to the counterparty institution's calendar
Add localize to bizday.py, and write the transactions whose date splits between Seoul and London into /root/bizday/counterparty.csv with the header txn_id,received_utc,seoul_date,london_date.
Write utc_offset with a sign and two-digit hours and minutes, like +09:00. local must be an RFC 3339 string with an offset — if you strip the offset, that string is no longer an instant. London is UTC+1 in summer.
Find the one hour of summer 1987
Write the 48 migrated transactions into /root/bizday/legacy_offsets.csv as txn_id,received_utc,seoul_local,utc_offset, and write kdt_rows=, kst_rows=, and date_shift_rows= into /root/bizday/dst_seoul.txt.
Asia/Seoul is UTC+9 now, but in the summers of 1987 and 1988 there was daylight saving time. zoneinfo knows that fact, so ask it rather than computing it yourself. date_shift_rows is the number of migrated transactions whose KST date differs from the UTC date.
Recompute value dates without overwriting the ledger
Create a value_date(txn_id, value_date) table in txn.db, recompute the value dates of the 300 records into it, and write only the transactions that differ from the old value date into /root/bizday/redate.csv as txn_id,received_utc,old_value_date,new_value_date. Do not touch txn.value_date.
Do not reimplement the rule; import bizday.py as a module (put /root/bizday on sys.path and import). If you erase the old value dates the ledger recorded, you can no longer tell which rule assigned those values. Put the recomputed result in the new table.
Carry it into the tally and the report
Write the recomputed count and amount per value date into /root/bizday/valuedate_summary.csv as value_date,txn_count,amount_sum in ascending order, and write five sections, ## 무엇이 틀렸나, ## 결제일을 어떻게 계산하나, ## 상대방 표준시, ## 과거 서머타임, and ## 재발 방지, into /root/bizday/bizday_report.md (the Korean headings mean: What was wrong, How the value date is computed, The counterparty's time zone, Past daylight saving time, and Preventing recurrence).
You get the tally by joining the value_date table and txn. In the report, write the number of changed records, the cutoff time, which time zone you compute in, the counterparty institution's time zone, and the 1987 and 1988 offsets, in numbers and names. It is also worth checking that dates that fall on holidays are missing from the tally.