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