#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 预设。模型与调度策略的组合方式见架构。