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/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 Error(`Invalid rating:[${value}]`);
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 Error(`Invalid rating:[${value}]`);
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 Error(`Invalid state:[${value}]`);
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 Error(`Invalid state:[${value}]`);
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 Error(`Invalid date:[${value}]`);
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 Error(`Invalid date:[${value}]`);
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 Error("Invalid date");
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 Error(`Invalid step value: ${step}`);
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 Error(`Invalid step unit: ${step}, expected m/h/d`);
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 < 0 || grade > 4) {
341
- throw new Error(`Invalid grade "${grade}",expected 1-4`);
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.3.2";
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
- let w17_w18_ceiling = W17_W18_Ceiling;
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 Error(`Non-finite or NaN value in parameters ${parameters}`);
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 Error(
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 Error("Requested retention rate should be in the range (0,1]");
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 Error(`Invalid delta_t "${t}"`);
932
+ throw new FSRSValidationError(`Invalid delta_t "${t}"`);
910
933
  }
911
934
  if (g < 0 || g > 4) {
912
- throw new Error(`Invalid grade "${g}"`);
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 Error(
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 Error("reschedule: state is required for manual rating");
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 Error("reschedule: due is required for manual rating");
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
- if (afterHandler && typeof afterHandler === "function") {
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 Error("Cannot review a manual rating");
1624
+ throw new FSRSValidationError("Cannot review a manual rating");
1599
1625
  }
1600
1626
  const recordLogItem = instance.review(g);
1601
- if (afterHandler && typeof afterHandler === "function") {
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 Error("Cannot rollback a manual rating");
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
- if (afterHandler && typeof afterHandler === "function") {
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
- if (afterHandler && typeof afterHandler === "function") {
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
  }