Data Model
Glucose readings, source metadata, unit contracts, timestamps, and time zones.
A reading contains one glucose value, its unit, and a timestamp. Source-specific details are optional metadata on that shape.
GlucoseReading
import type { GlucoseReading, GlucoseUnit } from '@glucoseiq/core'
const reading: GlucoseReading = {
value: 112,
unit: 'mg/dL',
timestamp: '2024-01-15T13:05:00Z',
}| Field | Contract |
|---|---|
value | A numeric glucose value in the accompanying unit |
unit | Exactly 'mg/dL' or 'mmol/L' |
timestamp | A string representing the measurement instant; use ISO 8601 with Z or an explicit offset |
The properties are readonly in TypeScript. That prevents accidental mutation
at compile time; it is not runtime validation of data received from JSON,
forms, or a vendor service.
For a single unknown value and unit, isValidGlucoseValue checks that the value
is positive and finite and that the unit is supported:
import { isValidGlucoseValue } from '@glucoseiq/core'
const input = { value: 112, unit: 'mg/dL' } as const
if (isValidGlucoseValue(input.value, input.unit)) {
// value and unit satisfy the runtime value check
}This helper does not validate a complete reading or its timestamp. Ingestion functions apply their own documented filtering and error behavior.
NormalizedCGMReading
Vendor normalizers return a structural superset of GlucoseReading:
import type { NormalizedCGMReading } from '@glucoseiq/core/connectors'
const reading: NormalizedCGMReading = {
value: 112,
unit: 'mg/dL',
timestamp: '2024-01-15T13:05:00.000Z',
trend: 'flat',
source: 'dexcom',
vendorId: 'record-123',
dedupKey: 'dexcom:record-123',
}The added fields are:
| Field | Meaning |
|---|---|
trend | Normalized direction from rapidRising through rapidFalling, or unknown |
source | 'dexcom', 'libre', 'nightscout', or 'unknown' |
vendorId | Original stable identifier when the payload provides one |
nativeUnit | Vendor display unit when the normalized value is emitted in another unit |
dedupKey | Stable source-qualified key when the adapter can construct one |
Reading-based analytics such as analyzeGlucose accept normalized readings:
import { analyzeGlucose } from '@glucoseiq/core'
import { normalizeLibreEntries } from '@glucoseiq/core/connectors'
const readings = normalizeLibreEntries([
{
Value: 6.2,
ValueInMgPerDl: 112,
GlucoseUnits: 0,
TrendArrow: 3,
Timestamp: '2024-01-15T13:00:00Z',
},
])
const report = analyzeGlucose(readings)The array normalizers return readings in chronological order. They map source
trend vocabularies to the shared CGMTrend union and use unknown when a
trend is absent or unmapped.
Units travel with every reading
GlucoseUnit has two supported values:
type GlucoseUnit = 'mg/dL' | 'mmol/L'Reading-based analytics inspect each reading's unit and normalize it for the calculation. A dataset may therefore contain both supported units:
import { analyzeGlucose, type GlucoseReading } from '@glucoseiq/core'
const readings: GlucoseReading[] = [
{ value: 108, unit: 'mg/dL', timestamp: '2024-01-15T13:00:00Z' },
{ value: 6.2, unit: 'mmol/L', timestamp: '2024-01-15T13:05:00Z' },
]
const report = analyzeGlucose(readings)Some low-level metric functions accept a number[] plus one separate unit
instead of GlucoseReading[]. For those functions, all numbers must use the
unit passed in their options or argument. Check the function signature before
removing unit metadata.
Use convertGlucoseUnit when you need to convert one value explicitly:
import { convertGlucoseUnit } from '@glucoseiq/core'
convertGlucoseUnit({ value: 180, unit: 'mg/dL' })
// { value: 10, unit: 'mmol/L' }buildAGPProfile also accepts an output unit. It converts each input reading
before returning percentile values and records the chosen unit on the result.
Timestamps represent instants
Use ISO 8601 timestamps with a Z suffix or explicit UTC offset:
const utc = '2024-01-15T13:05:00Z'
const sameInstant = '2024-01-15T08:05:00-05:00'Avoid offset-free timestamps such as 2024-01-15T08:05:00. JavaScript parses
those relative to the runtime environment, which can make the same payload mean
different instants on different machines.
The vendor normalizers convert accepted timestamps to UTC ISO strings.
parseGlucoseCSV does the same for rows it accepts.
A time zone is a view over an instant
A timestamp identifies when a reading occurred. An IANA time zone controls how that instant is assigned to a local clock for time-of-day analysis.
import {
buildAGPProfile,
splitDayNight,
type GlucoseReading,
} from '@glucoseiq/core'
const readings: GlucoseReading[] = [
{ value: 112, unit: 'mg/dL', timestamp: '2024-01-15T13:00:00Z' },
{ value: 6.2, unit: 'mmol/L', timestamp: '2024-01-15T13:05:00Z' },
]
const profile = buildAGPProfile(readings, {
timeZone: 'America/Detroit',
})
const { day, night } = splitDayNight(readings, {
timeZone: 'America/Detroit',
nightStartHour: 22,
nightEndHour: 6,
})buildAGPProfile and splitDayNight default to UTC. The timeZone option on
analyzeGlucose is forwarded to its AGP profile; summary metrics such as mean,
GMI, and Time in Range are not clock-bucketed by that option.
Use a real IANA identifier such as America/Detroit, not an abbreviation such
as EST. An invalid zone causes the time-zone-aware function to throw.
Keep identity outside the reading
The current reading contracts do not include a person, account, device, sensor session, or tenant identifier. Keep that context in your own containing record and pass one subject's readings to the analytics call:
import { analyzeGlucose, type GlucoseReading } from '@glucoseiq/core'
interface SubjectSeries {
subjectId: string
readings: GlucoseReading[]
}
const subjects: SubjectSeries[] = [
{
subjectId: 'subject-1',
readings: [
{ value: 112, unit: 'mg/dL', timestamp: '2024-01-15T13:00:00Z' },
],
},
]
const results = subjects.map(({ subjectId, readings }) => ({
subjectId,
report: analyzeGlucose(readings),
}))Keeping identity in the containing record limits the analytics input to measurement data. The host still owns authorization and privacy controls.