ts-fsrs 5.3.2 → 5.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +481 -0
- package/dist/index.cjs +67 -59
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +67 -59
- package/dist/index.mjs.map +1 -1
- package/dist/index.umd.js +69 -59
- package/dist/index.umd.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
class FSRSError extends Error {
|
|
4
|
+
constructor(message = "FSRS Error") {
|
|
5
|
+
super(message);
|
|
6
|
+
this.name = "FSRSError";
|
|
7
|
+
Error.captureStackTrace?.(this, FSRSError);
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
class FSRSValidationError extends FSRSError {
|
|
11
|
+
constructor(message) {
|
|
12
|
+
super(message);
|
|
13
|
+
this.name = "FSRSValidationError";
|
|
14
|
+
Error.captureStackTrace?.(this, FSRSValidationError);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
3
18
|
var State = /* @__PURE__ */ ((State2) => {
|
|
4
19
|
State2[State2["New"] = 0] = "New";
|
|
5
20
|
State2[State2["Learning"] = 1] = "Learning";
|
|
@@ -31,13 +46,13 @@ class TypeConvert {
|
|
|
31
46
|
const restOfString = value.slice(1).toLowerCase();
|
|
32
47
|
const ret = Rating[`${firstLetter}${restOfString}`];
|
|
33
48
|
if (ret === void 0) {
|
|
34
|
-
throw new
|
|
49
|
+
throw new FSRSValidationError(`Invalid rating:[${value}]`);
|
|
35
50
|
}
|
|
36
51
|
return ret;
|
|
37
52
|
} else if (typeof value === "number") {
|
|
38
53
|
return value;
|
|
39
54
|
}
|
|
40
|
-
throw new
|
|
55
|
+
throw new FSRSValidationError(`Invalid rating:[${value}]`);
|
|
41
56
|
}
|
|
42
57
|
static state(value) {
|
|
43
58
|
if (typeof value === "string") {
|
|
@@ -45,13 +60,13 @@ class TypeConvert {
|
|
|
45
60
|
const restOfString = value.slice(1).toLowerCase();
|
|
46
61
|
const ret = State[`${firstLetter}${restOfString}`];
|
|
47
62
|
if (ret === void 0) {
|
|
48
|
-
throw new
|
|
63
|
+
throw new FSRSValidationError(`Invalid state:[${value}]`);
|
|
49
64
|
}
|
|
50
65
|
return ret;
|
|
51
66
|
} else if (typeof value === "number") {
|
|
52
67
|
return value;
|
|
53
68
|
}
|
|
54
|
-
throw new
|
|
69
|
+
throw new FSRSValidationError(`Invalid state:[${value}]`);
|
|
55
70
|
}
|
|
56
71
|
static time(value) {
|
|
57
72
|
if (value instanceof Date) {
|
|
@@ -65,12 +80,12 @@ class TypeConvert {
|
|
|
65
80
|
if (!Number.isNaN(timestamp)) {
|
|
66
81
|
return new Date(timestamp);
|
|
67
82
|
} else {
|
|
68
|
-
throw new
|
|
83
|
+
throw new FSRSValidationError(`Invalid date:[${value}]`);
|
|
69
84
|
}
|
|
70
85
|
} else if (typeof value === "number") {
|
|
71
86
|
return new Date(value);
|
|
72
87
|
}
|
|
73
|
-
throw new
|
|
88
|
+
throw new FSRSValidationError(`Invalid date:[${value}]`);
|
|
74
89
|
}
|
|
75
90
|
static review_log(log) {
|
|
76
91
|
return {
|
|
@@ -106,7 +121,7 @@ function date_scheduler(now, t, isDay) {
|
|
|
106
121
|
}
|
|
107
122
|
function date_diff(now, pre, unit) {
|
|
108
123
|
if (!now || !pre) {
|
|
109
|
-
throw new
|
|
124
|
+
throw new FSRSValidationError("Invalid date");
|
|
110
125
|
}
|
|
111
126
|
const diff = TypeConvert.time(now).getTime() - TypeConvert.time(pre).getTime();
|
|
112
127
|
let r = 0;
|
|
@@ -232,7 +247,7 @@ const ConvertStepUnitToMinutes = (step) => {
|
|
|
232
247
|
const unit = step.slice(-1);
|
|
233
248
|
const value = parseInt(step.slice(0, -1), 10);
|
|
234
249
|
if (Number.isNaN(value) || !Number.isFinite(value) || value < 0) {
|
|
235
|
-
throw new
|
|
250
|
+
throw new FSRSValidationError(`Invalid step value: ${step}`);
|
|
236
251
|
}
|
|
237
252
|
switch (unit) {
|
|
238
253
|
case "m":
|
|
@@ -242,7 +257,9 @@ const ConvertStepUnitToMinutes = (step) => {
|
|
|
242
257
|
case "d":
|
|
243
258
|
return value * 1440;
|
|
244
259
|
default:
|
|
245
|
-
throw new
|
|
260
|
+
throw new FSRSValidationError(
|
|
261
|
+
`Invalid step unit: ${step}, expected m/h/d`
|
|
262
|
+
);
|
|
246
263
|
}
|
|
247
264
|
};
|
|
248
265
|
const BasicLearningStepsStrategy = (params, state, cur_step) => {
|
|
@@ -339,8 +356,8 @@ class AbstractScheduler {
|
|
|
339
356
|
this.init();
|
|
340
357
|
}
|
|
341
358
|
checkGrade(grade) {
|
|
342
|
-
if (!Number.isFinite(grade) || grade <
|
|
343
|
-
throw new
|
|
359
|
+
if (!Number.isFinite(grade) || grade < 1 || grade > 4) {
|
|
360
|
+
throw new FSRSValidationError(`Invalid grade "${grade}",expected 1-4`);
|
|
344
361
|
}
|
|
345
362
|
}
|
|
346
363
|
init() {
|
|
@@ -483,7 +500,7 @@ function alea(seed) {
|
|
|
483
500
|
return prng;
|
|
484
501
|
}
|
|
485
502
|
|
|
486
|
-
const version="5.
|
|
503
|
+
const version="5.4.0";
|
|
487
504
|
|
|
488
505
|
const default_request_retention = 0.9;
|
|
489
506
|
const default_maximum_interval = 36500;
|
|
@@ -554,27 +571,31 @@ const CLAMP_PARAMETERS = (w17_w18_ceiling, enable_short_term = default_enable_sh
|
|
|
554
571
|
];
|
|
555
572
|
|
|
556
573
|
const clipParameters = (parameters, numRelearningSteps, enableShortTerm = default_enable_short_term) => {
|
|
557
|
-
|
|
558
|
-
if (Math.max(0, numRelearningSteps) > 1) {
|
|
559
|
-
const value = -(Math.log(parameters[11]) + Math.log(Math.pow(2, parameters[13]) - 1) + parameters[14] * 0.3) / numRelearningSteps;
|
|
560
|
-
w17_w18_ceiling = clamp(+value.toFixed(8), 0.01, 2);
|
|
561
|
-
}
|
|
562
|
-
const clip = CLAMP_PARAMETERS(w17_w18_ceiling, enableShortTerm).slice(
|
|
574
|
+
const clip = CLAMP_PARAMETERS(W17_W18_Ceiling, enableShortTerm).slice(
|
|
563
575
|
0,
|
|
564
576
|
parameters.length
|
|
565
577
|
);
|
|
578
|
+
if (Math.max(0, numRelearningSteps) > 1) {
|
|
579
|
+
const w11 = clamp(parameters[11] || 0, clip[11][0], clip[11][1]);
|
|
580
|
+
const w13 = clamp(parameters[13] || 0, clip[13][0], clip[13][1]);
|
|
581
|
+
const w14 = clamp(parameters[14] || 0, clip[14][0], clip[14][1]);
|
|
582
|
+
const value = -(Math.log(w11) + Math.log(Math.pow(2, w13) - 1) + w14 * 0.3) / numRelearningSteps;
|
|
583
|
+
const w17_w18_ceiling = clamp(roundTo(value, 8), 0.01, W17_W18_Ceiling);
|
|
584
|
+
if (clip[17]) clip[17] = [clip[17][0], w17_w18_ceiling];
|
|
585
|
+
if (clip[18]) clip[18] = [clip[18][0], w17_w18_ceiling];
|
|
586
|
+
}
|
|
566
587
|
return clip.map(
|
|
567
588
|
([min, max], index) => clamp(parameters[index] || 0, min, max)
|
|
568
589
|
);
|
|
569
590
|
};
|
|
570
591
|
const checkParameters = (parameters) => {
|
|
571
|
-
const invalid = parameters.find(
|
|
572
|
-
(param) => !Number.isFinite(param) && !Number.isNaN(param)
|
|
573
|
-
);
|
|
592
|
+
const invalid = parameters.find((param) => !Number.isFinite(param));
|
|
574
593
|
if (invalid !== void 0) {
|
|
575
|
-
throw
|
|
594
|
+
throw new FSRSValidationError(
|
|
595
|
+
`Non-finite or NaN value in parameters ${parameters}`
|
|
596
|
+
);
|
|
576
597
|
} else if (![17, 19, 21].includes(parameters.length)) {
|
|
577
|
-
throw
|
|
598
|
+
throw new FSRSValidationError(
|
|
578
599
|
`Invalid parameter length: ${parameters.length}. Must be 17, 19 or 21 for FSRSv4, 5 and 6 respectively.`
|
|
579
600
|
);
|
|
580
601
|
}
|
|
@@ -692,7 +713,9 @@ class FSRSAlgorithm {
|
|
|
692
713
|
*/
|
|
693
714
|
calculate_interval_modifier(request_retention) {
|
|
694
715
|
if (request_retention <= 0 || request_retention > 1) {
|
|
695
|
-
throw new
|
|
716
|
+
throw new FSRSValidationError(
|
|
717
|
+
"Requested retention rate should be in the range (0,1]"
|
|
718
|
+
);
|
|
696
719
|
}
|
|
697
720
|
const { decay, factor } = computeDecayFactor(this.param.w);
|
|
698
721
|
return roundTo((Math.pow(request_retention, 1 / decay) - 1) / factor, 8);
|
|
@@ -908,10 +931,10 @@ class FSRSAlgorithm {
|
|
|
908
931
|
stability: 0
|
|
909
932
|
};
|
|
910
933
|
if (t < 0) {
|
|
911
|
-
throw new
|
|
934
|
+
throw new FSRSValidationError(`Invalid delta_t "${t}"`);
|
|
912
935
|
}
|
|
913
936
|
if (g < 0 || g > 4) {
|
|
914
|
-
throw new
|
|
937
|
+
throw new FSRSValidationError(`Invalid grade "${g}"`);
|
|
915
938
|
}
|
|
916
939
|
if (d === 0 && s === 0) {
|
|
917
940
|
return {
|
|
@@ -926,7 +949,7 @@ class FSRSAlgorithm {
|
|
|
926
949
|
};
|
|
927
950
|
}
|
|
928
951
|
if (d < 1 || s < S_MIN) {
|
|
929
|
-
throw new
|
|
952
|
+
throw new FSRSValidationError(
|
|
930
953
|
`Invalid memory state { difficulty: ${d}, stability: ${s} }`
|
|
931
954
|
);
|
|
932
955
|
}
|
|
@@ -1307,7 +1330,9 @@ class Reschedule {
|
|
|
1307
1330
|
*/
|
|
1308
1331
|
handleManualRating(card, state, reviewed, elapsed_days, stability, difficulty, due) {
|
|
1309
1332
|
if (typeof state === "undefined") {
|
|
1310
|
-
throw new
|
|
1333
|
+
throw new FSRSValidationError(
|
|
1334
|
+
"reschedule: state is required for manual rating"
|
|
1335
|
+
);
|
|
1311
1336
|
}
|
|
1312
1337
|
let log;
|
|
1313
1338
|
let next_card;
|
|
@@ -1328,7 +1353,9 @@ class Reschedule {
|
|
|
1328
1353
|
next_card.last_review = reviewed;
|
|
1329
1354
|
} else {
|
|
1330
1355
|
if (typeof due === "undefined") {
|
|
1331
|
-
throw new
|
|
1356
|
+
throw new FSRSValidationError(
|
|
1357
|
+
"reschedule: due is required for manual rating"
|
|
1358
|
+
);
|
|
1332
1359
|
}
|
|
1333
1360
|
const scheduled_days = date_diff(due, reviewed, "days");
|
|
1334
1361
|
log = {
|
|
@@ -1418,6 +1445,9 @@ class Reschedule {
|
|
|
1418
1445
|
}
|
|
1419
1446
|
}
|
|
1420
1447
|
|
|
1448
|
+
function applyAfterHandler(value, afterHandler) {
|
|
1449
|
+
return typeof afterHandler === "function" ? afterHandler(value) : value;
|
|
1450
|
+
}
|
|
1421
1451
|
class FSRS extends FSRSAlgorithm {
|
|
1422
1452
|
strategyHandler = /* @__PURE__ */ new Map();
|
|
1423
1453
|
Scheduler;
|
|
@@ -1533,11 +1563,7 @@ class FSRS extends FSRSAlgorithm {
|
|
|
1533
1563
|
repeat(card, now, afterHandler) {
|
|
1534
1564
|
const instance = this.getScheduler(card, now);
|
|
1535
1565
|
const recordLog = instance.preview();
|
|
1536
|
-
|
|
1537
|
-
return afterHandler(recordLog);
|
|
1538
|
-
} else {
|
|
1539
|
-
return recordLog;
|
|
1540
|
-
}
|
|
1566
|
+
return applyAfterHandler(recordLog, afterHandler);
|
|
1541
1567
|
}
|
|
1542
1568
|
/**
|
|
1543
1569
|
* Display the collection of cards and logs for the card scheduled at the current time, after applying a specific grade rating.
|
|
@@ -1597,14 +1623,10 @@ class FSRS extends FSRSAlgorithm {
|
|
|
1597
1623
|
const instance = this.getScheduler(card, now);
|
|
1598
1624
|
const g = TypeConvert.rating(grade);
|
|
1599
1625
|
if (g === Rating.Manual) {
|
|
1600
|
-
throw new
|
|
1626
|
+
throw new FSRSValidationError("Cannot review a manual rating");
|
|
1601
1627
|
}
|
|
1602
1628
|
const recordLogItem = instance.review(g);
|
|
1603
|
-
|
|
1604
|
-
return afterHandler(recordLogItem);
|
|
1605
|
-
} else {
|
|
1606
|
-
return recordLogItem;
|
|
1607
|
-
}
|
|
1629
|
+
return applyAfterHandler(recordLogItem, afterHandler);
|
|
1608
1630
|
}
|
|
1609
1631
|
/**
|
|
1610
1632
|
* Get the retrievability of the card
|
|
@@ -1649,7 +1671,7 @@ class FSRS extends FSRSAlgorithm {
|
|
|
1649
1671
|
const processedCard = TypeConvert.card(card);
|
|
1650
1672
|
const processedLog = TypeConvert.review_log(log);
|
|
1651
1673
|
if (processedLog.rating === Rating.Manual) {
|
|
1652
|
-
throw new
|
|
1674
|
+
throw new FSRSValidationError("Cannot rollback a manual rating");
|
|
1653
1675
|
}
|
|
1654
1676
|
let last_due;
|
|
1655
1677
|
let last_review;
|
|
@@ -1681,11 +1703,7 @@ class FSRS extends FSRSAlgorithm {
|
|
|
1681
1703
|
state: processedLog.state,
|
|
1682
1704
|
last_review
|
|
1683
1705
|
};
|
|
1684
|
-
|
|
1685
|
-
return afterHandler(prevCard);
|
|
1686
|
-
} else {
|
|
1687
|
-
return prevCard;
|
|
1688
|
-
}
|
|
1706
|
+
return applyAfterHandler(prevCard, afterHandler);
|
|
1689
1707
|
}
|
|
1690
1708
|
/**
|
|
1691
1709
|
*
|
|
@@ -1768,11 +1786,7 @@ class FSRS extends FSRSAlgorithm {
|
|
|
1768
1786
|
last_review: processedCard.last_review
|
|
1769
1787
|
};
|
|
1770
1788
|
const recordLogItem = { card: forget_card, log: forget_log };
|
|
1771
|
-
|
|
1772
|
-
return afterHandler(recordLogItem);
|
|
1773
|
-
} else {
|
|
1774
|
-
return recordLogItem;
|
|
1775
|
-
}
|
|
1789
|
+
return applyAfterHandler(recordLogItem, afterHandler);
|
|
1776
1790
|
}
|
|
1777
1791
|
/**
|
|
1778
1792
|
* Reschedules the current card and returns the rescheduled collections and reschedule item.
|
|
@@ -1839,15 +1853,9 @@ class FSRS extends FSRSAlgorithm {
|
|
|
1839
1853
|
len ? collections[len - 1] : void 0,
|
|
1840
1854
|
updateMemoryState
|
|
1841
1855
|
);
|
|
1842
|
-
if (recordLogHandler && typeof recordLogHandler === "function") {
|
|
1843
|
-
return {
|
|
1844
|
-
collections: collections.map(recordLogHandler),
|
|
1845
|
-
reschedule_item: manual_item ? recordLogHandler(manual_item) : null
|
|
1846
|
-
};
|
|
1847
|
-
}
|
|
1848
1856
|
return {
|
|
1849
|
-
collections,
|
|
1850
|
-
reschedule_item: manual_item
|
|
1857
|
+
collections: typeof recordLogHandler === "function" ? collections.map(recordLogHandler) : collections,
|
|
1858
|
+
reschedule_item: manual_item ? applyAfterHandler(manual_item, recordLogHandler) : null
|
|
1851
1859
|
};
|
|
1852
1860
|
}
|
|
1853
1861
|
}
|