#Chrono 概覽
Chrono 決定排程器如何表示時間、計算經過的天數,以及確定下次到期時間。模型使用以天為單位的間隔,Chrono 負責把這些間隔與應用程式中的時間值連接起來。
#選擇預設
從 ts-fsrs 匯入 dateChrono、numericChrono、temporalInstantChrono 和 defineChrono。
| 預設 | 時間值 | 天數運算 |
|---|---|---|
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 }) 傳入完整設定;只有 Numeric 等不需要設定的預設可以無參數呼叫 create()。透過排程器建立實例時,會驗證設定並補齊 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 接受相容的 schema,也接受回傳 { 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
)
)
同樣的兩小時時間差,UTC 日期預設得到 1,自訂 Chrono 得到 0.08333333333333333。呼叫 add(from, 0.5) 增加十二小時,得到 1 月 2 日 11:00 UTC。
這個自訂 Chrono 計算固定時長,不處理當地日曆天或日光節約時間。如果需要這些日曆規則,選擇 Temporal 預設。模型與排程策略的組合方式見架構。