Missing data is not the same thing as a convenient default. That sounds obvious, yet temporal software regularly converts an empty time field into midnight, noon, the current time, or the start of a day. The interface may look complete after that conversion, but the program has silently changed an unknown fact into a known one.
This matters anywhere an hour can change the result: medical timelines, transport schedules, legal deadlines, astronomical calculations, historical records, and calendrical systems. I encountered the problem while working with a BaZi calculation pipeline. A BaZi chart can use year, month, day, and hour components. If the birth time is absent, the honest result is a three-component analysis with hour-dependent conclusions withheld. Inserting noon would make the output look richer while making its provenance weaker.
The useful engineering question is not “Which fallback time should we choose?” It is “How do we keep uncertainty visible through every layer of the system?”
The public calculation evidence repository provides the concrete calendar-domain fixtures referenced below. The rest of this article focuses on the reusable software boundary behind them.
Model knowledge, not just a string
A common input model makes absence too easy to erase:
const birthTime = form.time || "12:00";
Enter fullscreen mode Exit fullscreen mode
After this line runs, downstream code cannot tell whether noon came from the user or the fallback. Validation, analytics, caching, and the result renderer all see the same string. The information loss happens before the calculation begins.
A small discriminated union keeps the two states separate:
/**
* @typedef {{ kind: "known", localTime: string, source: "user" }}
* KnownTime
* @typedef {{ kind: "unknown" }} UnknownTime
* @typedef {KnownTime | UnknownTime} BirthTime
*/
function parseBirthTime(value) {
const normalized = value?.trim();
return normalized
? { kind: "known", localTime: normalized, source: "user" }
: { kind: "unknown" };
}
Enter fullscreen mode Exit fullscreen mode
This type makes the missing state part of the program’s vocabulary. A caller must inspect kind before using localTime. More importantly, the source of a known value remains available for debugging and display.
The same pattern works in TypeScript, Rust enums, database check constraints, JSON Schema, or protocol buffers. The syntax is less important than preserving the distinction between observed and inferred data.
Separate date-level context from hour-level output
Unknown time does not make every calculation impossible. A date and place may still establish civil-date context, time-zone rules, and some date-level boundaries. The mistake is to let an internal helper value escape as if it were supplied by the user.
For example, a pipeline may need a stable instant to ask whether a date lies near a time-zone transition. It can use an internal reference time, provided that reference is labeled and never becomes an hour result:
function buildTemporalContext({ date, city, birthTime }) {
return {
date,
city,
birthTime,
dateProbe:
birthTime.kind === "known"
? { localTime: birthTime.localTime, purpose: "user-instant" }
: { localTime: "12:00", purpose: "date-context-only" },
};
}
Enter fullscreen mode Exit fullscreen mode
Here noon is not a birth-time fallback. It is a bounded implementation detail used only for date-level context. Naming the purpose prevents a later maintainer from reusing dateProbe.localTime to calculate an hour-dependent field.
Time zones are also data, not a fixed numeric offset. Historical offsets and daylight-saving rules change by place and date. A production system should use a maintained zone identifier and a versioned data source such as the IANA Time Zone Database, while retaining the original city and resolution result for auditability.
Make derived sections conditional by construction
Rendering logic should not ask whether an hour value happens to exist after several transformations. It should receive an explicit capability map from the calculation layer.
function buildCapabilities(birthTime) {
const hasHour = birthTime.kind === "known";
return {
yearAnalysis: true,
monthAnalysis: true,
dayAnalysis: true,
hourPillar: hasHour,
hourInteractions: hasHour,
preciseHourTiming: hasHour,
};
}
function buildHourResult(birthTime) {
if (birthTime.kind === "unknown") {
return { unknown: true };
}
return calculateHourDependentFields(birthTime.localTime);
}
Enter fullscreen mode Exit fullscreen mode
The serialized boundary for an absent hour is deliberately small:
{ "unknown": true }
Enter fullscreen mode Exit fullscreen mode
That shape is easier to test than a collection of empty strings, zeroes, or placeholder labels. It also stops clients from treating null as a transient loading state. The UI can render a clear explanation: date-level sections are available, while hour-dependent sections require a supplied time.
Conditional output should be structural, not cosmetic. Hiding an invented hour in CSS does not repair a payload that already contains fabricated calculations. The server response, cached artifact, downloadable file, and on-screen result should all share the same boundary.
Preserve uncertainty in cache keys
Caching can reintroduce the bug even when the calculator is correct. Consider two requests with the same date and city: one has an unknown time, and the other contains a user-supplied noon. They must not share an identity.
A canonical cache payload can include the discriminant:
function canonicalInput(input) {
return {
date: input.date,
cityId: input.cityId,
time:
input.birthTime.kind === "known"
? {
kind: "known",
localTime: input.birthTime.localTime,
source: input.birthTime.source,
}
: { kind: "unknown" },
};
}
Enter fullscreen mode Exit fullscreen mode
Hash the canonical payload rather than a display string. The unknown result can still be cached and restored quickly, but it must restore the same limitations. A fast cache hit is not permission to upgrade uncertainty into precision.
If the user later supplies a time, that is a new input state and should produce a different key. The application may explain that more sections are now available, but it should not imply that the earlier result was wrong; it was correctly bounded by the earlier evidence.
Test the boundary, not the prose
Copy such as “time optional” is useful, but it cannot guarantee behavior. The contract belongs in executable tests that cover parsing, serialization, capabilities, and cache identity.
The following fictional test vector is named shanghai_unknown_time. It represents a software fixture, not a real person or customer:
import assert from "node:assert/strict";
const shanghai_unknown_time = {
date: "1990-06-15",
cityId: "Asia/Shanghai",
birthTime: { kind: "unknown" },
};
const shanghai_known_noon = {
...shanghai_unknown_time,
birthTime: { kind: "known", localTime: "12:00", source: "user" },
};
assert.deepEqual(
buildHourResult(shanghai_unknown_time.birthTime),
{ unknown: true },
);
assert.equal(
buildCapabilities(shanghai_unknown_time.birthTime).hourPillar,
false,
);
assert.notDeepEqual(
canonicalInput(shanghai_unknown_time),
canonicalInput(shanghai_known_noon),
);
Enter fullscreen mode Exit fullscreen mode
Additional fixtures should cover blank strings, whitespace, malformed times, daylight-saving transitions, places that changed civil offsets, and dates near domain-specific boundaries. The most important negative assertion is that a missing time never produces an hour-dependent value.
The repository shows this style of fictional vector and boundary documentation in a concrete calendar domain. Its value is reproducibility: a reader can inspect the input, expected behavior, and limitations without relying on a marketing claim.
Explanations are part of the data contract
An unknown result should not look like an error. The system successfully calculated everything justified by the available input. The UI therefore needs three distinct states:
- Loading: the calculation has not finished.
- Unknown: the calculation finished, but the required fact was not supplied.
- Known: the calculation finished with a user-supplied fact.
Collapsing these states into one blank card creates confusion. Users may retry, assume the app is broken, or believe the system inferred something it did not. A concise explanation next to the omitted section is more trustworthy than silently filling the space.
This distinction should also survive exports and APIs. A generated document can say that hour-dependent sections were intentionally omitted. An API can return a typed unknown object. An event log can record time_kind=unknown without storing a fabricated clock value.
A practical review checklist
When reviewing any temporal feature with optional time input, ask:
- Can the program distinguish a supplied value from an internal probe?
- Does the missing state survive parsing, calculation, caching, rendering, and export?
- Are hour-independent and hour-dependent capabilities separated?
- Do known noon and unknown time produce different canonical inputs?
- Is the unknown state explained without presenting it as a failure?
- Can an executable fixture prove that no hour value is invented?
The broader principle is simple: uncertainty is information. Preserve it with the same care as a date, location, or identifier. A result with explicit limits is more useful than a precise-looking result built on a fact nobody supplied.
Disclosure: AI assistance was used to draft and edit this article. The examples, links, and boundary assertions were checked against the referenced repository and executable local tests before publication.
답글 남기기