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.mjs
CHANGED
|
@@ -1,3 +1,18 @@
|
|
|
1
|
+
class FSRSError extends Error {
|
|
2
|
+
constructor(message = "FSRS Error") {
|
|
3
|
+
super(message);
|
|
4
|
+
this.name = "FSRSError";
|
|
5
|
+
Error.captureStackTrace?.(this, FSRSError);
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
class FSRSValidationError extends FSRSError {
|
|
9
|
+
constructor(message) {
|
|
10
|
+
super(message);
|
|
11
|
+
this.name = "FSRSValidationError";
|
|
12
|
+
Error.captureStackTrace?.(this, FSRSValidationError);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
1
16
|
var State = /* @__PURE__ */ ((State2) => {
|
|
2
17
|
State2[State2["New"] = 0] = "New";
|
|
3
18
|
State2[State2["Learning"] = 1] = "Learning";
|
|
@@ -29,13 +44,13 @@ class TypeConvert {
|
|
|
29
44
|
const restOfString = value.slice(1).toLowerCase();
|
|
30
45
|
const ret = Rating[`${firstLetter}${restOfString}`];
|
|
31
46
|
if (ret === void 0) {
|
|
32
|
-
throw new
|
|
47
|
+
throw new FSRSValidationError(`Invalid rating:[${value}]`);
|
|
33
48
|
}
|
|
34
49
|
return ret;
|
|
35
50
|
} else if (typeof value === "number") {
|
|
36
51
|
return value;
|
|
37
52
|
}
|
|
38
|
-
throw new
|
|
53
|
+
throw new FSRSValidationError(`Invalid rating:[${value}]`);
|
|
39
54
|
}
|
|
40
55
|
static state(value) {
|
|
41
56
|
if (typeof value === "string") {
|
|
@@ -43,13 +58,13 @@ class TypeConvert {
|
|
|
43
58
|
const restOfString = value.slice(1).toLowerCase();
|
|
44
59
|
const ret = State[`${firstLetter}${restOfString}`];
|
|
45
60
|
if (ret === void 0) {
|
|
46
|
-
throw new
|
|
61
|
+
throw new FSRSValidationError(`Invalid state:[${value}]`);
|
|
47
62
|
}
|
|
48
63
|
return ret;
|
|
49
64
|
} else if (typeof value === "number") {
|
|
50
65
|
return value;
|
|
51
66
|
}
|
|
52
|
-
throw new
|
|
67
|
+
throw new FSRSValidationError(`Invalid state:[${value}]`);
|
|
53
68
|
}
|
|
54
69
|
static time(value) {
|
|
55
70
|
if (value instanceof Date) {
|
|
@@ -63,12 +78,12 @@ class TypeConvert {
|
|
|
63
78
|
if (!Number.isNaN(timestamp)) {
|
|
64
79
|
return new Date(timestamp);
|
|
65
80
|
} else {
|
|
66
|
-
throw new
|
|
81
|
+
throw new FSRSValidationError(`Invalid date:[${value}]`);
|
|
67
82
|
}
|
|
68
83
|
} else if (typeof value === "number") {
|
|
69
84
|
return new Date(value);
|
|
70
85
|
}
|
|
71
|
-
throw new
|
|
86
|
+
throw new FSRSValidationError(`Invalid date:[${value}]`);
|
|
72
87
|
}
|
|
73
88
|
static review_log(log) {
|
|
74
89
|
return {
|
|
@@ -104,7 +119,7 @@ function date_scheduler(now, t, isDay) {
|
|
|
104
119
|
}
|
|
105
120
|
function date_diff(now, pre, unit) {
|
|
106
121
|
if (!now || !pre) {
|
|
107
|
-
throw new
|
|
122
|
+
throw new FSRSValidationError("Invalid date");
|
|
108
123
|
}
|
|
109
124
|
const diff = TypeConvert.time(now).getTime() - TypeConvert.time(pre).getTime();
|
|
110
125
|
let r = 0;
|
|
@@ -230,7 +245,7 @@ const ConvertStepUnitToMinutes = (step) => {
|
|
|
230
245
|
const unit = step.slice(-1);
|
|
231
246
|
const value = parseInt(step.slice(0, -1), 10);
|
|
232
247
|
if (Number.isNaN(value) || !Number.isFinite(value) || value < 0) {
|
|
233
|
-
throw new
|
|
248
|
+
throw new FSRSValidationError(`Invalid step value: ${step}`);
|
|
234
249
|
}
|
|
235
250
|
switch (unit) {
|
|
236
251
|
case "m":
|
|
@@ -240,7 +255,9 @@ const ConvertStepUnitToMinutes = (step) => {
|
|
|
240
255
|
case "d":
|
|
241
256
|
return value * 1440;
|
|
242
257
|
default:
|
|
243
|
-
throw new
|
|
258
|
+
throw new FSRSValidationError(
|
|
259
|
+
`Invalid step unit: ${step}, expected m/h/d`
|
|
260
|
+
);
|
|
244
261
|
}
|
|
245
262
|
};
|
|
246
263
|
const BasicLearningStepsStrategy = (params, state, cur_step) => {
|
|
@@ -337,8 +354,8 @@ class AbstractScheduler {
|
|
|
337
354
|
this.init();
|
|
338
355
|
}
|
|
339
356
|
checkGrade(grade) {
|
|
340
|
-
if (!Number.isFinite(grade) || grade <
|
|
341
|
-
throw new
|
|
357
|
+
if (!Number.isFinite(grade) || grade < 1 || grade > 4) {
|
|
358
|
+
throw new FSRSValidationError(`Invalid grade "${grade}",expected 1-4`);
|
|
342
359
|
}
|
|
343
360
|
}
|
|
344
361
|
init() {
|
|
@@ -481,7 +498,7 @@ function alea(seed) {
|
|
|
481
498
|
return prng;
|
|
482
499
|
}
|
|
483
500
|
|
|
484
|
-
const version="5.
|
|
501
|
+
const version="5.4.0";
|
|
485
502
|
|
|
486
503
|
const default_request_retention = 0.9;
|
|
487
504
|
const default_maximum_interval = 36500;
|
|
@@ -552,27 +569,31 @@ const CLAMP_PARAMETERS = (w17_w18_ceiling, enable_short_term = default_enable_sh
|
|
|
552
569
|
];
|
|
553
570
|
|
|
554
571
|
const clipParameters = (parameters, numRelearningSteps, enableShortTerm = default_enable_short_term) => {
|
|
555
|
-
|
|
556
|
-
if (Math.max(0, numRelearningSteps) > 1) {
|
|
557
|
-
const value = -(Math.log(parameters[11]) + Math.log(Math.pow(2, parameters[13]) - 1) + parameters[14] * 0.3) / numRelearningSteps;
|
|
558
|
-
w17_w18_ceiling = clamp(+value.toFixed(8), 0.01, 2);
|
|
559
|
-
}
|
|
560
|
-
const clip = CLAMP_PARAMETERS(w17_w18_ceiling, enableShortTerm).slice(
|
|
572
|
+
const clip = CLAMP_PARAMETERS(W17_W18_Ceiling, enableShortTerm).slice(
|
|
561
573
|
0,
|
|
562
574
|
parameters.length
|
|
563
575
|
);
|
|
576
|
+
if (Math.max(0, numRelearningSteps) > 1) {
|
|
577
|
+
const w11 = clamp(parameters[11] || 0, clip[11][0], clip[11][1]);
|
|
578
|
+
const w13 = clamp(parameters[13] || 0, clip[13][0], clip[13][1]);
|
|
579
|
+
const w14 = clamp(parameters[14] || 0, clip[14][0], clip[14][1]);
|
|
580
|
+
const value = -(Math.log(w11) + Math.log(Math.pow(2, w13) - 1) + w14 * 0.3) / numRelearningSteps;
|
|
581
|
+
const w17_w18_ceiling = clamp(roundTo(value, 8), 0.01, W17_W18_Ceiling);
|
|
582
|
+
if (clip[17]) clip[17] = [clip[17][0], w17_w18_ceiling];
|
|
583
|
+
if (clip[18]) clip[18] = [clip[18][0], w17_w18_ceiling];
|
|
584
|
+
}
|
|
564
585
|
return clip.map(
|
|
565
586
|
([min, max], index) => clamp(parameters[index] || 0, min, max)
|
|
566
587
|
);
|
|
567
588
|
};
|
|
568
589
|
const checkParameters = (parameters) => {
|
|
569
|
-
const invalid = parameters.find(
|
|
570
|
-
(param) => !Number.isFinite(param) && !Number.isNaN(param)
|
|
571
|
-
);
|
|
590
|
+
const invalid = parameters.find((param) => !Number.isFinite(param));
|
|
572
591
|
if (invalid !== void 0) {
|
|
573
|
-
throw
|
|
592
|
+
throw new FSRSValidationError(
|
|
593
|
+
`Non-finite or NaN value in parameters ${parameters}`
|
|
594
|
+
);
|
|
574
595
|
} else if (![17, 19, 21].includes(parameters.length)) {
|
|
575
|
-
throw
|
|
596
|
+
throw new FSRSValidationError(
|
|
576
597
|
`Invalid parameter length: ${parameters.length}. Must be 17, 19 or 21 for FSRSv4, 5 and 6 respectively.`
|
|
577
598
|
);
|
|
578
599
|
}
|
|
@@ -690,7 +711,9 @@ class FSRSAlgorithm {
|
|
|
690
711
|
*/
|
|
691
712
|
calculate_interval_modifier(request_retention) {
|
|
692
713
|
if (request_retention <= 0 || request_retention > 1) {
|
|
693
|
-
throw new
|
|
714
|
+
throw new FSRSValidationError(
|
|
715
|
+
"Requested retention rate should be in the range (0,1]"
|
|
716
|
+
);
|
|
694
717
|
}
|
|
695
718
|
const { decay, factor } = computeDecayFactor(this.param.w);
|
|
696
719
|
return roundTo((Math.pow(request_retention, 1 / decay) - 1) / factor, 8);
|
|
@@ -906,10 +929,10 @@ class FSRSAlgorithm {
|
|
|
906
929
|
stability: 0
|
|
907
930
|
};
|
|
908
931
|
if (t < 0) {
|
|
909
|
-
throw new
|
|
932
|
+
throw new FSRSValidationError(`Invalid delta_t "${t}"`);
|
|
910
933
|
}
|
|
911
934
|
if (g < 0 || g > 4) {
|
|
912
|
-
throw new
|
|
935
|
+
throw new FSRSValidationError(`Invalid grade "${g}"`);
|
|
913
936
|
}
|
|
914
937
|
if (d === 0 && s === 0) {
|
|
915
938
|
return {
|
|
@@ -924,7 +947,7 @@ class FSRSAlgorithm {
|
|
|
924
947
|
};
|
|
925
948
|
}
|
|
926
949
|
if (d < 1 || s < S_MIN) {
|
|
927
|
-
throw new
|
|
950
|
+
throw new FSRSValidationError(
|
|
928
951
|
`Invalid memory state { difficulty: ${d}, stability: ${s} }`
|
|
929
952
|
);
|
|
930
953
|
}
|
|
@@ -1305,7 +1328,9 @@ class Reschedule {
|
|
|
1305
1328
|
*/
|
|
1306
1329
|
handleManualRating(card, state, reviewed, elapsed_days, stability, difficulty, due) {
|
|
1307
1330
|
if (typeof state === "undefined") {
|
|
1308
|
-
throw new
|
|
1331
|
+
throw new FSRSValidationError(
|
|
1332
|
+
"reschedule: state is required for manual rating"
|
|
1333
|
+
);
|
|
1309
1334
|
}
|
|
1310
1335
|
let log;
|
|
1311
1336
|
let next_card;
|
|
@@ -1326,7 +1351,9 @@ class Reschedule {
|
|
|
1326
1351
|
next_card.last_review = reviewed;
|
|
1327
1352
|
} else {
|
|
1328
1353
|
if (typeof due === "undefined") {
|
|
1329
|
-
throw new
|
|
1354
|
+
throw new FSRSValidationError(
|
|
1355
|
+
"reschedule: due is required for manual rating"
|
|
1356
|
+
);
|
|
1330
1357
|
}
|
|
1331
1358
|
const scheduled_days = date_diff(due, reviewed, "days");
|
|
1332
1359
|
log = {
|
|
@@ -1416,6 +1443,9 @@ class Reschedule {
|
|
|
1416
1443
|
}
|
|
1417
1444
|
}
|
|
1418
1445
|
|
|
1446
|
+
function applyAfterHandler(value, afterHandler) {
|
|
1447
|
+
return typeof afterHandler === "function" ? afterHandler(value) : value;
|
|
1448
|
+
}
|
|
1419
1449
|
class FSRS extends FSRSAlgorithm {
|
|
1420
1450
|
strategyHandler = /* @__PURE__ */ new Map();
|
|
1421
1451
|
Scheduler;
|
|
@@ -1531,11 +1561,7 @@ class FSRS extends FSRSAlgorithm {
|
|
|
1531
1561
|
repeat(card, now, afterHandler) {
|
|
1532
1562
|
const instance = this.getScheduler(card, now);
|
|
1533
1563
|
const recordLog = instance.preview();
|
|
1534
|
-
|
|
1535
|
-
return afterHandler(recordLog);
|
|
1536
|
-
} else {
|
|
1537
|
-
return recordLog;
|
|
1538
|
-
}
|
|
1564
|
+
return applyAfterHandler(recordLog, afterHandler);
|
|
1539
1565
|
}
|
|
1540
1566
|
/**
|
|
1541
1567
|
* Display the collection of cards and logs for the card scheduled at the current time, after applying a specific grade rating.
|
|
@@ -1595,14 +1621,10 @@ class FSRS extends FSRSAlgorithm {
|
|
|
1595
1621
|
const instance = this.getScheduler(card, now);
|
|
1596
1622
|
const g = TypeConvert.rating(grade);
|
|
1597
1623
|
if (g === Rating.Manual) {
|
|
1598
|
-
throw new
|
|
1624
|
+
throw new FSRSValidationError("Cannot review a manual rating");
|
|
1599
1625
|
}
|
|
1600
1626
|
const recordLogItem = instance.review(g);
|
|
1601
|
-
|
|
1602
|
-
return afterHandler(recordLogItem);
|
|
1603
|
-
} else {
|
|
1604
|
-
return recordLogItem;
|
|
1605
|
-
}
|
|
1627
|
+
return applyAfterHandler(recordLogItem, afterHandler);
|
|
1606
1628
|
}
|
|
1607
1629
|
/**
|
|
1608
1630
|
* Get the retrievability of the card
|
|
@@ -1647,7 +1669,7 @@ class FSRS extends FSRSAlgorithm {
|
|
|
1647
1669
|
const processedCard = TypeConvert.card(card);
|
|
1648
1670
|
const processedLog = TypeConvert.review_log(log);
|
|
1649
1671
|
if (processedLog.rating === Rating.Manual) {
|
|
1650
|
-
throw new
|
|
1672
|
+
throw new FSRSValidationError("Cannot rollback a manual rating");
|
|
1651
1673
|
}
|
|
1652
1674
|
let last_due;
|
|
1653
1675
|
let last_review;
|
|
@@ -1679,11 +1701,7 @@ class FSRS extends FSRSAlgorithm {
|
|
|
1679
1701
|
state: processedLog.state,
|
|
1680
1702
|
last_review
|
|
1681
1703
|
};
|
|
1682
|
-
|
|
1683
|
-
return afterHandler(prevCard);
|
|
1684
|
-
} else {
|
|
1685
|
-
return prevCard;
|
|
1686
|
-
}
|
|
1704
|
+
return applyAfterHandler(prevCard, afterHandler);
|
|
1687
1705
|
}
|
|
1688
1706
|
/**
|
|
1689
1707
|
*
|
|
@@ -1766,11 +1784,7 @@ class FSRS extends FSRSAlgorithm {
|
|
|
1766
1784
|
last_review: processedCard.last_review
|
|
1767
1785
|
};
|
|
1768
1786
|
const recordLogItem = { card: forget_card, log: forget_log };
|
|
1769
|
-
|
|
1770
|
-
return afterHandler(recordLogItem);
|
|
1771
|
-
} else {
|
|
1772
|
-
return recordLogItem;
|
|
1773
|
-
}
|
|
1787
|
+
return applyAfterHandler(recordLogItem, afterHandler);
|
|
1774
1788
|
}
|
|
1775
1789
|
/**
|
|
1776
1790
|
* Reschedules the current card and returns the rescheduled collections and reschedule item.
|
|
@@ -1837,15 +1851,9 @@ class FSRS extends FSRSAlgorithm {
|
|
|
1837
1851
|
len ? collections[len - 1] : void 0,
|
|
1838
1852
|
updateMemoryState
|
|
1839
1853
|
);
|
|
1840
|
-
if (recordLogHandler && typeof recordLogHandler === "function") {
|
|
1841
|
-
return {
|
|
1842
|
-
collections: collections.map(recordLogHandler),
|
|
1843
|
-
reschedule_item: manual_item ? recordLogHandler(manual_item) : null
|
|
1844
|
-
};
|
|
1845
|
-
}
|
|
1846
1854
|
return {
|
|
1847
|
-
collections,
|
|
1848
|
-
reschedule_item: manual_item
|
|
1855
|
+
collections: typeof recordLogHandler === "function" ? collections.map(recordLogHandler) : collections,
|
|
1856
|
+
reschedule_item: manual_item ? applyAfterHandler(manual_item, recordLogHandler) : null
|
|
1849
1857
|
};
|
|
1850
1858
|
}
|
|
1851
1859
|
}
|