@gstypecode/time-fields
v0.1.0
Published
Time field normalization utilities shared across data pipelines.
Maintainers
Readme
@gstypecode/time-fields
When "01:00 on September 21" actually means "01:00 on September 22"
The Problem: Operational Dates vs. Calendar Dates
In industries that operate through the night — restaurants, hospitals, call centers, security, logistics — workers think of their shift as one continuous block. A bar that opens at 20:00 and closes at 03:00 considers the entire shift as "September 21st," even though 03:00 technically falls on September 22nd.
When these shifts were tracked on paper forms, staff naturally wrote the operational date ("September 21") alongside clock times like "01:00" or "02:30." Everyone understood that "01:00 on September 21" meant the early morning after that business day — not 1 AM at the start of it.
Then the paper records got digitized. The dates and times were entered exactly as written. And now your database has timestamps that are off by a day:
Stored data:
date: "2025-09-21"
start: "01:00" ← meant Sept 22 at 01:00
end: "06:00" ← meant Sept 22 at 06:00
Naively combined:
start = 2025-09-21T01:00:00 ← WRONG. Off by one day.This isn't a bug — it's a legacy of how humans naturally track overnight work. But if you don't correct it, every downstream calculation — duration, overlap detection, payroll, analytics — silently produces wrong results.
This library detects and corrects these date shifts, converting operational-date records into accurate calendar timestamps.
How It Works
The core idea: if a start time falls within a "late-night window" (by default, before 03:00), the library recognizes it as belonging to the next calendar day and shifts it forward by 24 hours.
import { computeTimeFields } from "@gstypecode/time-fields";
const result = computeTimeFields({
date: new Date("2025-09-21T00:00:00+09:00"), // operational date from the form
start: "01:00", // clock time as written
end: "06:00",
trueTimeLength: "05:00",
});
result.start_timestamp; // => 2025-09-22T01:00:00+09:00 ✅ Corrected
result.end_timestamp; // => 2025-09-22T06:00:00+09:00 ✅ Corrected
result.duration_hours; // => 5
result.start_shift_flag; // => true (date correction was applied)Install
npm install @gstypecode/time-fieldsUnderstanding the Options
Each option maps directly to a real-world concept. Here's what they control and why you might change them.
The Business-Day Cutoff: shiftThresholdMinutes
What it means: "How late into the night do we still consider it 'yesterday'?"
The default is 180 (= 03:00). Any start time before 03:00 is treated as the next calendar day.
| Scenario | Setting |
|---|---|
| Bar closing at 03:00 — shifts never start after 03:00 | 180 (default) |
| 24-hour hospital — handoff at 06:00 | 360 |
| Office with occasional overtime past midnight | 60 |
const factory = createTimeFields({
shiftThresholdMinutes: 360, // anything before 06:00 = next day
});Cutoff Boundary: shiftComparisonOperator
What it means: "Does the cutoff time itself count as 'yesterday' or 'today'?"
"<"(default): start times strictly less than the threshold are shifted. A start of exactly 03:00 is NOT shifted."<=": start times at or below the threshold are shifted. A start of exactly 03:00 IS shifted.
Day Correction Amount: dayOffsetMinutes
What it means: "How many minutes to add when correcting the date."
Default is 1440 (= 24 hours). You almost never need to change this, unless your operational calendar uses non-standard day lengths.
End-Time Day Crossing: endCrossesDayComparison
What it means: "How to detect when the end time has also crossed into the next calendar day."
When the end time is numerically smaller than the start time (e.g., start: "22:00", end: "02:00"), the end time must also be shifted forward. This option controls the comparison operator for that detection, separately for shifted and non-shifted starts:
endCrossesDayComparison: {
shifted: "<=", // when start was date-corrected
notShifted: "<", // when start was NOT date-corrected
}Time Format: timeFormatRegex
What it means: "What format are the time strings in?"
Default matches HH:MM (e.g., "01:00", "23:59"). Override if your data uses a different format.
Confidence Values: confidence
What it means: "Numeric labels for how certain the shift detection was."
confidence.determined(default:1): the library had enough data to make a definitive decision.confidence.default(default:0): no start time was provided, so no determination was possible.
These values appear in start_shift_confidence in the result. Useful for flagging records that need manual review.
API
computeTimeFields(payload)
The main function. Takes an operational-date record and returns corrected timestamps.
Payload:
| Field | Type | Description |
|---|---|---|
| date | Date | The operational date (the date written on the form) |
| start | string? | Start time in HH:MM |
| end | string? | End time in HH:MM |
| trueTimeLength | string? | Actual working duration in HH:MM |
Result:
| Field | Type | Description |
|---|---|---|
| start_timestamp | Date \| null | Corrected start timestamp |
| end_timestamp | Date \| null | Corrected end timestamp |
| start_shift_flag | boolean \| null | Whether date correction was applied |
| start_shift_confidence | number | Confidence of the shift detection (1 = determined, 0 = unknown) |
| duration_hours | number \| null | Working duration in decimal hours |
| raw.start_minutes | number \| null | Parsed start time in minutes from midnight |
| raw.end_minutes | number \| null | Parsed end time in minutes from midnight |
| raw.duration_minutes | number \| null | Parsed duration in minutes |
createTimeFields(options?)
Factory API. Creates a configured instance for processing many records with the same rules.
import { createTimeFields } from "@gstypecode/time-fields";
const tf = createTimeFields({
shiftThresholdMinutes: 360,
shiftComparisonOperator: "<=",
});
for (const record of records) {
const result = tf.computeTimeFields(record);
}Utility Functions
| Function | Description |
|---|---|
| parseTimeToMinutes(str) | Parse HH:MM string to minutes from midnight |
| parseDurationToMinutes(str) | Same as above (semantic alias for duration strings) |
| computeShift(minutes) | Test whether a time in minutes would be date-shifted |
| addMinutes(date, min) | Add minutes to a Date (non-mutating) |
| constants.DEFAULT_OPTIONS | Default configuration (frozen object) |
Use Cases
- Hospitality / nightlife: Shifts spanning midnight recorded under the opening date
- Healthcare: Night-shift nursing logs written under the admission date
- Call centers: Overnight support tickets timestamped with the business date
- Security / logistics: Guard rotations and delivery windows crossing midnight
- ETL pipelines: Normalizing legacy paper-form data for analytics
License
MIT
