ts-fsrs 5.2.2 → 5.3.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 ADDED
@@ -0,0 +1,11 @@
1
+ # ts-fsrs
2
+
3
+ ## 5.3.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#266](https://github.com/open-spaced-repetition/ts-fsrs/pull/266) [`17fb34d`](https://github.com/open-spaced-repetition/ts-fsrs/commit/17fb34d849c66f2035c5a239e2dfd64ed40055c9) Thanks [@ishiko732](https://github.com/ishiko732)! - Non-decreasing SInc(Hard) in same-day reviews — sync with fsrs-rs#376 changes
8
+
9
+ ### Patch Changes
10
+
11
+ - [#314](https://github.com/open-spaced-repetition/ts-fsrs/pull/314) [`87f94f2`](https://github.com/open-spaced-repetition/ts-fsrs/commit/87f94f277f7030a449490cef943e1aebe1585b64) Thanks [@user1823](https://github.com/user1823)! - Performance improvements
package/dist/index.cjs CHANGED
@@ -54,8 +54,12 @@ class TypeConvert {
54
54
  throw new Error(`Invalid state:[${value}]`);
55
55
  }
56
56
  static time(value) {
57
- if (typeof value === "object" && value instanceof Date) {
57
+ if (value instanceof Date) {
58
58
  return value;
59
+ }
60
+ const date = new Date(value);
61
+ if (typeof value === "object" && value !== null && !Number.isNaN(Date.parse(value) || +date)) {
62
+ return date;
59
63
  } else if (typeof value === "string") {
60
64
  const timestamp = Date.parse(value);
61
65
  if (!Number.isNaN(timestamp)) {
@@ -79,15 +83,19 @@ class TypeConvert {
79
83
  }
80
84
  }
81
85
 
86
+ /* istanbul ignore next -- @preserve */
82
87
  Date.prototype.scheduler = function(t, isDay) {
83
88
  return date_scheduler(this, t, isDay);
84
89
  };
90
+ /* istanbul ignore next -- @preserve */
85
91
  Date.prototype.diff = function(pre, unit) {
86
92
  return date_diff(this, pre, unit);
87
93
  };
94
+ /* istanbul ignore next -- @preserve */
88
95
  Date.prototype.format = function() {
89
96
  return formatDate(this);
90
97
  };
98
+ /* istanbul ignore next -- @preserve */
91
99
  Date.prototype.dueFormat = function(last_review, unit, timeUnit) {
92
100
  return show_diff_message(this, last_review, unit, timeUnit);
93
101
  };
@@ -147,12 +155,15 @@ function show_diff_message(due, last_review, unit, timeUnit = TIMEUNITFORMAT) {
147
155
  }
148
156
  return `${Math.floor(diff)}${unit ? timeUnit[i] : ""}`;
149
157
  }
158
+ /* istanbul ignore next -- @preserve */
150
159
  function fixDate(value) {
151
160
  return TypeConvert.time(value);
152
161
  }
162
+ /* istanbul ignore next -- @preserve */
153
163
  function fixState(value) {
154
164
  return TypeConvert.state(value);
155
165
  }
166
+ /* istanbul ignore next -- @preserve */
156
167
  function fixRating(value) {
157
168
  return TypeConvert.rating(value);
158
169
  }
@@ -196,6 +207,10 @@ function get_fuzz_range(interval, elapsed_days, maximum_interval) {
196
207
  function clamp(value, min, max) {
197
208
  return Math.min(Math.max(value, min), max);
198
209
  }
210
+ function roundTo(num, decimals) {
211
+ const factor = 10 ** decimals;
212
+ return Math.round(num * factor) / factor;
213
+ }
199
214
  function dateDiffInDays(last, cur) {
200
215
  const utc1 = Date.UTC(
201
216
  last.getUTCFullYear(),
@@ -468,7 +483,7 @@ function alea(seed) {
468
483
  return prng;
469
484
  }
470
485
 
471
- const version="5.2.2";
486
+ const version="5.3.0";
472
487
 
473
488
  const default_request_retention = 0.9;
474
489
  const default_maximum_interval = 36500;
@@ -642,11 +657,11 @@ function createEmptyCard(now, afterHandler) {
642
657
  const computeDecayFactor = (decayOrParams) => {
643
658
  const decay = typeof decayOrParams === "number" ? -decayOrParams : -decayOrParams[20];
644
659
  const factor = Math.exp(Math.pow(decay, -1) * Math.log(0.9)) - 1;
645
- return { decay, factor: +factor.toFixed(8) };
660
+ return { decay, factor: roundTo(factor, 8) };
646
661
  };
647
662
  function forgetting_curve(decayOrParams, elapsed_days, stability) {
648
663
  const { decay, factor } = computeDecayFactor(decayOrParams);
649
- return +Math.pow(1 + factor * elapsed_days / stability, decay).toFixed(8);
664
+ return roundTo(Math.pow(1 + factor * elapsed_days / stability, decay), 8);
650
665
  }
651
666
  class FSRSAlgorithm {
652
667
  param;
@@ -680,7 +695,7 @@ class FSRSAlgorithm {
680
695
  throw new Error("Requested retention rate should be in the range (0,1]");
681
696
  }
682
697
  const { decay, factor } = computeDecayFactor(this.param.w);
683
- return +((Math.pow(request_retention, 1 / decay) - 1) / factor).toFixed(8);
698
+ return roundTo((Math.pow(request_retention, 1 / decay) - 1) / factor, 8);
684
699
  }
685
700
  /**
686
701
  * Get the parameters of the algorithm.
@@ -747,8 +762,9 @@ class FSRSAlgorithm {
747
762
  * @return {number} Difficulty $$D \in [1,10]$$
748
763
  */
749
764
  init_difficulty(g) {
750
- const d = this.param.w[4] - Math.exp((g - 1) * this.param.w[5]) + 1;
751
- return +d.toFixed(8);
765
+ const w = this.param.w;
766
+ const d = w[4] - Math.exp((g - 1) * w[5]) + 1;
767
+ return roundTo(d, 8);
752
768
  }
753
769
  /**
754
770
  * If fuzzing is disabled or ivl is less than 2.5, it returns the original interval.
@@ -783,7 +799,7 @@ class FSRSAlgorithm {
783
799
  * @see https://github.com/open-spaced-repetition/fsrs4anki/issues/697
784
800
  */
785
801
  linear_damping(delta_d, old_d) {
786
- return +(delta_d * (10 - old_d) / 9).toFixed(8);
802
+ return roundTo(delta_d * (10 - old_d) / 9, 8);
787
803
  }
788
804
  /**
789
805
  * The formula used is :
@@ -811,9 +827,8 @@ class FSRSAlgorithm {
811
827
  * @return {number} difficulty
812
828
  */
813
829
  mean_reversion(init, current) {
814
- return +(this.param.w[7] * init + (1 - this.param.w[7]) * current).toFixed(
815
- 8
816
- );
830
+ const w = this.param.w;
831
+ return roundTo(w[7] * init + (1 - w[7]) * current, 8);
817
832
  }
818
833
  /**
819
834
  * The formula used is :
@@ -825,13 +840,17 @@ class FSRSAlgorithm {
825
840
  * @return {number} S^\prime_r new stability after recall
826
841
  */
827
842
  next_recall_stability(d, s, r, g) {
828
- const hard_penalty = Rating.Hard === g ? this.param.w[15] : 1;
829
- const easy_bound = Rating.Easy === g ? this.param.w[16] : 1;
830
- return +clamp(
831
- s * (1 + Math.exp(this.param.w[8]) * (11 - d) * Math.pow(s, -this.param.w[9]) * (Math.exp((1 - r) * this.param.w[10]) - 1) * hard_penalty * easy_bound),
832
- S_MIN,
833
- 36500
834
- ).toFixed(8);
843
+ const w = this.param.w;
844
+ const hard_penalty = Rating.Hard === g ? w[15] : 1;
845
+ const easy_bound = Rating.Easy === g ? w[16] : 1;
846
+ return roundTo(
847
+ clamp(
848
+ s * (1 + Math.exp(w[8]) * (11 - d) * Math.pow(s, -w[9]) * (Math.exp((1 - r) * w[10]) - 1) * hard_penalty * easy_bound),
849
+ S_MIN,
850
+ 36500
851
+ ),
852
+ 8
853
+ );
835
854
  }
836
855
  /**
837
856
  * The formula used is :
@@ -844,11 +863,15 @@ class FSRSAlgorithm {
844
863
  * @return {number} S^\prime_f new stability after forgetting
845
864
  */
846
865
  next_forget_stability(d, s, r) {
847
- return +clamp(
848
- this.param.w[11] * Math.pow(d, -this.param.w[12]) * (Math.pow(s + 1, this.param.w[13]) - 1) * Math.exp((1 - r) * this.param.w[14]),
849
- S_MIN,
850
- 36500
851
- ).toFixed(8);
866
+ const w = this.param.w;
867
+ return roundTo(
868
+ clamp(
869
+ w[11] * Math.pow(d, -w[12]) * (Math.pow(s + 1, w[13]) - 1) * Math.exp((1 - r) * w[14]),
870
+ S_MIN,
871
+ 36500
872
+ ),
873
+ 8
874
+ );
852
875
  }
853
876
  /**
854
877
  * The formula used is :
@@ -857,9 +880,10 @@ class FSRSAlgorithm {
857
880
  * @param {Grade} g Grade (Rating[0.again,1.hard,2.good,3.easy])
858
881
  */
859
882
  next_short_term_stability(s, g) {
860
- const sinc = Math.pow(s, -this.param.w[19]) * Math.exp(this.param.w[17] * (g - 3 + this.param.w[18]));
861
- const maskedSinc = g >= 3 ? Math.max(sinc, 1) : sinc;
862
- return +clamp(s * maskedSinc, S_MIN, 36500).toFixed(8);
883
+ const w = this.param.w;
884
+ const sinc = Math.pow(s, -w[19]) * Math.exp(w[17] * (g - 3 + w[18]));
885
+ const maskedSinc = g >= Rating.Hard ? Math.max(sinc, 1) : sinc;
886
+ return roundTo(clamp(s * maskedSinc, S_MIN, 36500), 8);
863
887
  }
864
888
  /**
865
889
  * The formula used is :
@@ -875,9 +899,10 @@ class FSRSAlgorithm {
875
899
  * @param memory_state - The current state of memory, which can be null.
876
900
  * @param t - The time elapsed since the last review.
877
901
  * @param {Rating} g Grade (Rating[0.Manual,1.Again,2.Hard,3.Good,4.Easy])
902
+ * @param r - Optional retrievability value. If not provided, it will be calculated.
878
903
  * @returns The next state of memory with updated difficulty and stability.
879
904
  */
880
- next_state(memory_state, t, g) {
905
+ next_state(memory_state, t, g, r) {
881
906
  const { difficulty: d, stability: s } = memory_state ?? {
882
907
  difficulty: 0,
883
908
  stability: 0
@@ -905,22 +930,22 @@ class FSRSAlgorithm {
905
930
  `Invalid memory state { difficulty: ${d}, stability: ${s} }`
906
931
  );
907
932
  }
908
- const r = this.forgetting_curve(t, s);
909
- const s_after_success = this.next_recall_stability(d, s, r, g);
910
- const s_after_fail = this.next_forget_stability(d, s, r);
911
- const s_after_short_term = this.next_short_term_stability(s, g);
912
- let new_s = s_after_success;
913
- if (g === 1) {
933
+ const w = this.param.w;
934
+ r = typeof r === "number" ? r : this.forgetting_curve(t, s);
935
+ let new_s;
936
+ if (t === 0 && this.param.enable_short_term) {
937
+ new_s = this.next_short_term_stability(s, g);
938
+ } else if (g === 1) {
939
+ const s_after_fail = this.next_forget_stability(d, s, r);
914
940
  let [w_17, w_18] = [0, 0];
915
941
  if (this.param.enable_short_term) {
916
- w_17 = this.param.w[17];
917
- w_18 = this.param.w[18];
942
+ w_17 = w[17];
943
+ w_18 = w[18];
918
944
  }
919
945
  const next_s_min = s / Math.exp(w_17 * w_18);
920
- new_s = clamp(+next_s_min.toFixed(8), S_MIN, s_after_fail);
921
- }
922
- if (t === 0 && this.param.enable_short_term) {
923
- new_s = s_after_short_term;
946
+ new_s = clamp(roundTo(next_s_min, 8), S_MIN, s_after_fail);
947
+ } else {
948
+ new_s = this.next_recall_stability(d, s, r, g);
924
949
  }
925
950
  const new_d = this.next_difficulty(d, g);
926
951
  return { difficulty: new_d, stability: new_s };
@@ -1005,9 +1030,7 @@ class BasicScheduler extends AbstractScheduler {
1005
1030
  if (exist) {
1006
1031
  return exist;
1007
1032
  }
1008
- const next = TypeConvert.card(this.current);
1009
- next.difficulty = clamp(this.algorithm.init_difficulty(grade), 1, 10);
1010
- next.stability = this.algorithm.init_stability(grade);
1033
+ const next = this.next_ds(this.elapsed_days, grade);
1011
1034
  this.applyLearningSteps(next, grade, State.Learning);
1012
1035
  const item = {
1013
1036
  card: next,
@@ -1021,14 +1044,11 @@ class BasicScheduler extends AbstractScheduler {
1021
1044
  if (exist) {
1022
1045
  return exist;
1023
1046
  }
1024
- const { state, difficulty, stability } = this.last;
1025
- const next = TypeConvert.card(this.current);
1026
- next.difficulty = this.algorithm.next_difficulty(difficulty, grade);
1027
- next.stability = this.algorithm.next_short_term_stability(stability, grade);
1047
+ const next = this.next_ds(this.elapsed_days, grade);
1028
1048
  this.applyLearningSteps(
1029
1049
  next,
1030
1050
  grade,
1031
- state
1051
+ this.last.state
1032
1052
  /** Learning or Relearning */
1033
1053
  );
1034
1054
  const item = {
@@ -1044,21 +1064,14 @@ class BasicScheduler extends AbstractScheduler {
1044
1064
  return exist;
1045
1065
  }
1046
1066
  const interval = this.elapsed_days;
1047
- const { difficulty, stability } = this.last;
1048
- const retrievability = this.algorithm.forgetting_curve(interval, stability);
1049
- const next_again = TypeConvert.card(this.current);
1050
- const next_hard = TypeConvert.card(this.current);
1051
- const next_good = TypeConvert.card(this.current);
1052
- const next_easy = TypeConvert.card(this.current);
1053
- this.next_ds(
1054
- next_again,
1055
- next_hard,
1056
- next_good,
1057
- next_easy,
1058
- difficulty,
1059
- stability,
1060
- retrievability
1067
+ const retrievability = this.algorithm.forgetting_curve(
1068
+ interval,
1069
+ this.current.stability
1061
1070
  );
1071
+ const next_again = this.next_ds(interval, Rating.Again, retrievability);
1072
+ const next_hard = this.next_ds(interval, Rating.Hard, retrievability);
1073
+ const next_good = this.next_ds(interval, Rating.Good, retrievability);
1074
+ const next_easy = this.next_ds(interval, Rating.Easy, retrievability);
1062
1075
  this.next_interval(next_hard, next_good, next_easy, interval);
1063
1076
  this.next_state(next_hard, next_good, next_easy);
1064
1077
  this.applyLearningSteps(next_again, Rating.Again, State.Relearning);
@@ -1088,50 +1101,20 @@ class BasicScheduler extends AbstractScheduler {
1088
1101
  /**
1089
1102
  * Review next_ds
1090
1103
  */
1091
- next_ds(next_again, next_hard, next_good, next_easy, difficulty, stability, retrievability) {
1092
- next_again.difficulty = this.algorithm.next_difficulty(
1093
- difficulty,
1094
- Rating.Again
1095
- );
1096
- const nextSMin = stability / Math.exp(
1097
- this.algorithm.parameters.w[17] * this.algorithm.parameters.w[18]
1098
- );
1099
- const s_after_fail = this.algorithm.next_forget_stability(
1100
- difficulty,
1101
- stability,
1102
- retrievability
1103
- );
1104
- next_again.stability = clamp(+nextSMin.toFixed(8), S_MIN, s_after_fail);
1105
- next_hard.difficulty = this.algorithm.next_difficulty(
1106
- difficulty,
1107
- Rating.Hard
1108
- );
1109
- next_hard.stability = this.algorithm.next_recall_stability(
1110
- difficulty,
1111
- stability,
1112
- retrievability,
1113
- Rating.Hard
1114
- );
1115
- next_good.difficulty = this.algorithm.next_difficulty(
1116
- difficulty,
1117
- Rating.Good
1118
- );
1119
- next_good.stability = this.algorithm.next_recall_stability(
1120
- difficulty,
1121
- stability,
1122
- retrievability,
1123
- Rating.Good
1124
- );
1125
- next_easy.difficulty = this.algorithm.next_difficulty(
1126
- difficulty,
1127
- Rating.Easy
1128
- );
1129
- next_easy.stability = this.algorithm.next_recall_stability(
1130
- difficulty,
1131
- stability,
1132
- retrievability,
1133
- Rating.Easy
1104
+ next_ds(t, g, r) {
1105
+ const next_state = this.algorithm.next_state(
1106
+ {
1107
+ difficulty: this.current.difficulty,
1108
+ stability: this.current.stability
1109
+ },
1110
+ t,
1111
+ g,
1112
+ r
1134
1113
  );
1114
+ const card = TypeConvert.card(this.current);
1115
+ card.difficulty = next_state.difficulty;
1116
+ card.stability = next_state.stability;
1117
+ return card;
1135
1118
  }
1136
1119
  /**
1137
1120
  * Review next_interval
@@ -1174,12 +1157,11 @@ class LongTermScheduler extends AbstractScheduler {
1174
1157
  }
1175
1158
  this.current.scheduled_days = 0;
1176
1159
  this.current.elapsed_days = 0;
1177
- const next_again = TypeConvert.card(this.current);
1178
- const next_hard = TypeConvert.card(this.current);
1179
- const next_good = TypeConvert.card(this.current);
1180
- const next_easy = TypeConvert.card(this.current);
1181
- this.init_ds(next_again, next_hard, next_good, next_easy);
1182
1160
  const first_interval = 0;
1161
+ const next_again = this.next_ds(first_interval, Rating.Again);
1162
+ const next_hard = this.next_ds(first_interval, Rating.Hard);
1163
+ const next_good = this.next_ds(first_interval, Rating.Good);
1164
+ const next_easy = this.next_ds(first_interval, Rating.Easy);
1183
1165
  this.next_interval(
1184
1166
  next_again,
1185
1167
  next_hard,
@@ -1191,31 +1173,20 @@ class LongTermScheduler extends AbstractScheduler {
1191
1173
  this.update_next(next_again, next_hard, next_good, next_easy);
1192
1174
  return this.next.get(grade);
1193
1175
  }
1194
- init_ds(next_again, next_hard, next_good, next_easy) {
1195
- next_again.difficulty = clamp(
1196
- this.algorithm.init_difficulty(Rating.Again),
1197
- 1,
1198
- 10
1199
- );
1200
- next_again.stability = this.algorithm.init_stability(Rating.Again);
1201
- next_hard.difficulty = clamp(
1202
- this.algorithm.init_difficulty(Rating.Hard),
1203
- 1,
1204
- 10
1205
- );
1206
- next_hard.stability = this.algorithm.init_stability(Rating.Hard);
1207
- next_good.difficulty = clamp(
1208
- this.algorithm.init_difficulty(Rating.Good),
1209
- 1,
1210
- 10
1211
- );
1212
- next_good.stability = this.algorithm.init_stability(Rating.Good);
1213
- next_easy.difficulty = clamp(
1214
- this.algorithm.init_difficulty(Rating.Easy),
1215
- 1,
1216
- 10
1176
+ next_ds(t, g, r) {
1177
+ const next_state = this.algorithm.next_state(
1178
+ {
1179
+ difficulty: this.current.difficulty,
1180
+ stability: this.current.stability
1181
+ },
1182
+ t,
1183
+ g,
1184
+ r
1217
1185
  );
1218
- next_easy.stability = this.algorithm.init_stability(Rating.Easy);
1186
+ const card = TypeConvert.card(this.current);
1187
+ card.difficulty = next_state.difficulty;
1188
+ card.stability = next_state.stability;
1189
+ return card;
1219
1190
  }
1220
1191
  /**
1221
1192
  * @see https://github.com/open-spaced-repetition/ts-fsrs/issues/98#issuecomment-2241923194
@@ -1229,72 +1200,20 @@ class LongTermScheduler extends AbstractScheduler {
1229
1200
  return exist;
1230
1201
  }
1231
1202
  const interval = this.elapsed_days;
1232
- const { difficulty, stability } = this.last;
1233
- const retrievability = this.algorithm.forgetting_curve(interval, stability);
1234
- const next_again = TypeConvert.card(this.current);
1235
- const next_hard = TypeConvert.card(this.current);
1236
- const next_good = TypeConvert.card(this.current);
1237
- const next_easy = TypeConvert.card(this.current);
1238
- this.next_ds(
1239
- next_again,
1240
- next_hard,
1241
- next_good,
1242
- next_easy,
1243
- difficulty,
1244
- stability,
1245
- retrievability
1203
+ const retrievability = this.algorithm.forgetting_curve(
1204
+ interval,
1205
+ this.current.stability
1246
1206
  );
1207
+ const next_again = this.next_ds(interval, Rating.Again, retrievability);
1208
+ const next_hard = this.next_ds(interval, Rating.Hard, retrievability);
1209
+ const next_good = this.next_ds(interval, Rating.Good, retrievability);
1210
+ const next_easy = this.next_ds(interval, Rating.Easy, retrievability);
1247
1211
  this.next_interval(next_again, next_hard, next_good, next_easy, interval);
1248
1212
  this.next_state(next_again, next_hard, next_good, next_easy);
1249
1213
  next_again.lapses += 1;
1250
1214
  this.update_next(next_again, next_hard, next_good, next_easy);
1251
1215
  return this.next.get(grade);
1252
1216
  }
1253
- /**
1254
- * Review next_ds
1255
- */
1256
- next_ds(next_again, next_hard, next_good, next_easy, difficulty, stability, retrievability) {
1257
- next_again.difficulty = this.algorithm.next_difficulty(
1258
- difficulty,
1259
- Rating.Again
1260
- );
1261
- const s_after_fail = this.algorithm.next_forget_stability(
1262
- difficulty,
1263
- stability,
1264
- retrievability
1265
- );
1266
- next_again.stability = clamp(stability, S_MIN, s_after_fail);
1267
- next_hard.difficulty = this.algorithm.next_difficulty(
1268
- difficulty,
1269
- Rating.Hard
1270
- );
1271
- next_hard.stability = this.algorithm.next_recall_stability(
1272
- difficulty,
1273
- stability,
1274
- retrievability,
1275
- Rating.Hard
1276
- );
1277
- next_good.difficulty = this.algorithm.next_difficulty(
1278
- difficulty,
1279
- Rating.Good
1280
- );
1281
- next_good.stability = this.algorithm.next_recall_stability(
1282
- difficulty,
1283
- stability,
1284
- retrievability,
1285
- Rating.Good
1286
- );
1287
- next_easy.difficulty = this.algorithm.next_difficulty(
1288
- difficulty,
1289
- Rating.Easy
1290
- );
1291
- next_easy.stability = this.algorithm.next_recall_stability(
1292
- difficulty,
1293
- stability,
1294
- retrievability,
1295
- Rating.Easy
1296
- );
1297
- }
1298
1217
  /**
1299
1218
  * Review/New next_interval
1300
1219
  */
@@ -1982,6 +1901,7 @@ exports.fsrs = fsrs;
1982
1901
  exports.generatorParameters = generatorParameters;
1983
1902
  exports.get_fuzz_range = get_fuzz_range;
1984
1903
  exports.migrateParameters = migrateParameters;
1904
+ exports.roundTo = roundTo;
1985
1905
  exports.show_diff_message = show_diff_message;
1986
1906
  module.exports = Object.assign(exports.default || {}, exports)
1987
1907
  //# sourceMappingURL=index.cjs.map