04 — Dates¶
Dates are ambiguous to parse. 01/05/2025 could be the 5th of January or the
1st of May, and a parser that guesses can guess wrong, producing a date that is
not the one in the source. The framework does not guess: you state the exact
input format, and a value that doesn't match raises an error rather than being
misread. This example reformats some shipment timestamps to canonical forms, and
uses a malformed input to show the fail-fast behaviour.
What it teaches¶
- Reformatting with
convert_date. Parse a date with one format string and emit it in another. - Strict, fail-fast parsing. A value that doesn't match the declared
source_formatraises an error rather than being guessed at. - No fuzzy / multi-format parser. Each rule pins exactly one source format; a genuinely mixed column must be split and handled per format.
The data¶
input.csv — shipment timestamps in one consistent source format:
| shipment_id | ordered_at | delivered_on |
|---|---|---|
| SH1 | 2025-01-05 09:30:00 |
2025-01-08 |
| … | … | … |
The harmonization choices (and why)¶
| Source | Target | Conversion | Why |
|---|---|---|---|
ordered_at |
order_date |
%Y-%m-%d %H:%M:%S → %Y-%m-%d |
Drop time-of-day to a canonical date. |
ordered_at |
order_month |
%Y-%m-%d %H:%M:%S → %b %Y |
Month-grain label (Jan 2025) for rollups, from the same column. |
delivered_on |
delivered_us |
%Y-%m-%d → %m/%d/%Y |
US display format; source is date-only, so no time in the source format. |
How the format strings work¶
Each convert_date takes two format strings: a source_format that says how to
read the incoming value, and a target_format that says how to write it
out. Both use Python's strftime/strptime codes, where each %-token stands
for one part of a date or time — %Y is a four-digit year, %m a zero-padded
month number, %d a zero-padded day, %H:%M:%S the time, and %b an
abbreviated month name. So %Y-%m-%d %H:%M:%S reads 2025-01-05 09:30:00, and
%b %Y writes the same instant back out as Jan 2025. The literal characters
between the tokens — the dashes, slashes, and spaces — are matched and emitted
verbatim, which is why %m/%d/%Y produces the slash-separated US form.
Two rules read the same column, ordered_at, with the same source_format
but different target_formats, producing both a canonical order_date and a
coarser order_month label from one source. The source format is a property of
the input; the target format is a separate choice about how you want the result
to look.
Fail-fast is a feature¶
convert_date does not try to be clever. Run it against bad_input.csv (a row
with 01/05/2025 9:30 AM, which doesn't match %Y-%m-%d %H:%M:%S):
../../../harmonization-framework/venv/bin/python -c "
from harmonization_framework import RuleSet
from harmonization_framework.harmonize import harmonize_file
r=RuleSet(); r.load('rules.json', clean=True)
harmonize_file('bad_input.csv','/tmp/out.csv', r, dataset_name='events')
"
You get:
ValueError: Failed to parse date/time value '01/05/2025 9:30 AM' with source_format='%Y-%m-%d %H:%M:%S'
A silent misparse (guessing month vs. day) would corrupt data quietly; a loud
error is the safer default for a meaningful field. If a column genuinely mixes
formats, split it (e.g. by a flag column) and apply a convert_date per format
— there is no single multi-format rule.
--on-missingis unrelated. That CLI flag governs missing columns, not malformed values. A bad date value always raises an error regardless of--on-missing.
The rules, serialized¶
As in chapter 01, the saved file is the mapping, and the same rule set serializes to JSON (the default) or YAML — the extension decides which, and both load identically. Shown in both formats below.
Worth noticing here is how convert_date captures its input and output formats
in the file itself, so the parsing and the target representation are both
explicit — there is no hidden assumption about how a date string is read or
written.
rules.json:
[
{
"sources": [
"ordered_at"
],
"target": "order_date",
"operations": [
{
"operation": "convert_date",
"source_format": "%Y-%m-%d %H:%M:%S",
"target_format": "%Y-%m-%d"
}
],
"metadata": {
"rationale": "Reduce a full timestamp to a canonical date-only field; the time component is not needed downstream."
}
},
{
"sources": [
"ordered_at"
],
"target": "order_month",
"operations": [
{
"operation": "convert_date",
"source_format": "%Y-%m-%d %H:%M:%S",
"target_format": "%b %Y"
}
],
"metadata": {
"rationale": "Month-grain label for reporting/rollups, derived from the same timestamp."
}
},
{
"sources": [
"delivered_on"
],
"target": "delivered_us",
"operations": [
{
"operation": "convert_date",
"source_format": "%Y-%m-%d",
"target_format": "%m/%d/%Y"
}
],
"metadata": {
"rationale": "Reformat ISO date to US display format. Source is date-only, so the source_format has no time component."
}
}
]
rules.yaml:
- sources: [ordered_at]
target: order_date
operations:
- {operation: convert_date, source_format: '%Y-%m-%d %H:%M:%S', target_format: '%Y-%m-%d'}
metadata: {rationale: Reduce a full timestamp to a canonical date-only field; the
time component is not needed downstream.}
- sources: [ordered_at]
target: order_month
operations:
- {operation: convert_date, source_format: '%Y-%m-%d %H:%M:%S', target_format: '%b
%Y'}
metadata: {rationale: 'Month-grain label for reporting/rollups, derived from the
same timestamp.'}
- sources: [delivered_on]
target: delivered_us
operations:
- {operation: convert_date, source_format: '%Y-%m-%d', target_format: '%m/%d/%Y'}
metadata: {rationale: 'Reformat ISO date to US display format. Source is date-only,
so the source_format has no time component.'}
Running it¶
../../../harmonization-framework/venv/bin/python build_rules.py
../../../harmonization-framework/venv/bin/python run_python.py
../../../harmonization-framework/venv/bin/python run_yaml.py # same, from rules.yaml
bash run_cli.sh
Expected output¶
Data files¶
Rendered from the committed CSV files in this example.
input.csv¶
| shipment_id | ordered_at | delivered_on |
|---|---|---|
| SH1 | 2025-01-05 09:30:00 | 2025-01-08 |
| SH2 | 2025-02-14 17:05:00 | 2025-02-19 |
| SH3 | 2025-03-30 23:59:00 | 2025-04-02 |
| SH4 | 2025-11-01 06:00:00 | 2025-11-03 |
expected_output.csv¶
| shipment_id | ordered_at | delivered_on | order_date | order_month | delivered_us | source dataset | original_id |
|---|---|---|---|---|---|---|---|
| SH1 | 2025-01-05 09:30:00 | 2025-01-08 | 2025-01-05 | Jan 2025 | 01/08/2025 | events | 0 |
| SH2 | 2025-02-14 17:05:00 | 2025-02-19 | 2025-02-14 | Feb 2025 | 02/19/2025 | events | 1 |
| SH3 | 2025-03-30 23:59:00 | 2025-04-02 | 2025-03-30 | Mar 2025 | 04/02/2025 | events | 2 |
| SH4 | 2025-11-01 06:00:00 | 2025-11-03 | 2025-11-01 | Nov 2025 | 11/03/2025 | events | 3 |