#Chrono の概要
Chrono は、スケジューラーが時間をどう表現し、経過日数と次の復習日時をどう計算するかを定めます。モデルは日単位の間隔を扱い、Chrono がその間隔をアプリケーションの時刻値に結び付けます。
#プリセットを選ぶ
dateChrono、numericChrono、temporalInstantChrono、defineChrono は ts-fsrs からインポートできます。
| プリセット | 時刻値 | 日数の計算 |
|---|---|---|
dateChrono | JavaScript Date | difference はデフォルトで UTC の日付差を計算し、fractionalDays: true では24時間単位の小数日数を返します。add は常に24時間単位で加算し、小数も扱えます。 |
numericChrono | 日単位の数値 | difference(from, to) は to - from、add(from, days) は from + days です。 |
temporalInstantChrono | Temporal.Instant | タイムゾーンを指定でき、日数差に小数部分を含めるかどうかも選べます。 |
各プリセットの詳細: Date、Numeric、Temporal Instant。
#定義と実行時の操作
Chrono の定義には schema、normalize、defaultValue、create が含まれます。定義を defineScheduler({ model, chrono }) に渡します。時間演算だけを使いたい場合は、定義の create({ config }) を呼び出してください。設定済みの実行時オブジェクトには scheduler.chrono からもアクセスできます。
Date と Temporal の create({ config }) には完全な設定を渡してください。引数なしの create() は Numeric など設定が不要なプリセットで使えます。スケジューラーの生成時には設定の検証と schema のデフォルト値の補完が行われます。
| 操作 | 役割 |
|---|---|
now() | 呼び出し側が now を省略したときに時刻値を返します。 |
difference(from, to) | モデルが使う経過日数を返します。 |
add(from, days) | 最終的な日数間隔を次の復習日時に変換します。 |
compare(left, right) | 正確な前後関係を負数・ゼロ・正数で返す任意の操作です。Date と Temporal のプリセットで利用できます。 |
#normalize の役割
normalize は ChronoTimeNormalizer 型の Standard Schema で、出力型は ChronoNormalizedTime<Time>({ previous: Time | null, current: Time })です。defineChrono は互換性のあるスキーマ、または { value } か { issues } を返す検証コールバックを受け取ります。previous: null は前回のイベントがないことを表します。
normalize は Chrono 固有の時刻フィールドを { previous, current } に変換します。これにより、スケジューラーは card や revlog のフィールド名を知らなくても、経過日数の計算やロールバック時の時刻フィールドの復元ができます。復習時には { card, time: now }(card フィールドを持たない Chrono では { time } のみ)、ロールバック時には { revlog } を受け取ります。
Date と Temporal では、previous は lastReviewAt、current は dueAt であり、今回の復習時刻 now ではありません。Numeric は時刻フィールドを保存せず、{ previous: 0, current: time } を返します。具体的な対応は Chrono の実装が決めます。復習とロールバックの流れは Date を参照してください。
normalize は時刻表現を読み取り、defaultValue はスケジューラーが渡す時刻値を具体的な card と revlog のフィールドに書き戻します。この組み合わせにより、スケジューラーを保存先のフィールド名から切り離せます。
#defineChrono で経過日数をカスタマイズする
以下の例は Date プリセットの schema、normalize、デフォルト値、正確な時刻比較をそのまま使い、経過時間だけを24時間単位の小数日数に変更します。これらを再利用することで、入力検証と card/revlog のフィールドの整合性を保てます。保存するフィールドを変える場合は、対応する schema、復習とロールバックの normalize、デフォルト値をまとめて定義してください。
小数日数だけが必要なら、組み込みの dateChrono.create({ config: { fractionalDays: true } }) で対応できます。以下は 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
)
)
同じ2時間の差でも、UTC 日付のプリセットでは 1、カスタム Chrono では 0.08333333333333333 になります。add(from, 0.5) は12時間を加算し、1月2日 11:00 UTC を返します。
このカスタム Chrono は固定の経過時間を計算し、現地の暦日や夏時間は扱いません。こうした暦のルールが必要な場合は Temporal プリセットを選んでください。モデルとスケジューリングポリシーの組み合わせ方はアーキテクチャを参照してください。