ts-fsrs 5.3.3 → 5.4.1
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 +62 -54
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +62 -54
- package/dist/index.mjs.map +1 -1
- package/dist/index.umd.js +64 -54
- 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.1";
|
|
485
502
|
|
|
486
503
|
const default_request_retention = 0.9;
|
|
487
504
|
const default_maximum_interval = 36500;
|
|
@@ -561,7 +578,11 @@ const clipParameters = (parameters, numRelearningSteps, enableShortTerm = defaul
|
|
|
561
578
|
const w13 = clamp(parameters[13] || 0, clip[13][0], clip[13][1]);
|
|
562
579
|
const w14 = clamp(parameters[14] || 0, clip[14][0], clip[14][1]);
|
|
563
580
|
const value = -(Math.log(w11) + Math.log(Math.pow(2, w13) - 1) + w14 * 0.3) / numRelearningSteps;
|
|
564
|
-
const w17_w18_ceiling = clamp(
|
|
581
|
+
const w17_w18_ceiling = clamp(
|
|
582
|
+
roundTo(Math.sqrt(Math.max(value, 0)), 8),
|
|
583
|
+
0.01,
|
|
584
|
+
W17_W18_Ceiling
|
|
585
|
+
);
|
|
565
586
|
if (clip[17]) clip[17] = [clip[17][0], w17_w18_ceiling];
|
|
566
587
|
if (clip[18]) clip[18] = [clip[18][0], w17_w18_ceiling];
|
|
567
588
|
}
|
|
@@ -570,13 +591,13 @@ const clipParameters = (parameters, numRelearningSteps, enableShortTerm = defaul
|
|
|
570
591
|
);
|
|
571
592
|
};
|
|
572
593
|
const checkParameters = (parameters) => {
|
|
573
|
-
const invalid = parameters.find(
|
|
574
|
-
(param) => !Number.isFinite(param) && !Number.isNaN(param)
|
|
575
|
-
);
|
|
594
|
+
const invalid = parameters.find((param) => !Number.isFinite(param));
|
|
576
595
|
if (invalid !== void 0) {
|
|
577
|
-
throw
|
|
596
|
+
throw new FSRSValidationError(
|
|
597
|
+
`Non-finite or NaN value in parameters ${parameters}`
|
|
598
|
+
);
|
|
578
599
|
} else if (![17, 19, 21].includes(parameters.length)) {
|
|
579
|
-
throw
|
|
600
|
+
throw new FSRSValidationError(
|
|
580
601
|
`Invalid parameter length: ${parameters.length}. Must be 17, 19 or 21 for FSRSv4, 5 and 6 respectively.`
|
|
581
602
|
);
|
|
582
603
|
}
|
|
@@ -694,7 +715,9 @@ class FSRSAlgorithm {
|
|
|
694
715
|
*/
|
|
695
716
|
calculate_interval_modifier(request_retention) {
|
|
696
717
|
if (request_retention <= 0 || request_retention > 1) {
|
|
697
|
-
throw new
|
|
718
|
+
throw new FSRSValidationError(
|
|
719
|
+
"Requested retention rate should be in the range (0,1]"
|
|
720
|
+
);
|
|
698
721
|
}
|
|
699
722
|
const { decay, factor } = computeDecayFactor(this.param.w);
|
|
700
723
|
return roundTo((Math.pow(request_retention, 1 / decay) - 1) / factor, 8);
|
|
@@ -910,10 +933,10 @@ class FSRSAlgorithm {
|
|
|
910
933
|
stability: 0
|
|
911
934
|
};
|
|
912
935
|
if (t < 0) {
|
|
913
|
-
throw new
|
|
936
|
+
throw new FSRSValidationError(`Invalid delta_t "${t}"`);
|
|
914
937
|
}
|
|
915
938
|
if (g < 0 || g > 4) {
|
|
916
|
-
throw new
|
|
939
|
+
throw new FSRSValidationError(`Invalid grade "${g}"`);
|
|
917
940
|
}
|
|
918
941
|
if (d === 0 && s === 0) {
|
|
919
942
|
return {
|
|
@@ -928,7 +951,7 @@ class FSRSAlgorithm {
|
|
|
928
951
|
};
|
|
929
952
|
}
|
|
930
953
|
if (d < 1 || s < S_MIN) {
|
|
931
|
-
throw new
|
|
954
|
+
throw new FSRSValidationError(
|
|
932
955
|
`Invalid memory state { difficulty: ${d}, stability: ${s} }`
|
|
933
956
|
);
|
|
934
957
|
}
|
|
@@ -1309,7 +1332,9 @@ class Reschedule {
|
|
|
1309
1332
|
*/
|
|
1310
1333
|
handleManualRating(card, state, reviewed, elapsed_days, stability, difficulty, due) {
|
|
1311
1334
|
if (typeof state === "undefined") {
|
|
1312
|
-
throw new
|
|
1335
|
+
throw new FSRSValidationError(
|
|
1336
|
+
"reschedule: state is required for manual rating"
|
|
1337
|
+
);
|
|
1313
1338
|
}
|
|
1314
1339
|
let log;
|
|
1315
1340
|
let next_card;
|
|
@@ -1330,7 +1355,9 @@ class Reschedule {
|
|
|
1330
1355
|
next_card.last_review = reviewed;
|
|
1331
1356
|
} else {
|
|
1332
1357
|
if (typeof due === "undefined") {
|
|
1333
|
-
throw new
|
|
1358
|
+
throw new FSRSValidationError(
|
|
1359
|
+
"reschedule: due is required for manual rating"
|
|
1360
|
+
);
|
|
1334
1361
|
}
|
|
1335
1362
|
const scheduled_days = date_diff(due, reviewed, "days");
|
|
1336
1363
|
log = {
|
|
@@ -1420,6 +1447,9 @@ class Reschedule {
|
|
|
1420
1447
|
}
|
|
1421
1448
|
}
|
|
1422
1449
|
|
|
1450
|
+
function applyAfterHandler(value, afterHandler) {
|
|
1451
|
+
return typeof afterHandler === "function" ? afterHandler(value) : value;
|
|
1452
|
+
}
|
|
1423
1453
|
class FSRS extends FSRSAlgorithm {
|
|
1424
1454
|
strategyHandler = /* @__PURE__ */ new Map();
|
|
1425
1455
|
Scheduler;
|
|
@@ -1535,11 +1565,7 @@ class FSRS extends FSRSAlgorithm {
|
|
|
1535
1565
|
repeat(card, now, afterHandler) {
|
|
1536
1566
|
const instance = this.getScheduler(card, now);
|
|
1537
1567
|
const recordLog = instance.preview();
|
|
1538
|
-
|
|
1539
|
-
return afterHandler(recordLog);
|
|
1540
|
-
} else {
|
|
1541
|
-
return recordLog;
|
|
1542
|
-
}
|
|
1568
|
+
return applyAfterHandler(recordLog, afterHandler);
|
|
1543
1569
|
}
|
|
1544
1570
|
/**
|
|
1545
1571
|
* Display the collection of cards and logs for the card scheduled at the current time, after applying a specific grade rating.
|
|
@@ -1599,14 +1625,10 @@ class FSRS extends FSRSAlgorithm {
|
|
|
1599
1625
|
const instance = this.getScheduler(card, now);
|
|
1600
1626
|
const g = TypeConvert.rating(grade);
|
|
1601
1627
|
if (g === Rating.Manual) {
|
|
1602
|
-
throw new
|
|
1628
|
+
throw new FSRSValidationError("Cannot review a manual rating");
|
|
1603
1629
|
}
|
|
1604
1630
|
const recordLogItem = instance.review(g);
|
|
1605
|
-
|
|
1606
|
-
return afterHandler(recordLogItem);
|
|
1607
|
-
} else {
|
|
1608
|
-
return recordLogItem;
|
|
1609
|
-
}
|
|
1631
|
+
return applyAfterHandler(recordLogItem, afterHandler);
|
|
1610
1632
|
}
|
|
1611
1633
|
/**
|
|
1612
1634
|
* Get the retrievability of the card
|
|
@@ -1651,7 +1673,7 @@ class FSRS extends FSRSAlgorithm {
|
|
|
1651
1673
|
const processedCard = TypeConvert.card(card);
|
|
1652
1674
|
const processedLog = TypeConvert.review_log(log);
|
|
1653
1675
|
if (processedLog.rating === Rating.Manual) {
|
|
1654
|
-
throw new
|
|
1676
|
+
throw new FSRSValidationError("Cannot rollback a manual rating");
|
|
1655
1677
|
}
|
|
1656
1678
|
let last_due;
|
|
1657
1679
|
let last_review;
|
|
@@ -1683,11 +1705,7 @@ class FSRS extends FSRSAlgorithm {
|
|
|
1683
1705
|
state: processedLog.state,
|
|
1684
1706
|
last_review
|
|
1685
1707
|
};
|
|
1686
|
-
|
|
1687
|
-
return afterHandler(prevCard);
|
|
1688
|
-
} else {
|
|
1689
|
-
return prevCard;
|
|
1690
|
-
}
|
|
1708
|
+
return applyAfterHandler(prevCard, afterHandler);
|
|
1691
1709
|
}
|
|
1692
1710
|
/**
|
|
1693
1711
|
*
|
|
@@ -1770,11 +1788,7 @@ class FSRS extends FSRSAlgorithm {
|
|
|
1770
1788
|
last_review: processedCard.last_review
|
|
1771
1789
|
};
|
|
1772
1790
|
const recordLogItem = { card: forget_card, log: forget_log };
|
|
1773
|
-
|
|
1774
|
-
return afterHandler(recordLogItem);
|
|
1775
|
-
} else {
|
|
1776
|
-
return recordLogItem;
|
|
1777
|
-
}
|
|
1791
|
+
return applyAfterHandler(recordLogItem, afterHandler);
|
|
1778
1792
|
}
|
|
1779
1793
|
/**
|
|
1780
1794
|
* Reschedules the current card and returns the rescheduled collections and reschedule item.
|
|
@@ -1841,15 +1855,9 @@ class FSRS extends FSRSAlgorithm {
|
|
|
1841
1855
|
len ? collections[len - 1] : void 0,
|
|
1842
1856
|
updateMemoryState
|
|
1843
1857
|
);
|
|
1844
|
-
if (recordLogHandler && typeof recordLogHandler === "function") {
|
|
1845
|
-
return {
|
|
1846
|
-
collections: collections.map(recordLogHandler),
|
|
1847
|
-
reschedule_item: manual_item ? recordLogHandler(manual_item) : null
|
|
1848
|
-
};
|
|
1849
|
-
}
|
|
1850
1858
|
return {
|
|
1851
|
-
collections,
|
|
1852
|
-
reschedule_item: manual_item
|
|
1859
|
+
collections: typeof recordLogHandler === "function" ? collections.map(recordLogHandler) : collections,
|
|
1860
|
+
reschedule_item: manual_item ? applyAfterHandler(manual_item, recordLogHandler) : null
|
|
1853
1861
|
};
|
|
1854
1862
|
}
|
|
1855
1863
|
}
|