npm package: @gstypecode/time-fields
Description: Time field normalization utilities shared across data pipelines.
Author: gstypecode
Latest version: 0.1.0

# @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.

```js
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

```bash
npm install @gstypecode/time-fields
```

---

## Understanding 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` |

```js
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:

```js
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.

```js
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