ts-fsrs 2.1.1 → 3.0.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/README.md CHANGED
@@ -9,6 +9,15 @@ developers apply FSRS to their flashcard applications, there by improving the us
9
9
  npm install ts-fsrs
10
10
  ```
11
11
 
12
+ # Environment Variables
13
+ If you need to customize default parameters, you can modify the values using `.env`/`.env.local`/`.env.production`/`.env.development`.
14
+
15
+ Copy the [.env.local.example](./example/.env.local.example) file in this directory to .env.local (which will be ignored by Git):
16
+
17
+ ```bash
18
+ cp .env.local.example .env.local
19
+ ```
20
+
12
21
  # Example
13
22
 
14
23
  ```typescript
@@ -0,0 +1,16 @@
1
+ import { Card, FSRSParameters, FSRS } from "./index";
2
+ import { EnvParams } from "./type";
3
+ export declare const envParams: EnvParams;
4
+ export declare const default_request_retention: number;
5
+ export declare const default_maximum_interval: number;
6
+ export declare const default_w: number[];
7
+ export declare const default_enable_fuzz: boolean;
8
+ export declare const FSRSVersion: string;
9
+ export declare const generatorParameters: (props?: Partial<FSRSParameters>) => {
10
+ request_retention: number;
11
+ maximum_interval: number;
12
+ w: number[];
13
+ enable_fuzz: boolean;
14
+ };
15
+ export declare const createEmptyCard: (now?: Date) => Card;
16
+ export declare const fsrs: (params?: Partial<FSRSParameters>) => FSRS;
@@ -0,0 +1,58 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.fsrs = exports.createEmptyCard = exports.generatorParameters = exports.FSRSVersion = exports.default_enable_fuzz = exports.default_w = exports.default_maximum_interval = exports.default_request_retention = exports.envParams = void 0;
7
+ const index_1 = require("./index");
8
+ const dotenv_1 = __importDefault(require("dotenv"));
9
+ dotenv_1.default.config({ path: `./.env.local` });
10
+ dotenv_1.default.config({ path: `./.env.production` });
11
+ dotenv_1.default.config({ path: `./.env.` });
12
+ dotenv_1.default.config({ path: `./.env.development` });
13
+ exports.envParams = {
14
+ FSRS_REQUEST_RETENTION: Number(process.env.FSRS_REQUEST_RETENTION),
15
+ FSRS_MAXIMUM_INTERVAL: Number(process.env.FSRS_MAXIMUM_INTERVAL),
16
+ FSRS_W: process.env.FSRS_W
17
+ ? JSON.parse(process.env.FSRS_W)
18
+ : undefined,
19
+ FSRS_ENABLE_FUZZ: Boolean(process.env.FSRS_ENABLE_FUZZ),
20
+ };
21
+ exports.default_request_retention = !isNaN(exports.envParams.FSRS_REQUEST_RETENTION)
22
+ ? exports.envParams.FSRS_REQUEST_RETENTION
23
+ : 0.9;
24
+ exports.default_maximum_interval = !isNaN(exports.envParams.FSRS_MAXIMUM_INTERVAL)
25
+ ? exports.envParams.FSRS_MAXIMUM_INTERVAL
26
+ : 36500;
27
+ exports.default_w = exports.envParams.FSRS_W || [
28
+ 0.4, 0.6, 2.4, 5.8, 4.93, 0.94, 0.86, 0.01, 1.49, 0.14, 0.94, 2.18, 0.05,
29
+ 0.34, 1.26, 0.29, 2.61,
30
+ ];
31
+ exports.default_enable_fuzz = exports.envParams.FSRS_ENABLE_FUZZ || false;
32
+ exports.FSRSVersion = "3.0.0";
33
+ const generatorParameters = (props) => {
34
+ return {
35
+ request_retention: props?.request_retention || exports.default_request_retention,
36
+ maximum_interval: props?.maximum_interval || exports.default_maximum_interval,
37
+ w: props?.w || exports.default_w,
38
+ enable_fuzz: props?.enable_fuzz || exports.default_enable_fuzz,
39
+ };
40
+ };
41
+ exports.generatorParameters = generatorParameters;
42
+ const createEmptyCard = (now) => {
43
+ return {
44
+ due: now || new Date(),
45
+ stability: 0,
46
+ difficulty: 0,
47
+ elapsed_days: 0,
48
+ scheduled_days: 0,
49
+ reps: 0,
50
+ lapses: 0,
51
+ state: index_1.State.New,
52
+ };
53
+ };
54
+ exports.createEmptyCard = createEmptyCard;
55
+ const fsrs = (params) => {
56
+ return new index_1.FSRS(params || {});
57
+ };
58
+ exports.fsrs = fsrs;
package/dist/fsrs.d.ts CHANGED
@@ -1,12 +1,13 @@
1
- import { Card, FSRSParameters, Rating, SchedulingCard, State } from "./index";
2
- import { int } from "./help";
3
- export default class FSRS {
1
+ import { SchedulingCard } from "./index";
2
+ import { FSRSParameters, Card, State, Rating } from "./models";
3
+ import type { int } from "./type";
4
+ export declare class FSRS {
4
5
  private param;
5
6
  private readonly intervalModifier;
6
7
  private seed?;
7
- constructor(param?: FSRSParameters);
8
+ constructor(param: Partial<FSRSParameters>);
8
9
  repeat: (card: Card, now: Date) => {
9
- 0: {
10
+ 1: {
10
11
  card: Card;
11
12
  log: {
12
13
  rating: Rating;
@@ -16,7 +17,7 @@ export default class FSRS {
16
17
  review: Date;
17
18
  };
18
19
  };
19
- 1: {
20
+ 2: {
20
21
  card: Card;
21
22
  log: {
22
23
  rating: Rating;
@@ -26,7 +27,7 @@ export default class FSRS {
26
27
  review: Date;
27
28
  };
28
29
  };
29
- 2: {
30
+ 3: {
30
31
  card: Card;
31
32
  log: {
32
33
  rating: Rating;
@@ -36,7 +37,7 @@ export default class FSRS {
36
37
  review: Date;
37
38
  };
38
39
  };
39
- 3: {
40
+ 4: {
40
41
  card: Card;
41
42
  log: {
42
43
  rating: Rating;
@@ -59,28 +60,34 @@ export default class FSRS {
59
60
  next_ds(s: SchedulingCard, last_d: number, last_s: number, retrievability: number): void;
60
61
  /**
61
62
  * The formula used is :
62
- * $$S_0(G) = w_0 + G \cdot w_1$$
63
+ * $$S_0(G) = w_{G-1}$$
63
64
  * $$\max \{S_0,0.1\}$$
64
- * @param g Grade (rating at Anki) [0.again,1.hard,2.good,3.easy]
65
+ * @param g Grade (rating at Anki) [1.again,2.hard,3.good,4.easy]
65
66
  * @return Stability (interval when R=90%)
66
67
  */
67
68
  init_stability(g: number): number;
68
69
  /**
69
70
  * The formula used is :
70
- * $$D_0(G) = w_2 + (G-2) \cdot w_3$$
71
+ * $$D_0(G) = w_4 - (G-3) \cdot w_5$$
71
72
  * $$\min \{\max \{D_0(G),1\},10\}$$
72
- * @param g Grade (rating at Anki) [0.again,1.hard,2.good,3.easy]
73
+ * where the D_0(3)=w_4 when the first rating is good.
74
+ * @param g Grade (rating at Anki) [1.again,2.hard,3.good,4.easy]
73
75
  * @return Difficulty D \in [1,10]
74
76
  */
75
77
  init_difficulty(g: number): number;
76
78
  apply_fuzz(ivl: number): number;
79
+ /**
80
+ * Ref:
81
+ * constructor(param: Partial<FSRSParameters>)
82
+ * this.intervalModifier = 9 * (1 / this.param.request_retention - 1);
83
+ */
77
84
  next_interval(s: number): int;
78
85
  /**
79
86
  * The formula used is :
80
- * $$next_d = D + w_4 \cdot (R - 2)$$
87
+ * $$next_d = D - w_6 \cdot (R - 2)$$
81
88
  * $$D^\prime(D,R) = w_5 \cdot D_0(2) +(1 - w_5) \cdot next_d$$
82
89
  * @param d
83
- * @param g Grade (Rating[0.again,1.hard,2.good,3.easy])
90
+ * @param g Grade (rating at Anki) [1.again,2.hard,3.good,4.easy]
84
91
  * @return next_D
85
92
  */
86
93
  next_difficulty(d: number, g: number): number;
@@ -91,24 +98,25 @@ export default class FSRS {
91
98
  constrain_difficulty(difficulty: number): number;
92
99
  /**
93
100
  * The formula used is :
94
- * $$w_5 \cdot init +(1 - w_5) \cdot current$$
95
- * @param init $$w_2 : D_0(2) = w_2 + (R-2) \cdot w_3= w_2$$
96
- * @param current $$D + w_4 \cdot (R - 2)$$
101
+ * $$w_7 \cdot init +(1 - w_7) \cdot current$$
102
+ * @param init $$w_2 : D_0(3) = w_2 + (R-2) \cdot w_3= w_2$$
103
+ * @param current $$D - w_6 \cdot (R - 2)$$
97
104
  * @return difficulty
98
105
  */
99
106
  mean_reversion(init: number, current: number): number;
100
107
  /**
101
108
  * The formula used is :
102
- * $$S^\prime_r(D,S,R) = S\cdot(e^{w_6}\cdot (11-D)\cdot S^{w_7}\cdot(e^{w_8\cdot(1-R)}-1)+1)$$
109
+ * $$S^\prime_r(D,S,R,G) = S\cdot(e^{w_8}\cdot (11-D)\cdot S^{-w_9}\cdot(e^{w_10\cdot(1-R)}-1)\cdot w_15(if G=2) \cdot w_16(if G=4)+1)$$
103
110
  * @param d Difficulty D \in [1,10]
104
111
  * @param s Stability (interval when R=90%)
105
112
  * @param r Retrievability (probability of recall)
113
+ * @param g Grade (Rating[0.again,1.hard,2.good,3.easy])
106
114
  * @return S^\prime_r new stability after recall
107
115
  */
108
- next_recall_stability(d: number, s: number, r: number): number;
116
+ next_recall_stability(d: number, s: number, r: number, g: Rating): number;
109
117
  /**
110
118
  * The formula used is :
111
- * $$S^\prime_f(D,S,R) = w_9\cdot D^{w_{10}}\cdot S^{w_{11}}\cdot e^{w_{12}\cdot(1-R)}.$$
119
+ * $$S^\prime_f(D,S,R) = w_11\cdot D^{-w_{12}}\cdot ((S+1)^{w_{13}}-1) \cdot e^{w_{14}\cdot(1-R)}.$$
112
120
  * @param d Difficulty D \in [1,10]
113
121
  * @param s Stability (interval when R=90%)
114
122
  * @param r Retrievability (probability of recall)
@@ -117,7 +125,7 @@ export default class FSRS {
117
125
  next_forget_stability(d: number, s: number, r: number): number;
118
126
  /**
119
127
  * The formula used is :
120
- * $$R(t,S) = 0.9^{\frac{t}{S}}$$
128
+ * $$R(t,S) = (1 + \frac{t}{9 \cdot S})^{-1},$$
121
129
  * @param t t days since the last review
122
130
  * @param s Stability (interval when R=90%)
123
131
  * @return r Retrievability (probability of recall)
package/dist/fsrs.js CHANGED
@@ -3,25 +3,30 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.FSRS = void 0;
6
7
  const seedrandom_1 = __importDefault(require("seedrandom"));
7
8
  const index_1 = require("./index");
8
9
  const help_1 = require("./help");
10
+ const models_1 = require("./models");
11
+ // Ref: https://github.com/open-spaced-repetition/fsrs4anki/wiki/The-Algorithm#fsrs-v4
9
12
  class FSRS {
10
13
  param;
11
14
  intervalModifier;
12
15
  seed;
13
16
  constructor(param) {
14
- this.param = param || (0, index_1.generatorParameters)();
15
- this.intervalModifier =
16
- Math.log(this.param.request_retention) / Math.log(0.9);
17
+ this.param = (0, index_1.generatorParameters)(param);
18
+ // Ref: https://github.com/open-spaced-repetition/py-fsrs/blob/ecd68e453611eb808c7367c7a5312d7cadeedf5c/src/fsrs/fsrs.py#L79
19
+ // The formula used is : I(r,s)=9 \cdot s \cdot (\frac{1}{r}-1)
20
+ this.intervalModifier = 9 * (1 / this.param.request_retention - 1);
17
21
  }
18
22
  repeat = (card, now) => {
19
23
  card = {
20
24
  ...card,
25
+ last_review: card.last_review ? (0, help_1.fixDate)(card.last_review) : undefined,
21
26
  };
22
27
  now = new Date((0, help_1.fixDate)(now));
23
28
  card.elapsed_days =
24
- card.state === index_1.State.New ? 0 : now.diff(card.last_review, "days"); //相距时间
29
+ card.state === models_1.State.New ? 0 : now.diff(card.last_review, "days"); //相距时间
25
30
  card.last_review = now; // 上次复习时间
26
31
  card.reps += 1;
27
32
  const s = new index_1.SchedulingCard(card);
@@ -29,33 +34,33 @@ class FSRS {
29
34
  this.seed = String(card.last_review.getTime()) + String(card.elapsed_days);
30
35
  let easy_interval, good_interval, hard_interval;
31
36
  switch (card.state) {
32
- case index_1.State.New:
37
+ case models_1.State.New:
33
38
  this.init_ds(s);
34
39
  s.again.due = now.scheduler(1);
35
40
  s.hard.due = now.scheduler(5);
36
41
  s.good.due = now.scheduler(10);
37
- easy_interval = this.next_interval(s.easy.stability * this.param.easy_bonus);
42
+ easy_interval = this.next_interval(s.easy.stability);
38
43
  s.easy.scheduled_days = easy_interval;
39
44
  s.easy.due = now.scheduler(easy_interval, true);
40
45
  break;
41
- case index_1.State.Learning:
42
- case index_1.State.Relearning:
46
+ case models_1.State.Learning:
47
+ case models_1.State.Relearning:
43
48
  hard_interval = 0;
44
49
  good_interval = this.next_interval(s.good.stability);
45
- easy_interval = Math.max(this.next_interval(s.easy.stability * this.param.easy_bonus), good_interval + 1);
50
+ easy_interval = Math.max(this.next_interval(s.easy.stability), good_interval + 1);
46
51
  s.schedule(now, hard_interval, good_interval, easy_interval);
47
52
  break;
48
- case index_1.State.Review: {
53
+ case models_1.State.Review: {
49
54
  const interval = card.elapsed_days;
50
55
  const last_d = card.difficulty;
51
56
  const last_s = card.stability;
52
57
  const retrievability = this.current_retrievability(interval, last_s);
53
58
  this.next_ds(s, last_d, last_s, retrievability);
54
- hard_interval = this.next_interval(last_s * this.param.hard_factor);
59
+ hard_interval = this.next_interval(s.hard.stability);
55
60
  good_interval = this.next_interval(s.good.stability);
56
61
  hard_interval = Math.min(hard_interval, good_interval);
57
62
  good_interval = Math.max(good_interval, hard_interval + 1);
58
- easy_interval = Math.max(this.next_interval(s.easy.stability * this.param.easy_bonus), good_interval + 1);
63
+ easy_interval = Math.max(this.next_interval(s.easy.stability), good_interval + 1);
59
64
  s.schedule(now, hard_interval, good_interval, easy_interval);
60
65
  break;
61
66
  }
@@ -63,21 +68,21 @@ class FSRS {
63
68
  return s.record_log(card, now);
64
69
  };
65
70
  get_retrievability = (card, now) => {
66
- if (card.state !== index_1.State.Review) {
71
+ if (card.state !== models_1.State.Review) {
67
72
  return undefined;
68
73
  }
69
74
  const t = Math.max(now.diff(card.last_review, "days"), 0);
70
75
  return ((this.current_retrievability(t, card.stability) * 100).toFixed(2) + "%");
71
76
  };
72
77
  init_ds(s) {
73
- s.again.difficulty = this.init_difficulty(index_1.Rating.Again);
74
- s.again.stability = this.init_stability(index_1.Rating.Again);
75
- s.hard.difficulty = this.init_difficulty(index_1.Rating.Hard);
76
- s.hard.stability = this.init_stability(index_1.Rating.Hard);
77
- s.good.difficulty = this.init_difficulty(index_1.Rating.Good);
78
- s.good.stability = this.init_stability(index_1.Rating.Good);
79
- s.easy.difficulty = this.init_difficulty(index_1.Rating.Easy);
80
- s.easy.stability = this.init_stability(index_1.Rating.Easy);
78
+ s.again.difficulty = this.init_difficulty(models_1.Rating.Again);
79
+ s.again.stability = this.init_stability(models_1.Rating.Again);
80
+ s.hard.difficulty = this.init_difficulty(models_1.Rating.Hard);
81
+ s.hard.stability = this.init_stability(models_1.Rating.Hard);
82
+ s.good.difficulty = this.init_difficulty(models_1.Rating.Good);
83
+ s.good.stability = this.init_stability(models_1.Rating.Good);
84
+ s.easy.difficulty = this.init_difficulty(models_1.Rating.Easy);
85
+ s.easy.stability = this.init_stability(models_1.Rating.Easy);
81
86
  }
82
87
  /**
83
88
  *
@@ -87,34 +92,35 @@ class FSRS {
87
92
  * @param retrievability Retrievability
88
93
  */
89
94
  next_ds(s, last_d, last_s, retrievability) {
90
- s.again.difficulty = this.next_difficulty(last_d, index_1.Rating.Again);
95
+ s.again.difficulty = this.next_difficulty(last_d, models_1.Rating.Again);
91
96
  s.again.stability = this.next_forget_stability(s.again.difficulty, last_s, retrievability);
92
- s.hard.difficulty = this.next_difficulty(last_d, index_1.Rating.Hard);
93
- s.hard.stability = this.next_recall_stability(s.hard.difficulty, last_s, retrievability);
94
- s.good.difficulty = this.next_difficulty(last_d, index_1.Rating.Good);
95
- s.good.stability = this.next_recall_stability(s.good.difficulty, last_s, retrievability);
96
- s.easy.difficulty = this.next_difficulty(last_d, index_1.Rating.Easy);
97
- s.easy.stability = this.next_recall_stability(s.easy.difficulty, last_s, retrievability);
97
+ s.hard.difficulty = this.next_difficulty(last_d, models_1.Rating.Hard);
98
+ s.hard.stability = this.next_recall_stability(s.hard.difficulty, last_s, retrievability, models_1.Rating.Hard);
99
+ s.good.difficulty = this.next_difficulty(last_d, models_1.Rating.Good);
100
+ s.good.stability = this.next_recall_stability(s.good.difficulty, last_s, retrievability, models_1.Rating.Good);
101
+ s.easy.difficulty = this.next_difficulty(last_d, models_1.Rating.Easy);
102
+ s.easy.stability = this.next_recall_stability(s.easy.difficulty, last_s, retrievability, models_1.Rating.Easy);
98
103
  }
99
104
  /**
100
105
  * The formula used is :
101
- * $$S_0(G) = w_0 + G \cdot w_1$$
106
+ * $$S_0(G) = w_{G-1}$$
102
107
  * $$\max \{S_0,0.1\}$$
103
- * @param g Grade (rating at Anki) [0.again,1.hard,2.good,3.easy]
108
+ * @param g Grade (rating at Anki) [1.again,2.hard,3.good,4.easy]
104
109
  * @return Stability (interval when R=90%)
105
110
  */
106
111
  init_stability(g) {
107
- return Math.max(this.param.w[0] + this.param.w[1] * g, 0.1);
112
+ return Math.max(this.param.w[g - 1], 0.1);
108
113
  }
109
114
  /**
110
115
  * The formula used is :
111
- * $$D_0(G) = w_2 + (G-2) \cdot w_3$$
116
+ * $$D_0(G) = w_4 - (G-3) \cdot w_5$$
112
117
  * $$\min \{\max \{D_0(G),1\},10\}$$
113
- * @param g Grade (rating at Anki) [0.again,1.hard,2.good,3.easy]
118
+ * where the D_0(3)=w_4 when the first rating is good.
119
+ * @param g Grade (rating at Anki) [1.again,2.hard,3.good,4.easy]
114
120
  * @return Difficulty D \in [1,10]
115
121
  */
116
122
  init_difficulty(g) {
117
- return Math.min(Math.max(this.param.w[2] + this.param.w[3] * (g - 2), 1), 10);
123
+ return Math.min(Math.max(this.param.w[4] - (g - 3) * this.param.w[5], 1), 10);
118
124
  }
119
125
  apply_fuzz(ivl) {
120
126
  if (!this.param.enable_fuzz || ivl < 2.5)
@@ -126,21 +132,26 @@ class FSRS {
126
132
  const max_ivl = Math.round(ivl * 1.05 + 1);
127
133
  return Math.floor(fuzz_factor * (max_ivl - min_ivl + 1) + min_ivl);
128
134
  }
135
+ /**
136
+ * Ref:
137
+ * constructor(param: Partial<FSRSParameters>)
138
+ * this.intervalModifier = 9 * (1 / this.param.request_retention - 1);
139
+ */
129
140
  next_interval(s) {
130
141
  const newInterval = this.apply_fuzz(s * this.intervalModifier);
131
142
  return Math.min(Math.max(Math.round(newInterval), 1), this.param.maximum_interval);
132
143
  }
133
144
  /**
134
145
  * The formula used is :
135
- * $$next_d = D + w_4 \cdot (R - 2)$$
146
+ * $$next_d = D - w_6 \cdot (R - 2)$$
136
147
  * $$D^\prime(D,R) = w_5 \cdot D_0(2) +(1 - w_5) \cdot next_d$$
137
148
  * @param d
138
- * @param g Grade (Rating[0.again,1.hard,2.good,3.easy])
149
+ * @param g Grade (rating at Anki) [1.again,2.hard,3.good,4.easy]
139
150
  * @return next_D
140
151
  */
141
152
  next_difficulty(d, g) {
142
- const next_d = d + this.param.w[4] * (g - 2);
143
- return this.constrain_difficulty(this.mean_reversion(this.param.w[2], next_d));
153
+ const next_d = d - this.param.w[6] * (g - 3);
154
+ return this.constrain_difficulty(this.mean_reversion(this.param.w[4], next_d));
144
155
  }
145
156
  /**
146
157
  * The formula used is :
@@ -151,53 +162,58 @@ class FSRS {
151
162
  }
152
163
  /**
153
164
  * The formula used is :
154
- * $$w_5 \cdot init +(1 - w_5) \cdot current$$
155
- * @param init $$w_2 : D_0(2) = w_2 + (R-2) \cdot w_3= w_2$$
156
- * @param current $$D + w_4 \cdot (R - 2)$$
165
+ * $$w_7 \cdot init +(1 - w_7) \cdot current$$
166
+ * @param init $$w_2 : D_0(3) = w_2 + (R-2) \cdot w_3= w_2$$
167
+ * @param current $$D - w_6 \cdot (R - 2)$$
157
168
  * @return difficulty
158
169
  */
159
170
  mean_reversion(init, current) {
160
- return this.param.w[5] * init + (1 - this.param.w[5]) * current;
171
+ return this.param.w[7] * init + (1 - this.param.w[7]) * current;
161
172
  }
162
173
  /**
163
174
  * The formula used is :
164
- * $$S^\prime_r(D,S,R) = S\cdot(e^{w_6}\cdot (11-D)\cdot S^{w_7}\cdot(e^{w_8\cdot(1-R)}-1)+1)$$
175
+ * $$S^\prime_r(D,S,R,G) = S\cdot(e^{w_8}\cdot (11-D)\cdot S^{-w_9}\cdot(e^{w_10\cdot(1-R)}-1)\cdot w_15(if G=2) \cdot w_16(if G=4)+1)$$
165
176
  * @param d Difficulty D \in [1,10]
166
177
  * @param s Stability (interval when R=90%)
167
178
  * @param r Retrievability (probability of recall)
179
+ * @param g Grade (Rating[0.again,1.hard,2.good,3.easy])
168
180
  * @return S^\prime_r new stability after recall
169
181
  */
170
- next_recall_stability(d, s, r) {
182
+ next_recall_stability(d, s, r, g) {
183
+ const hard_penalty = models_1.Rating.Hard === g ? this.param.w[15] : 1;
184
+ const easy_bound = models_1.Rating.Easy === g ? this.param.w[16] : 1;
171
185
  return (s *
172
186
  (1 +
173
- Math.exp(this.param.w[6]) *
187
+ Math.exp(this.param.w[8]) *
174
188
  (11 - d) *
175
- Math.pow(s, this.param.w[7]) *
176
- (Math.exp((1 - r) * this.param.w[8]) - 1)));
189
+ Math.pow(s, -this.param.w[9]) *
190
+ (Math.exp((1 - r) * this.param.w[10]) - 1) *
191
+ hard_penalty *
192
+ easy_bound));
177
193
  }
178
194
  /**
179
195
  * The formula used is :
180
- * $$S^\prime_f(D,S,R) = w_9\cdot D^{w_{10}}\cdot S^{w_{11}}\cdot e^{w_{12}\cdot(1-R)}.$$
196
+ * $$S^\prime_f(D,S,R) = w_11\cdot D^{-w_{12}}\cdot ((S+1)^{w_{13}}-1) \cdot e^{w_{14}\cdot(1-R)}.$$
181
197
  * @param d Difficulty D \in [1,10]
182
198
  * @param s Stability (interval when R=90%)
183
199
  * @param r Retrievability (probability of recall)
184
200
  * @return S^\prime_f new stability after forgetting
185
201
  */
186
202
  next_forget_stability(d, s, r) {
187
- return (this.param.w[9] *
188
- Math.pow(d, this.param.w[10]) *
189
- Math.pow(s, this.param.w[11]) *
190
- Math.exp((1 - r) * this.param.w[12]));
203
+ return (this.param.w[11] *
204
+ Math.pow(d, -this.param.w[12]) *
205
+ (Math.pow(s + 1, this.param.w[13]) - 1) *
206
+ Math.exp((1 - r) * this.param.w[14]));
191
207
  }
192
208
  /**
193
209
  * The formula used is :
194
- * $$R(t,S) = 0.9^{\frac{t}{S}}$$
210
+ * $$R(t,S) = (1 + \frac{t}{9 \cdot S})^{-1},$$
195
211
  * @param t t days since the last review
196
212
  * @param s Stability (interval when R=90%)
197
213
  * @return r Retrievability (probability of recall)
198
214
  */
199
215
  current_retrievability(t, s) {
200
- return Math.exp((Math.log(0.9) * t) / s);
216
+ return Math.pow(1 + t / (9 * s), -1);
201
217
  }
202
218
  }
203
- exports.default = FSRS;
219
+ exports.FSRS = FSRS;
package/dist/help.d.ts CHANGED
@@ -1,10 +1,4 @@
1
- export type unit = "days" | "minutes";
2
- export type int = number & {
3
- __int__: void;
4
- };
5
- export type double = number & {
6
- __double__: void;
7
- };
1
+ import type { int, unit } from "./type";
8
2
  declare global {
9
3
  export interface Date {
10
4
  scheduler(t: int, isDay?: boolean): Date;
package/dist/index.d.ts CHANGED
@@ -1,26 +1,7 @@
1
- import FSRS from "./fsrs";
2
- import { Card, default_easy_bonus, default_enable_fuzz, default_hard_factor, default_maximum_interval, default_request_retention, default_w, FSRSParameters, Rating, RatingType, ReviewLog, SchedulingLog, State, StateType } from "./models";
3
- import { SchedulingCard } from "./scheduler";
4
- import FSRSVersion from "./fsrs_version";
5
- declare const fsrs: (param?: FSRSParameters) => FSRS;
6
- declare const createEmptyCard: (now?: Date) => Card;
7
- declare const generatorParameters: (props?: {
8
- request_retention?: number;
9
- maximum_interval?: number;
10
- easy_bonus?: number;
11
- hard_factor?: number;
12
- w?: number[];
13
- enable_fuzz?: boolean;
14
- }) => {
15
- request_retention: number;
16
- maximum_interval: number;
17
- easy_bonus: number;
18
- hard_factor: number;
19
- w: number[];
20
- enable_fuzz: boolean;
21
- };
22
- export { fsrs, FSRSVersion, State, Rating, SchedulingCard, createEmptyCard, generatorParameters, };
23
- export type { StateType, RatingType, ReviewLog, Card, SchedulingLog, FSRSParameters, };
24
- export { default_request_retention, default_maximum_interval, default_easy_bonus, default_hard_factor, default_w, default_enable_fuzz, };
1
+ export { SchedulingCard } from "./scheduler";
2
+ export { default_request_retention, default_maximum_interval, default_w, default_enable_fuzz, FSRSVersion, generatorParameters, createEmptyCard, fsrs, envParams } from "./default";
25
3
  export { date_scheduler, date_diff, formatDate, show_diff_message, } from "./help";
26
- export type { int, double } from "./help";
4
+ export type { int, double } from "./type";
5
+ export type { FSRSParameters, Card, ReviewLog, StateType, RatingType, } from "./models";
6
+ export { State, Rating } from "./models";
7
+ export { FSRS } from "./fsrs";
package/dist/index.js CHANGED
@@ -1,64 +1,25 @@
1
1
  "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
2
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.show_diff_message = exports.formatDate = exports.date_diff = exports.date_scheduler = exports.default_enable_fuzz = exports.default_w = exports.default_hard_factor = exports.default_easy_bonus = exports.default_maximum_interval = exports.default_request_retention = exports.generatorParameters = exports.createEmptyCard = exports.SchedulingCard = exports.Rating = exports.State = exports.FSRSVersion = exports.fsrs = void 0;
7
- const fsrs_1 = __importDefault(require("./fsrs"));
8
- const models_1 = require("./models");
9
- Object.defineProperty(exports, "default_easy_bonus", { enumerable: true, get: function () { return models_1.default_easy_bonus; } });
10
- Object.defineProperty(exports, "default_enable_fuzz", { enumerable: true, get: function () { return models_1.default_enable_fuzz; } });
11
- Object.defineProperty(exports, "default_hard_factor", { enumerable: true, get: function () { return models_1.default_hard_factor; } });
12
- Object.defineProperty(exports, "default_maximum_interval", { enumerable: true, get: function () { return models_1.default_maximum_interval; } });
13
- Object.defineProperty(exports, "default_request_retention", { enumerable: true, get: function () { return models_1.default_request_retention; } });
14
- Object.defineProperty(exports, "default_w", { enumerable: true, get: function () { return models_1.default_w; } });
15
- Object.defineProperty(exports, "Rating", { enumerable: true, get: function () { return models_1.Rating; } });
16
- Object.defineProperty(exports, "State", { enumerable: true, get: function () { return models_1.State; } });
17
- const scheduler_1 = require("./scheduler");
3
+ exports.FSRS = exports.Rating = exports.State = exports.show_diff_message = exports.formatDate = exports.date_diff = exports.date_scheduler = exports.envParams = exports.fsrs = exports.createEmptyCard = exports.generatorParameters = exports.FSRSVersion = exports.default_enable_fuzz = exports.default_w = exports.default_maximum_interval = exports.default_request_retention = exports.SchedulingCard = void 0;
4
+ var scheduler_1 = require("./scheduler");
18
5
  Object.defineProperty(exports, "SchedulingCard", { enumerable: true, get: function () { return scheduler_1.SchedulingCard; } });
19
- const fsrs_version_1 = __importDefault(require("./fsrs_version"));
20
- exports.FSRSVersion = fsrs_version_1.default;
21
- const fsrs = (param) => {
22
- return new fsrs_1.default(param);
23
- };
24
- exports.fsrs = fsrs;
25
- const createEmptyCard = (now) => {
26
- return {
27
- due: now || new Date(),
28
- stability: 0,
29
- difficulty: 0,
30
- elapsed_days: 0,
31
- scheduled_days: 0,
32
- reps: 0,
33
- lapses: 0,
34
- state: models_1.State.New,
35
- };
36
- };
37
- exports.createEmptyCard = createEmptyCard;
38
- const generatorParameters = (props) => {
39
- if (!props) {
40
- return {
41
- request_retention: models_1.default_request_retention,
42
- maximum_interval: models_1.default_maximum_interval,
43
- easy_bonus: models_1.default_easy_bonus,
44
- hard_factor: models_1.default_hard_factor,
45
- w: models_1.default_w,
46
- enable_fuzz: models_1.default_enable_fuzz,
47
- };
48
- }
49
- const { w, request_retention, hard_factor, maximum_interval, enable_fuzz, easy_bonus, } = props;
50
- return {
51
- request_retention: request_retention || models_1.default_request_retention,
52
- maximum_interval: maximum_interval || models_1.default_maximum_interval,
53
- easy_bonus: easy_bonus || models_1.default_easy_bonus,
54
- hard_factor: hard_factor || models_1.default_hard_factor,
55
- w: w || models_1.default_w,
56
- enable_fuzz: enable_fuzz || models_1.default_enable_fuzz,
57
- };
58
- };
59
- exports.generatorParameters = generatorParameters;
6
+ var default_1 = require("./default");
7
+ Object.defineProperty(exports, "default_request_retention", { enumerable: true, get: function () { return default_1.default_request_retention; } });
8
+ Object.defineProperty(exports, "default_maximum_interval", { enumerable: true, get: function () { return default_1.default_maximum_interval; } });
9
+ Object.defineProperty(exports, "default_w", { enumerable: true, get: function () { return default_1.default_w; } });
10
+ Object.defineProperty(exports, "default_enable_fuzz", { enumerable: true, get: function () { return default_1.default_enable_fuzz; } });
11
+ Object.defineProperty(exports, "FSRSVersion", { enumerable: true, get: function () { return default_1.FSRSVersion; } });
12
+ Object.defineProperty(exports, "generatorParameters", { enumerable: true, get: function () { return default_1.generatorParameters; } });
13
+ Object.defineProperty(exports, "createEmptyCard", { enumerable: true, get: function () { return default_1.createEmptyCard; } });
14
+ Object.defineProperty(exports, "fsrs", { enumerable: true, get: function () { return default_1.fsrs; } });
15
+ Object.defineProperty(exports, "envParams", { enumerable: true, get: function () { return default_1.envParams; } });
60
16
  var help_1 = require("./help");
61
17
  Object.defineProperty(exports, "date_scheduler", { enumerable: true, get: function () { return help_1.date_scheduler; } });
62
18
  Object.defineProperty(exports, "date_diff", { enumerable: true, get: function () { return help_1.date_diff; } });
63
19
  Object.defineProperty(exports, "formatDate", { enumerable: true, get: function () { return help_1.formatDate; } });
64
20
  Object.defineProperty(exports, "show_diff_message", { enumerable: true, get: function () { return help_1.show_diff_message; } });
21
+ var models_1 = require("./models");
22
+ Object.defineProperty(exports, "State", { enumerable: true, get: function () { return models_1.State; } });
23
+ Object.defineProperty(exports, "Rating", { enumerable: true, get: function () { return models_1.Rating; } });
24
+ var fsrs_1 = require("./fsrs");
25
+ Object.defineProperty(exports, "FSRS", { enumerable: true, get: function () { return fsrs_1.FSRS; } });
package/dist/models.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export type StateType = "Learning" | "New" | "Review" | "Relearning";
1
+ export type StateType = "New" | "Learning" | "Review" | "Relearning";
2
2
  export declare enum State {
3
3
  New = 0,
4
4
  Learning = 1,
@@ -7,10 +7,10 @@ export declare enum State {
7
7
  }
8
8
  export type RatingType = "Again" | "Hard" | "Good" | "Easy";
9
9
  export declare enum Rating {
10
- Again = 0,
11
- Hard = 1,
12
- Good = 2,
13
- Easy = 3
10
+ Again = 1,
11
+ Hard = 2,
12
+ Good = 3,
13
+ Easy = 4
14
14
  }
15
15
  export interface ReviewLog {
16
16
  rating: Rating;
@@ -30,23 +30,9 @@ export interface Card {
30
30
  state: State;
31
31
  last_review?: Date;
32
32
  }
33
- export interface SchedulingLog {
34
- [key: number]: {
35
- card: Card;
36
- log: ReviewLog;
37
- };
38
- }
39
33
  export interface FSRSParameters {
40
34
  request_retention: number;
41
35
  maximum_interval: number;
42
- easy_bonus: number;
43
- hard_factor: number;
44
36
  w: number[];
45
37
  enable_fuzz: boolean;
46
38
  }
47
- export declare const default_request_retention = 0.9;
48
- export declare const default_maximum_interval = 36500;
49
- export declare const default_easy_bonus = 1.3;
50
- export declare const default_hard_factor = 1.2;
51
- export declare const default_w: number[];
52
- export declare const default_enable_fuzz = false;
package/dist/models.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.default_enable_fuzz = exports.default_w = exports.default_hard_factor = exports.default_easy_bonus = exports.default_maximum_interval = exports.default_request_retention = exports.Rating = exports.State = void 0;
3
+ exports.Rating = exports.State = void 0;
4
4
  var State;
5
5
  (function (State) {
6
6
  State[State["New"] = 0] = "New";
@@ -10,16 +10,8 @@ var State;
10
10
  })(State || (exports.State = State = {}));
11
11
  var Rating;
12
12
  (function (Rating) {
13
- Rating[Rating["Again"] = 0] = "Again";
14
- Rating[Rating["Hard"] = 1] = "Hard";
15
- Rating[Rating["Good"] = 2] = "Good";
16
- Rating[Rating["Easy"] = 3] = "Easy";
13
+ Rating[Rating["Again"] = 1] = "Again";
14
+ Rating[Rating["Hard"] = 2] = "Hard";
15
+ Rating[Rating["Good"] = 3] = "Good";
16
+ Rating[Rating["Easy"] = 4] = "Easy";
17
17
  })(Rating || (exports.Rating = Rating = {}));
18
- exports.default_request_retention = 0.9;
19
- exports.default_maximum_interval = 36500;
20
- exports.default_easy_bonus = 1.3;
21
- exports.default_hard_factor = 1.2;
22
- exports.default_w = [
23
- 1, 1, 5, -0.5, -0.5, 0.2, 1.4, -0.12, 0.8, 2, -0.2, 0.2, 1,
24
- ];
25
- exports.default_enable_fuzz = false;
@@ -9,7 +9,7 @@ export declare class SchedulingCard {
9
9
  update_state(state: State): this;
10
10
  schedule(now: Date, hard_interval: number, good_interval: number, easy_interval: number): SchedulingCard;
11
11
  record_log(card: Card, now: Date): {
12
- 0: {
12
+ 1: {
13
13
  card: Card;
14
14
  log: {
15
15
  rating: Rating;
@@ -19,7 +19,7 @@ export declare class SchedulingCard {
19
19
  review: Date;
20
20
  };
21
21
  };
22
- 1: {
22
+ 2: {
23
23
  card: Card;
24
24
  log: {
25
25
  rating: Rating;
@@ -29,7 +29,7 @@ export declare class SchedulingCard {
29
29
  review: Date;
30
30
  };
31
31
  };
32
- 2: {
32
+ 3: {
33
33
  card: Card;
34
34
  log: {
35
35
  rating: Rating;
@@ -39,7 +39,7 @@ export declare class SchedulingCard {
39
39
  review: Date;
40
40
  };
41
41
  };
42
- 3: {
42
+ 4: {
43
43
  card: Card;
44
44
  log: {
45
45
  rating: Rating;
package/dist/type.d.ts ADDED
@@ -0,0 +1,13 @@
1
+ export type unit = "days" | "minutes";
2
+ export type int = number & {
3
+ __int__: void;
4
+ };
5
+ export type double = number & {
6
+ __double__: void;
7
+ };
8
+ export interface EnvParams {
9
+ FSRS_REQUEST_RETENTION: number;
10
+ FSRS_MAXIMUM_INTERVAL: number;
11
+ FSRS_W?: number[];
12
+ FSRS_ENABLE_FUZZ?: boolean;
13
+ }
package/dist/type.js ADDED
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ts-fsrs",
3
- "version": "2.1.1",
3
+ "version": "3.0.0",
4
4
  "description": "ts-fsrs is a TypeScript package used to implement the Free Spaced Repetition Scheduler (FSRS) algorithm. It helps developers apply FSRS to their flashcard applications, thereby improving the user learning experience.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -10,11 +10,12 @@
10
10
  "FSRS"
11
11
  ],
12
12
  "dependencies": {
13
+ "dotenv": "^16.3.1",
13
14
  "seedrandom": "^3.0.5"
14
15
  },
15
16
  "devDependencies": {
16
- "@types/node": "^20.6.2",
17
17
  "@types/jest": "^29.5.5",
18
+ "@types/node": "^20.6.2",
18
19
  "@types/seedrandom": "^3.0.5",
19
20
  "@typescript-eslint/eslint-plugin": "^6.7.0",
20
21
  "@typescript-eslint/parser": "^6.7.0",
@@ -1,2 +0,0 @@
1
- declare const FSRSVersion: string;
2
- export default FSRSVersion;
@@ -1,7 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
4
- // @ts-ignore
5
- const package_json_1 = require("../../package.json");
6
- const FSRSVersion = package_json_1.version;
7
- exports.default = FSRSVersion;