#Chrono overview
Chrono defines how a scheduler represents time, measures elapsed days, and calculates the next due time. The model works with intervals in days; Chrono connects those intervals to your application's time values.
#Choose a preset
Import dateChrono, numericChrono, temporalInstantChrono, and defineChrono from ts-fsrs.
| Preset | Time values | Day arithmetic |
|---|---|---|
dateChrono | JavaScript Date | difference counts UTC date boundaries by default; with fractionalDays: true, it measures elapsed time in fixed 24-hour days. add always adds fixed 24-hour days, including fractions. |
numericChrono | Numbers in days | difference(from, to) is to - from; add(from, days) is from + days. |
temporalInstantChrono | Temporal.Instant | Supports a configured timezone and optional fractional day differences. |
See the preset guides for details: Date, Numeric, and Temporal Instant.
#Definition and runtime operations
A Chrono definition contains schema, normalize, defaultValue, and create. Pass the definition to defineScheduler({ model, chrono }); call its create({ config }) when you need the time operations directly. The scheduler also exposes its configured runtime as scheduler.chrono.
For Date and Temporal, pass the complete configuration to create({ config }); only presets without configuration, such as Numeric, support create() without arguments. Scheduler creation validates configuration and supplies schema defaults.
| Operation | Meaning |
|---|---|
now() | Supplies a time value when the caller omits now. |
difference(from, to) | Returns elapsed days for the model. |
add(from, days) | Converts the final interval in days into a due time. |
compare(left, right) | Optional exact ordering: negative, zero, or positive. Date and Temporal presets provide it. |
#What normalize does
normalize is a ChronoTimeNormalizer Standard Schema whose output is ChronoNormalizedTime<Time>: { previous: Time | null, current: Time }. defineChrono accepts either a compatible schema or a validation callback returning { value } or { issues }. previous: null means there is no previous event.
normalize translates a Chrono's concrete time fields into { previous, current }, so the scheduler can calculate elapsed days and restore time fields during rollback without knowing the card or revlog field names. During review it receives { card, time: now } (or only { time } for a Chrono without card fields); during rollback it receives { revlog }.
For Date and Temporal, previous is lastReviewAt and current is dueAt, not the current review time now. Numeric has no stored time fields and returns { previous: 0, current: time }. The mapping belongs to the Chrono implementation; see Date for the review and rollback flow.
normalize reads the time representation; defaultValue writes the scheduler's time values back into concrete card and revlog fields. Together they keep the scheduler independent of the stored field names.
#Customize elapsed days with defineChrono
This example keeps the Date preset's schemas, time normalizer, defaults, and precise comparison, but measures elapsed time in fixed 24-hour days with fractions. Reusing those parts preserves input validation and the card/revlog contract. If you change the stored fields, define matching schemas, review and rollback normalizers, and defaults together.
For fractional elapsed days, the built-in dateChrono.create({ config: { fractionalDays: true } }) already provides this behavior. The following custom definition demonstrates how to override an operation with defineChrono.
import { const dateChrono: Chrono<{
readonly time: SRSSchema<{
input: Date;
output: Date;
}>;
readonly config: SRSSchema<{
input: {
readonly fractionalDays?: boolean | undefined;
};
output: {
readonly fractionalDays: boolean;
};
}>;
readonly fields: {
readonly card: SRSSchema<{
input: {
dueAt: Date;
lastReviewAt?: (Date | null) | undefined;
};
output: {
dueAt: Date;
lastReviewAt: Date | null;
};
}>;
readonly revlog: SRSSchema<{
input: {
dueAt: Date;
lastReviewAt?: (Date | null) | undefined;
reviewTime: Date;
};
output: {
dueAt: Date;
lastReviewAt: Date | null;
reviewTime: Date;
};
}>;
};
}>
dateChrono , function defineChrono<const Schema extends AnyChronoSchema>(definition: ChronoDefinition<Schema>): Chrono<ChronoDefinitionEnv<Schema>> defineChrono } from 'ts-fsrs'
const millisecondsPerDay = 86_400_000
// Keep Date validation, card/revlog fields, and time normalization from the preset.
const const fractionalDateChrono: Chrono<ChronoDefinitionEnv<{
readonly time: SRSSchema<{
input: Date;
output: Date;
}>;
} & {
readonly config: SRSSchema<{
input: {
readonly fractionalDays?: boolean | undefined;
};
output: {
readonly fractionalDays: boolean;
};
}>;
} & {
readonly card: SRSSchema<{
input: {
dueAt: Date;
lastReviewAt?: (Date | null) | undefined;
};
output: {
dueAt: Date;
lastReviewAt: Date | null;
};
}>;
} & {
readonly revlog: SRSSchema<{
input: {
dueAt: Date;
lastReviewAt?: (Date | null) | undefined;
reviewTime: Date;
};
output: {
dueAt: Date;
lastReviewAt: Date | null;
reviewTime: Date;
};
}>;
}>>
fractionalDateChrono = defineChrono<{
readonly time: SRSSchema<{
input: Date;
output: Date;
}>;
} & {
readonly config: SRSSchema<{
input: {
readonly fractionalDays?: boolean | undefined;
};
output: {
readonly fractionalDays: boolean;
};
}>;
} & {
readonly card: SRSSchema<{
input: {
dueAt: Date;
lastReviewAt?: (Date | null) | undefined;
};
output: {
dueAt: Date;
lastReviewAt: Date | null;
};
}>;
} & {
readonly revlog: SRSSchema<{
input: {
dueAt: Date;
lastReviewAt?: (Date | null) | undefined;
reviewTime: Date;
};
output: {
dueAt: Date;
lastReviewAt: Date | null;
reviewTime: Date;
};
}>;
}>(definition: ChronoDefinition<...>): Chrono<...>
defineChrono ({
schema: {
readonly time: SRSSchema<{
input: Date;
output: Date;
}>;
} & {
readonly config: SRSSchema<{
input: {
readonly fractionalDays?: boolean | undefined;
};
output: {
readonly fractionalDays: boolean;
};
}>;
} & {
readonly card: SRSSchema<{
input: {
dueAt: Date;
lastReviewAt?: (Date | null) | undefined;
};
output: {
dueAt: Date;
lastReviewAt: Date | null;
};
}>;
} & {
readonly revlog: SRSSchema<{
input: {
dueAt: Date;
lastReviewAt?: (Date | null) | undefined;
reviewTime: Date;
};
output: {
dueAt: Date;
lastReviewAt: Date | null;
reviewTime: Date;
};
}>;
}
schema : const dateChrono: Chrono<{
readonly time: SRSSchema<{
input: Date;
output: Date;
}>;
readonly config: SRSSchema<{
input: {
readonly fractionalDays?: boolean | undefined;
};
output: {
readonly fractionalDays: boolean;
};
}>;
readonly fields: {
readonly card: SRSSchema<{
input: {
dueAt: Date;
lastReviewAt?: (Date | null) | undefined;
};
output: {
dueAt: Date;
lastReviewAt: Date | null;
};
}>;
readonly revlog: SRSSchema<{
input: {
dueAt: Date;
lastReviewAt?: (Date | null) | undefined;
reviewTime: Date;
};
output: {
dueAt: Date;
lastReviewAt: Date | null;
reviewTime: Date;
};
}>;
};
}>
dateChrono .Chrono<{ readonly time: SRSSchema<{ input: Date; output: Date; }>; readonly config: SRSSchema<{ input: { readonly fractionalDays?: boolean | undefined; }; output: { readonly fractionalDays: boolean; }; }>; readonly fields: { readonly card: SRSSchema<{ input: { dueAt: Date; lastReviewAt?: (Date | null) | undefined; }; output: { dueAt: Date; lastReviewAt: Date | null; }; }>; readonly revlog: SRSSchema<{ input: { dueAt: Date; lastReviewAt?: (Date | null) | undefined; reviewTime: Date; }; output: { dueAt: Date; lastReviewAt: Date | null; reviewTime: Date; }; }>; }; }>.schema: {
readonly time: SRSSchema<{
input: Date;
output: Date;
}>;
} & {
readonly config: SRSSchema<{
input: {
readonly fractionalDays?: boolean | undefined;
};
output: {
readonly fractionalDays: boolean;
};
}>;
} & {
readonly card: SRSSchema<{
input: {
dueAt: Date;
lastReviewAt?: (Date | null) | undefined;
};
output: {
dueAt: Date;
lastReviewAt: Date | null;
};
}>;
} & {
readonly revlog: SRSSchema<{
input: {
dueAt: Date;
lastReviewAt?: (Date | null) | undefined;
reviewTime: Date;
};
output: {
dueAt: Date;
lastReviewAt: Date | null;
reviewTime: Date;
};
}>;
}
schema ,
normalize: ChronoTimeNormalizerDefinition<SRSSchema<{
input: Date;
output: Date;
}>, SRSSchema<{
input: {
dueAt: Date;
lastReviewAt?: (Date | null) | undefined;
};
output: {
dueAt: Date;
lastReviewAt: Date | null;
};
}>, SRSSchema<{
input: {
dueAt: Date;
lastReviewAt?: (Date | null) | undefined;
reviewTime: Date;
};
output: {
dueAt: Date;
lastReviewAt: Date | null;
reviewTime: Date;
};
}>>
normalize : const dateChrono: Chrono<{
readonly time: SRSSchema<{
input: Date;
output: Date;
}>;
readonly config: SRSSchema<{
input: {
readonly fractionalDays?: boolean | undefined;
};
output: {
readonly fractionalDays: boolean;
};
}>;
readonly fields: {
readonly card: SRSSchema<{
input: {
dueAt: Date;
lastReviewAt?: (Date | null) | undefined;
};
output: {
dueAt: Date;
lastReviewAt: Date | null;
};
}>;
readonly revlog: SRSSchema<{
input: {
dueAt: Date;
lastReviewAt?: (Date | null) | undefined;
reviewTime: Date;
};
output: {
dueAt: Date;
lastReviewAt: Date | null;
reviewTime: Date;
};
}>;
};
}>
dateChrono .Chrono<{ readonly time: SRSSchema<{ input: Date; output: Date; }>; readonly config: SRSSchema<{ input: { readonly fractionalDays?: boolean | undefined; }; output: { readonly fractionalDays: boolean; }; }>; readonly fields: { readonly card: SRSSchema<{ input: { dueAt: Date; lastReviewAt?: (Date | null) | undefined; }; output: { dueAt: Date; lastReviewAt: Date | null; }; }>; readonly revlog: SRSSchema<{ input: { dueAt: Date; lastReviewAt?: (Date | null) | undefined; reviewTime: Date; }; output: { dueAt: Date; lastReviewAt: Date | null; reviewTime: Date; }; }>; }; }>.normalize: ChronoTimeNormalizer<{
readonly time: SRSSchema<{
input: Date;
output: Date;
}>;
readonly config: SRSSchema<{
input: {
readonly fractionalDays?: boolean | undefined;
};
output: {
readonly fractionalDays: boolean;
};
}>;
readonly fields: {
readonly card: SRSSchema<{
input: {
dueAt: Date;
lastReviewAt?: (Date | null) | undefined;
};
output: {
dueAt: Date;
lastReviewAt: Date | null;
};
}>;
readonly revlog: SRSSchema<{
input: {
dueAt: Date;
lastReviewAt?: (Date | null) | undefined;
reviewTime: Date;
};
output: {
dueAt: Date;
lastReviewAt: Date | null;
reviewTime: Date;
};
}>;
};
}>
normalize ,
defaultValue?: Readonly<{
readonly card?: FieldDefault<SRSSchema<{
input: {
dueAt: Date;
lastReviewAt?: (Date | null) | undefined;
};
output: {
dueAt: Date;
lastReviewAt: Date | null;
};
}>, ChronoDefaultCtx<{
readonly fractionalDays: boolean;
}, Date, "card">> | undefined;
} & {
readonly revlog?: FieldDefault<SRSSchema<{
input: {
dueAt: Date;
lastReviewAt?: (Date | null) | undefined;
reviewTime: Date;
};
output: {
dueAt: Date;
lastReviewAt: Date | null;
reviewTime: Date;
};
}>, ChronoDefaultCtx<...>> | undefined;
}> | undefined
defaultValue : const dateChrono: Chrono<{
readonly time: SRSSchema<{
input: Date;
output: Date;
}>;
readonly config: SRSSchema<{
input: {
readonly fractionalDays?: boolean | undefined;
};
output: {
readonly fractionalDays: boolean;
};
}>;
readonly fields: {
readonly card: SRSSchema<{
input: {
dueAt: Date;
lastReviewAt?: (Date | null) | undefined;
};
output: {
dueAt: Date;
lastReviewAt: Date | null;
};
}>;
readonly revlog: SRSSchema<{
input: {
dueAt: Date;
lastReviewAt?: (Date | null) | undefined;
reviewTime: Date;
};
output: {
dueAt: Date;
lastReviewAt: Date | null;
reviewTime: Date;
};
}>;
};
}>
dateChrono .Chrono<{ readonly time: SRSSchema<{ input: Date; output: Date; }>; readonly config: SRSSchema<{ input: { readonly fractionalDays?: boolean | undefined; }; output: { readonly fractionalDays: boolean; }; }>; readonly fields: { readonly card: SRSSchema<{ input: { dueAt: Date; lastReviewAt?: (Date | null) | undefined; }; output: { dueAt: Date; lastReviewAt: Date | null; }; }>; readonly revlog: SRSSchema<{ input: { dueAt: Date; lastReviewAt?: (Date | null) | undefined; reviewTime: Date; }; output: { dueAt: Date; lastReviewAt: Date | null; reviewTime: Date; }; }>; }; }>.defaultValue: Readonly<{
readonly card?: FieldDefault<SRSSchema<{
input: {
dueAt: Date;
lastReviewAt?: (Date | null) | undefined;
};
output: {
dueAt: Date;
lastReviewAt: Date | null;
};
}>, ChronoDefaultCtx<{
readonly fractionalDays: boolean;
}, Date, "card">> | undefined;
} & {
readonly revlog?: FieldDefault<SRSSchema<{
input: {
dueAt: Date;
lastReviewAt?: (Date | null) | undefined;
reviewTime: Date;
};
output: {
dueAt: Date;
lastReviewAt: Date | null;
reviewTime: Date;
};
}>, ChronoDefaultCtx<...>> | undefined;
}>
defaultValue ,
create: (ctx: {
readonly config: {
readonly fractionalDays: boolean;
};
}) => ChronoCore<Date>
create (ctx) {
return {
...const dateChrono: Chrono<{
readonly time: SRSSchema<{
input: Date;
output: Date;
}>;
readonly config: SRSSchema<{
input: {
readonly fractionalDays?: boolean | undefined;
};
output: {
readonly fractionalDays: boolean;
};
}>;
readonly fields: {
readonly card: SRSSchema<{
input: {
dueAt: Date;
lastReviewAt?: (Date | null) | undefined;
};
output: {
dueAt: Date;
lastReviewAt: Date | null;
};
}>;
readonly revlog: SRSSchema<{
input: {
dueAt: Date;
lastReviewAt?: (Date | null) | undefined;
reviewTime: Date;
};
output: {
dueAt: Date;
lastReviewAt: Date | null;
reviewTime: Date;
};
}>;
};
}>
dateChrono .Chrono<{ readonly time: SRSSchema<{ input: Date; output: Date; }>; readonly config: SRSSchema<{ input: { readonly fractionalDays?: boolean | undefined; }; output: { readonly fractionalDays: boolean; }; }>; readonly fields: { readonly card: SRSSchema<{ input: { dueAt: Date; lastReviewAt?: (Date | null) | undefined; }; output: { dueAt: Date; lastReviewAt: Date | null; }; }>; readonly revlog: SRSSchema<{ input: { dueAt: Date; lastReviewAt?: (Date | null) | undefined; reviewTime: Date; }; output: { dueAt: Date; lastReviewAt: Date | null; reviewTime: Date; }; }>; }; }>.create: (ctx: {
readonly config: {
readonly fractionalDays: boolean;
};
}) => ChronoCore<Date>
create (ctx),
ChronoCore<Date>.difference: (from: Date, to: Date) => numberReturns the distance from the previous review time to the current time.
difference : (from, to) =>
(to.getTime() - from.getTime()) / millisecondsPerDay,
}
},
})
const from = new Date('2026-01-01T23:00:00.000Z')
const now = new Date('2026-01-02T01:00:00.000Z')
const const clock: ChronoCore<Date> clock = const fractionalDateChrono: Chrono<ChronoDefinitionEnv<{
readonly time: SRSSchema<{
input: Date;
output: Date;
}>;
} & {
readonly config: SRSSchema<{
input: {
readonly fractionalDays?: boolean | undefined;
};
output: {
readonly fractionalDays: boolean;
};
}>;
} & {
readonly card: SRSSchema<{
input: {
dueAt: Date;
lastReviewAt?: (Date | null) | undefined;
};
output: {
dueAt: Date;
lastReviewAt: Date | null;
};
}>;
} & {
readonly revlog: SRSSchema<{
input: {
dueAt: Date;
lastReviewAt?: (Date | null) | undefined;
reviewTime: Date;
};
output: {
dueAt: Date;
lastReviewAt: Date | null;
reviewTime: Date;
};
}>;
}>>
fractionalDateChrono .Chrono<ChronoDefinitionEnv<{ readonly time: SRSSchema<{ input: Date; output: Date; }>; } & { readonly config: SRSSchema<{ input: { readonly fractionalDays?: boolean | undefined; }; output: { readonly fractionalDays: boolean; }; }>; } & { ...; } & { ...; }>>.create: (ctx: {
readonly config: {
readonly fractionalDays: boolean;
};
}) => ChronoCore<Date>
create ({ config: { fractionalDays: false } })
console.log(
JSON.stringify(
{
utcDateDifference: const dateChrono: Chrono<{
readonly time: SRSSchema<{
input: Date;
output: Date;
}>;
readonly config: SRSSchema<{
input: {
readonly fractionalDays?: boolean | undefined;
};
output: {
readonly fractionalDays: boolean;
};
}>;
readonly fields: {
readonly card: SRSSchema<{
input: {
dueAt: Date;
lastReviewAt?: (Date | null) | undefined;
};
output: {
dueAt: Date;
lastReviewAt: Date | null;
};
}>;
readonly revlog: SRSSchema<{
input: {
dueAt: Date;
lastReviewAt?: (Date | null) | undefined;
reviewTime: Date;
};
output: {
dueAt: Date;
lastReviewAt: Date | null;
reviewTime: Date;
};
}>;
};
}>
dateChrono
.Chrono<{ readonly time: SRSSchema<{ input: Date; output: Date; }>; readonly config: SRSSchema<{ input: { readonly fractionalDays?: boolean | undefined; }; output: { readonly fractionalDays: boolean; }; }>; readonly fields: { readonly card: SRSSchema<{ input: { dueAt: Date; lastReviewAt?: (Date | null) | undefined; }; output: { dueAt: Date; lastReviewAt: Date | null; }; }>; readonly revlog: SRSSchema<{ input: { dueAt: Date; lastReviewAt?: (Date | null) | undefined; reviewTime: Date; }; output: { dueAt: Date; lastReviewAt: Date | null; reviewTime: Date; }; }>; }; }>.create: (ctx: {
readonly config: {
readonly fractionalDays: boolean;
};
}) => ChronoCore<Date>
create ({ config: { fractionalDays: false } })
.ChronoCore<Date>.difference: (from: Date, to: Date) => numberReturns the distance from the previous review time to the current time.
difference (from, now),
fractionalDifference: const clock: ChronoCore<Date> clock .ChronoCore<Date>.difference: (from: Date, to: Date) => numberReturns the distance from the previous review time to the current time.
difference (from, now),
halfDayLater: const clock: ChronoCore<Date> clock .ChronoCore<Date>.add: (from: Date, days: number) => Date add (from, 0.5).toISOString(),
},
null,
2
)
)
The two-hour gap yields 1 with the UTC-date preset and 0.08333333333333333 with the custom Chrono. Calling add(from, 0.5) adds twelve hours, giving January 2 at 11:00 UTC.
This custom Chrono measures fixed durations; it does not account for local calendar days or daylight saving. Choose the Temporal preset when those calendar rules are needed. See Architecture for choosing the model and scheduling policies.