From e1250a42bcd579a7877482dd51f7eaacc0611d7a Mon Sep 17 00:00:00 2001 From: Brian Wo <45139213+brainwo@users.noreply.github.com> Date: Thu, 29 Aug 2024 13:30:13 +0800 Subject: [PATCH 1/8] Port FSRS5 from py-fsrs Port from https://github.com/open-spaced-repetition/py-fsrs/compare/1b4cbe4...a200a78 --- lib/src/fsrs_base.dart | 157 ++++++++++++++++++++++++++++------------- lib/src/models.dart | 155 +++++++++++++++++++++++++++------------- 2 files changed, 213 insertions(+), 99 deletions(-) diff --git a/lib/src/fsrs_base.dart b/lib/src/fsrs_base.dart index d9537b7..79490cf 100644 --- a/lib/src/fsrs_base.dart +++ b/lib/src/fsrs_base.dart @@ -4,23 +4,48 @@ import './models.dart'; class FSRS { late Parameters p; - late double decay; - late double factor; - - FSRS() { - p = Parameters(); + late final double decay; + late final double factor; + + FSRS({ + double? requestRetention, + int? maximumInterval, + double? w, + }) { + p = Parameters( + requestRetention: requestRetention, + ); decay = -0.5; factor = pow(0.9, 1 / decay) - 1; } - Map repeat(Card card, DateTime now) { + (Card card, ReviewLog reviewLog) reviewCard( + Card card, + Rating rating, + DateTime? now, + ) { + final date = now ?? DateTime.now(); + final schedulingCards = repeat(card, date); + + final reviewCard = schedulingCards[rating]!.card; + final reviewLog = schedulingCards[rating]!.reviewLog; + + return (reviewCard, reviewLog); + } + + Map repeat( + Card card, + DateTime? now, + ) { + final date = now ?? DateTime.now(); + card = card.copyWith(); if (card.state == State.newState) { card.elapsedDays = 0; } else { - card.elapsedDays = now.difference(card.lastReview).inDays; + card.elapsedDays = date.difference(card.lastReview).inDays; } - card.lastReview = now; + card.lastReview = date; card.reps++; final s = SchedulingCards(card); @@ -30,27 +55,32 @@ class FSRS { case State.newState: _initDS(s); - s.again.due = now.add(Duration(minutes: 1)); - s.hard.due = now.add(Duration(minutes: 5)); - s.good.due = now.add(Duration(minutes: 10)); + s.again.due = date.add(Duration(minutes: 1)); + s.hard.due = date.add(Duration(minutes: 5)); + s.good.due = date.add(Duration(minutes: 10)); final easyInterval = _nextInterval(s.easy.stability); s.easy.scheduledDays = easyInterval; - s.easy.due = now.add(Duration(days: easyInterval)); + s.easy.due = date.add(Duration(days: easyInterval)); case State.learning: case State.relearning: + final interval = card.elapsedDays; + final lastD = card.difficulty; + final lastS = card.stability; + final retrievability = _forgettingCurve(interval, lastS); + _nextDS(s, lastD, lastS, retrievability, card.state); + final hardInterval = 0; final goodInterval = _nextInterval(s.good.stability); final easyInterval = max(_nextInterval(s.easy.stability), goodInterval + 1); - s.schedule(now, hardInterval.toDouble(), goodInterval.toDouble(), - easyInterval.toDouble()); + s.schedule(date, hardInterval, goodInterval, easyInterval); case State.review: final interval = card.elapsedDays; final lastD = card.difficulty; final lastS = card.stability; final retrievability = _forgettingCurve(interval, lastS); - _nextDS(s, lastD, lastS, retrievability); + _nextDS(s, lastD, lastS, retrievability, card.state); var hardInterval = _nextInterval(s.hard.stability); var goodInterval = _nextInterval(s.good.stability); @@ -58,53 +88,50 @@ class FSRS { goodInterval = max(goodInterval, hardInterval + 1); final easyInterval = max(_nextInterval(s.easy.stability), goodInterval + 1); - s.schedule(now, hardInterval.toDouble(), goodInterval.toDouble(), - easyInterval.toDouble()); + s.schedule(date, hardInterval, goodInterval, easyInterval); } - return s.recordLog(card, now); + return s.recordLog(card, date); } void _initDS(SchedulingCards s) { - s.again.difficulty = _initDifficulty(Rating.again.val); + s.again.difficulty = _initDifficulty(Rating.again); s.again.stability = _initStability(Rating.again.val); - s.hard.difficulty = _initDifficulty(Rating.hard.val); + s.hard.difficulty = _initDifficulty(Rating.hard); s.hard.stability = _initStability(Rating.hard.val); - s.good.difficulty = _initDifficulty(Rating.good.val); + s.good.difficulty = _initDifficulty(Rating.good); s.good.stability = _initStability(Rating.good.val); - s.easy.difficulty = _initDifficulty(Rating.easy.val); + s.easy.difficulty = _initDifficulty(Rating.easy); s.easy.stability = _initStability(Rating.easy.val); } - double _initStability(int r) { - return max(p.w[r - 1], 0.1); - } + double _initStability(int r) => max(p.w[r - 1], 0.1); - double _initDifficulty(int r) { - return min(max(p.w[4] - p.w[5] * (r - 3), 1), 10); - } + double _initDifficulty(Rating r) => + min(max(p.w[4] - exp(p.w[5] * (r.val - 1) + 1), 1), 10); - double _forgettingCurve(int elapsedDays, double stability) { - return pow(1 + factor * elapsedDays / stability, decay).toDouble(); - } + double _forgettingCurve(int elapsedDays, double stability) => + pow(1 + factor * elapsedDays / stability, decay).toDouble(); int _nextInterval(double s) { final newInterval = s / factor * (pow(p.requestRetention, 1 / decay) - 1); return min(max(newInterval.round(), 1), p.maximumInterval); } - double _nextDifficulty(double d, int r) { - final nextD = d - p.w[6] * (r - 3); - return min(max(_meanReversion(p.w[4], nextD), 1), 10); + double _nextDifficulty(double d, Rating r) { + final nextD = d - p.w[6] * (r.val - 3); + return min(max(_meanReversion(_initDifficulty(Rating.easy), nextD), 1), 10); } - double _meanReversion(double init, double current) { - return p.w[7] * init + (1 - p.w[7]) * current; - } + double _shortTermStability(double stability, Rating rating) => + stability * exp(p.w[17] * (rating.val - 3 + p.w[18])); + + double _meanReversion(double init, double current) => + p.w[7] * init + (1 - p.w[7]) * current; double _nextRecallStability(double d, double s, double r, Rating rating) { - final hardPenalty = (rating == Rating.hard) ? p.w[15] : 1; - final easyBonus = (rating == Rating.easy) ? p.w[16] : 1; + final hardPenalty = rating == Rating.hard ? p.w[15] : 1; + final easyBonus = rating == Rating.easy ? p.w[16] : 1; return s * (1 + exp(p.w[8]) * @@ -123,17 +150,45 @@ class FSRS { } void _nextDS( - SchedulingCards s, double lastD, double lastS, double retrievability) { - s.again.difficulty = _nextDifficulty(lastD, Rating.again.val); - s.again.stability = _nextForgetStability(lastD, lastS, retrievability); - s.hard.difficulty = _nextDifficulty(lastD, Rating.hard.val); - s.hard.stability = - _nextRecallStability(lastD, lastS, retrievability, Rating.hard); - s.good.difficulty = _nextDifficulty(lastD, Rating.good.val); - s.good.stability = - _nextRecallStability(lastD, lastS, retrievability, Rating.good); - s.easy.difficulty = _nextDifficulty(lastD, Rating.easy.val); - s.easy.stability = - _nextRecallStability(lastD, lastS, retrievability, Rating.easy); + SchedulingCards s, + double lastD, + double lastS, + double retrievability, + State state, + ) { + switch (state) { + case State.learning: + case State.relearning: + s.again.stability = _shortTermStability(lastS, Rating.again); + s.hard.stability = _shortTermStability(lastS, Rating.hard); + s.good.stability = _shortTermStability(lastS, Rating.good); + s.easy.stability = _shortTermStability(lastS, Rating.easy); + case State.review: + s.again.stability = _nextForgetStability( + lastD, + lastS, + retrievability, + ); + s.hard.stability = _nextRecallStability( + lastD, + lastS, + retrievability, + Rating.hard, + ); + s.good.stability = _nextRecallStability( + lastD, + lastS, + retrievability, + Rating.good, + ); + s.easy.stability = _nextRecallStability( + lastD, + lastS, + retrievability, + Rating.easy, + ); + case State.newState: + return; + } } } diff --git a/lib/src/models.dart b/lib/src/models.dart index 803f339..8b042dd 100644 --- a/lib/src/models.dart +++ b/lib/src/models.dart @@ -35,8 +35,14 @@ class ReviewLog { DateTime review; State state; - ReviewLog(this.rating, this.scheduledDays, this.elapsedDays, this.review, - this.state); + ReviewLog( + this.rating, + this.scheduledDays, + this.elapsedDays, + this.review, + this.state, + ); + @override String toString() { return jsonEncode({ @@ -133,64 +139,117 @@ class SchedulingCards { void schedule( DateTime now, - double hardInterval, - double goodInterval, - double easyInterval, + int hardInterval, + int goodInterval, + int easyInterval, ) { again.scheduledDays = 0; - hard.scheduledDays = hardInterval.toInt(); - good.scheduledDays = goodInterval.toInt(); - easy.scheduledDays = easyInterval.toInt(); + hard.scheduledDays = hardInterval; + good.scheduledDays = goodInterval; + easy.scheduledDays = easyInterval; again.due = now.add(Duration(minutes: 5)); hard.due = (hardInterval > 0) - ? now.add(Duration(days: hardInterval.toInt())) + ? now.add(Duration(days: hardInterval)) : now.add(Duration(minutes: 10)); - good.due = now.add(Duration(days: goodInterval.toInt())); - easy.due = now.add(Duration(days: easyInterval.toInt())); + good.due = now.add(Duration(days: goodInterval)); + easy.due = now.add(Duration(days: easyInterval)); } - Map recordLog(Card card, DateTime now) { - return { - Rating.again: SchedulingInfo( + Map recordLog(Card card, DateTime now) => { + Rating.again: SchedulingInfo( again, - ReviewLog(Rating.again, again.scheduledDays, card.elapsedDays, now, - card.state)), - Rating.hard: SchedulingInfo( + ReviewLog( + Rating.again, + again.scheduledDays, + card.elapsedDays, + now, + card.state, + ), + ), + Rating.hard: SchedulingInfo( hard, - ReviewLog(Rating.hard, hard.scheduledDays, card.elapsedDays, now, - card.state)), - Rating.good: SchedulingInfo( + ReviewLog( + Rating.hard, + hard.scheduledDays, + card.elapsedDays, + now, + card.state, + ), + ), + Rating.good: SchedulingInfo( good, - ReviewLog(Rating.good, good.scheduledDays, card.elapsedDays, now, - card.state)), - Rating.easy: SchedulingInfo( + ReviewLog( + Rating.good, + good.scheduledDays, + card.elapsedDays, + now, + card.state, + ), + ), + Rating.easy: SchedulingInfo( easy, - ReviewLog(Rating.easy, easy.scheduledDays, card.elapsedDays, now, - card.state)), - }; - } + ReviewLog( + Rating.easy, + easy.scheduledDays, + card.elapsedDays, + now, + card.state, + ), + ), + }; } class Parameters { - double requestRetention = 0.9; - int maximumInterval = 36500; - List w = [ - 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, - 0.34, - 1.26, - 0.29, - 2.61 - ]; + Parameters({ + double? requestRetention = 0.9, + int? maximumInterval = 36500, + List? w = const [ + 0.4072, + 1.1829, + 3.1262, + 15.4722, + 7.2102, + 0.5316, + 1.0651, + 0.0234, + 1.616, + 0.1544, + 1.0824, + 1.9813, + 0.0953, + 0.2975, + 2.2042, + 0.2407, + 2.9466, + 0.5034, + 0.6567, + ], + }) : requestRetention = requestRetention ?? 0.9, + maximumInterval = maximumInterval ?? 36500, + w = w ?? + const [ + 0.4072, + 1.1829, + 3.1262, + 15.4722, + 7.2102, + 0.5316, + 1.0651, + 0.0234, + 1.616, + 0.1544, + 1.0824, + 1.9813, + 0.0953, + 0.2975, + 2.2042, + 0.2407, + 2.9466, + 0.5034, + 0.6567, + ]; + + double requestRetention; + int maximumInterval; + List w; } From 9bc397bd12e5ca80df482e66c0425af6d9581d90 Mon Sep 17 00:00:00 2001 From: Brian Wo <45139213+brainwo@users.noreply.github.com> Date: Thu, 29 Aug 2024 13:35:36 +0800 Subject: [PATCH 2/8] fix: missing argument for Parameters --- lib/src/fsrs_base.dart | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/src/fsrs_base.dart b/lib/src/fsrs_base.dart index 79490cf..61909b7 100644 --- a/lib/src/fsrs_base.dart +++ b/lib/src/fsrs_base.dart @@ -10,10 +10,12 @@ class FSRS { FSRS({ double? requestRetention, int? maximumInterval, - double? w, + List? w, }) { p = Parameters( requestRetention: requestRetention, + maximumInterval: maximumInterval, + w: w, ); decay = -0.5; factor = pow(0.9, 1 / decay) - 1; From cd1a895140af497fd0a13ebd50bd24f98061884b Mon Sep 17 00:00:00 2001 From: Brian Wo <45139213+brainwo@users.noreply.github.com> Date: Thu, 29 Aug 2024 17:39:27 +0800 Subject: [PATCH 3/8] refactor code style --- lib/src/fsrs_base.dart | 5 +++-- lib/src/models.dart | 28 ++++------------------------ 2 files changed, 7 insertions(+), 26 deletions(-) diff --git a/lib/src/fsrs_base.dart b/lib/src/fsrs_base.dart index 61909b7..e2ffbb8 100644 --- a/lib/src/fsrs_base.dart +++ b/lib/src/fsrs_base.dart @@ -36,9 +36,9 @@ class FSRS { } Map repeat( - Card card, + Card card, [ DateTime? now, - ) { + ]) { final date = now ?? DateTime.now(); card = card.copyWith(); @@ -90,6 +90,7 @@ class FSRS { goodInterval = max(goodInterval, hardInterval + 1); final easyInterval = max(_nextInterval(s.easy.stability), goodInterval + 1); + s.schedule(date, hardInterval, goodInterval, easyInterval); } diff --git a/lib/src/models.dart b/lib/src/models.dart index 8b042dd..fd36441 100644 --- a/lib/src/models.dart +++ b/lib/src/models.dart @@ -148,7 +148,7 @@ class SchedulingCards { good.scheduledDays = goodInterval; easy.scheduledDays = easyInterval; again.due = now.add(Duration(minutes: 5)); - hard.due = (hardInterval > 0) + hard.due = hardInterval > 0 ? now.add(Duration(days: hardInterval)) : now.add(Duration(minutes: 10)); good.due = now.add(Duration(days: goodInterval)); @@ -201,29 +201,9 @@ class SchedulingCards { class Parameters { Parameters({ - double? requestRetention = 0.9, - int? maximumInterval = 36500, - List? w = const [ - 0.4072, - 1.1829, - 3.1262, - 15.4722, - 7.2102, - 0.5316, - 1.0651, - 0.0234, - 1.616, - 0.1544, - 1.0824, - 1.9813, - 0.0953, - 0.2975, - 2.2042, - 0.2407, - 2.9466, - 0.5034, - 0.6567, - ], + double? requestRetention, + int? maximumInterval, + List? w, }) : requestRetention = requestRetention ?? 0.9, maximumInterval = maximumInterval ?? 36500, w = w ?? From 58c4f7840a9767fae917aa5374a29f428653a237 Mon Sep 17 00:00:00 2001 From: Brian Wo <45139213+brainwo@users.noreply.github.com> Date: Thu, 29 Aug 2024 17:40:40 +0800 Subject: [PATCH 4/8] Add missing difficulty adjusment --- lib/src/fsrs_base.dart | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/src/fsrs_base.dart b/lib/src/fsrs_base.dart index e2ffbb8..a32912c 100644 --- a/lib/src/fsrs_base.dart +++ b/lib/src/fsrs_base.dart @@ -159,6 +159,11 @@ class FSRS { double retrievability, State state, ) { + s.again.difficulty = _nextDifficulty(lastD, Rating.again); + s.hard.difficulty = _nextDifficulty(lastD, Rating.hard); + s.good.difficulty = _nextDifficulty(lastD, Rating.good); + s.easy.difficulty = _nextDifficulty(lastD, Rating.easy); + switch (state) { case State.learning: case State.relearning: From 9e74e25c699e9917f39b3e71de382d0da7bbce97 Mon Sep 17 00:00:00 2001 From: Brian Wo <45139213+brainwo@users.noreply.github.com> Date: Fri, 30 Aug 2024 04:23:30 +0800 Subject: [PATCH 5/8] WIP test --- test/fsrs_test.dart | 304 +++++++++++++++++++++++++++++++++----------- 1 file changed, 231 insertions(+), 73 deletions(-) diff --git a/test/fsrs_test.dart b/test/fsrs_test.dart index 2e45102..1616b25 100644 --- a/test/fsrs_test.dart +++ b/test/fsrs_test.dart @@ -1,84 +1,242 @@ import 'package:fsrs/fsrs.dart'; import 'package:test/test.dart'; -import 'package:collection/collection.dart'; + +const testW = [ + 0.4197, + 1.1869, + 3.0412, + 15.2441, + 7.1434, + 0.6477, + 1.0007, + 0.0674, + 1.6597, + 0.1712, + 1.1178, + 2.0225, + 0.0904, + 0.3025, + 2.1214, + 0.2498, + 2.9466, + 0.4891, + 0.6468, +]; void main() { - group('A group of tests', () { - setUp(() { - // Additional setup goes here. + test('Review Card', () { + final f = FSRS(w: testW); + var card = Card(); + var now = DateTime(2022, 11, 29, 12, 30).toUtc(); + + const ratings = [ + Rating.good, + Rating.good, + Rating.good, + Rating.good, + Rating.good, + Rating.good, + Rating.again, + Rating.again, + Rating.good, + Rating.good, + Rating.good, + Rating.good, + Rating.good, + ]; + + List ivlHistory = []; + + for (final rating in ratings) { + (card, _) = f.reviewCard(card, rating, now); + final ivl = card.scheduledDays; + ivlHistory.add(ivl); + now = card.due; + } + + expect(ivlHistory, [ + 0, + 4, + 17, + 62, + 198, + 563, + 0, + 0, + 9, + 27, + 74, + 190, + 457, + ]); + }); + + test('Memo State', () { + final f = FSRS(w: testW); + var card = Card(); + var now = DateTime(2022, 11, 29, 12, 30).toUtc(); + + var schedulingCards = f.repeat(card, now); + const reviews = [ + (Rating.again, 0), + (Rating.good, 0), + (Rating.good, 1), + (Rating.good, 3), + (Rating.good, 8), + (Rating.good, 21), + ]; + + for (final (rating, ivl) in reviews) { + card = schedulingCards[rating]!.card; + now = now.add(Duration(days: ivl)); + schedulingCards = f.repeat(card, now); + } + + expect(schedulingCards[Rating.good]!.card.stability, 71.4554); + expect(schedulingCards[Rating.good]!.card.difficulty, 5.0976); + }); + + test('Default Arg', () { + final f = FSRS(); + + var card = Card(); + + final schedulingCards = f.repeat(card); + + final cardRating = Rating.good; + + card = schedulingCards[cardRating]!.card; + + final due = card.due; + + final timeDelta = due.difference(DateTime.now().toUtc()); + + expect(timeDelta.inSeconds, greaterThan(500)); + }); + + group("Custom Scheduler Args:", () { + const ratings = [ + Rating.good, + Rating.good, + Rating.good, + Rating.good, + Rating.good, + Rating.good, + Rating.again, + Rating.again, + Rating.good, + Rating.good, + Rating.good, + Rating.good, + Rating.good, + ]; + + test('IVL', () { + final f = FSRS( + requestRetention: 0.9, + maximumInterval: 36500, + w: [ + 0.4197, + 1.1869, + 3.0412, + 15.2441, + 7.1434, + 0.6477, + 1.0007, + 0.0674, + 1.6597, + 0.1712, + 1.1178, + 2.0225, + 0.0904, + 0.3025, + 2.1214, + 0.2498, + 2.9466, + 0, + 0.6468, + ], + ); + + var card = Card(); + var now = DateTime(2022, 11, 29, 12, 30); + final List ivlHistory = []; + + for (final rating in ratings) { + (card, _) = f.reviewCard(card, rating, now); + final ivl = card.scheduledDays; + ivlHistory.add(ivl); + now = card.due; + } + + expect(ivlHistory, [0, 3, 13, 50, 163, 473, 0, 0, 12, 34, 91, 229, 541]); }); - test('First Test', () { - testRepeat(); + test('Verify parameters', () { + const requestRetention = 0.85; + const maximumInterval = 3650; + const w = [ + 0.1456, + 0.4186, + 1.1104, + 4.1315, + 5.2417, + 1.3098, + 0.8975, + 0.0000, + 1.5674, + 0.0567, + 0.9661, + 2.0275, + 0.1592, + 0.2446, + 1.5071, + 0.2272, + 2.8755, + 1.234, + 5.6789, + ]; + + final f = FSRS( + requestRetention: requestRetention, + maximumInterval: maximumInterval, + w: w, + ); + + var card = Card(); + var now = DateTime(2022, 11, 29, 12, 30); + final List ivlHistory = []; + + for (final rating in ratings) { + (card, _) = f.reviewCard(card, rating, now); + final ivl = card.scheduledDays; + ivlHistory.add(ivl); + now = card.due; + } + + expect(f.p.w, w); + expect(f.p.requestRetention, requestRetention); + expect(f.p.maximumInterval, maximumInterval); }); }); -} -void printSchedulingCards(Map schedulingCards) { - print("again.card: ${schedulingCards[Rating.again]?.card}"); - print("again.reviewLog: ${schedulingCards[Rating.again]?.reviewLog}"); - print("hard.card: ${schedulingCards[Rating.hard]?.card}"); - print("hard.reviewLog: ${schedulingCards[Rating.hard]?.reviewLog}"); - print("good.card: ${schedulingCards[Rating.good]?.card}"); - print("good.reviewLog: ${schedulingCards[Rating.good]?.reviewLog}"); - print("easy.card: ${schedulingCards[Rating.easy]?.card}"); - print("easy.reviewLog: ${schedulingCards[Rating.easy]?.reviewLog}"); - print(""); -} + test('DateTime', () { + final f = FSRS(); + var card = Card(); + + expect(DateTime.now().compareTo(card.due), greaterThanOrEqualTo(0)); -void testRepeat() { - var f = FSRS(); - f.p.w = [ - 1.14, - 1.01, - 5.44, - 14.67, - 5.3024, - 1.5662, - 1.2503, - 0.0028, - 1.5489, - 0.1763, - 0.9953, - 2.7473, - 0.0179, - 0.3105, - 0.3976, - 0.0, - 2.0902 - ]; - var card = Card(); - var now = DateTime(2022, 11, 29, 12, 30, 0, 0); - var schedulingCards = f.repeat(card, now); - printSchedulingCards(schedulingCards); - - var ratings = [ - Rating.good, - Rating.good, - Rating.good, - Rating.good, - Rating.good, - Rating.good, - Rating.again, - Rating.again, - Rating.good, - Rating.good, - Rating.good, - Rating.good, - Rating.good, - ]; - var ivlHistory = []; - - for (var rating in ratings) { - card = schedulingCards[rating]?.card ?? Card(); - var ivl = card.scheduledDays; - ivlHistory.add(ivl); - now = card.due; - schedulingCards = f.repeat(card, now); - printSchedulingCards(schedulingCards); - } - - print(ivlHistory); - assert(ListEquality() - .equals(ivlHistory, [0, 5, 16, 43, 106, 236, 0, 0, 12, 25, 47, 85, 147])); + final schedulingCards = f.repeat(card, DateTime.now().toUtc()); + card = schedulingCards[Rating.good]!.card; + + expect(card.due.compareTo(card.lastReview), greaterThanOrEqualTo(0)); + }); + + test('Card Serialization', () { + // TODO + }); + + test('ReviewLog Serialization', () { + // TODO + }); } From 7206cbf062998a325ac920113c7b1ad45485a2d7 Mon Sep 17 00:00:00 2001 From: Brian Wo <45139213+brainwo@users.noreply.github.com> Date: Fri, 30 Aug 2024 04:23:53 +0800 Subject: [PATCH 6/8] chore: cleaning up dependencies --- pubspec.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 2759d27..f605f78 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -9,7 +9,6 @@ environment: # Add regular dependencies here. dependencies: - collection: ^1.18.0 freezed_annotation: ^2.4.1 json_annotation: ^4.9.0 # path: ^1.8.0 From ea404347470f96a6093a4ebf8f027716ea91ccae Mon Sep 17 00:00:00 2001 From: Brian Wo <45139213+brainwo@users.noreply.github.com> Date: Fri, 30 Aug 2024 11:10:50 +0800 Subject: [PATCH 7/8] feat: add Todo for documentations --- lib/src/fsrs_base.dart | 57 ++++++++++++++++++++++++++----------- lib/src/models.dart | 49 +++++++++++++++++++++++++++---- lib/src/models.freezed.dart | 20 +++++++++++-- test/fsrs_test.dart | 15 ++++++---- 4 files changed, 110 insertions(+), 31 deletions(-) diff --git a/lib/src/fsrs_base.dart b/lib/src/fsrs_base.dart index a32912c..0eacde0 100644 --- a/lib/src/fsrs_base.dart +++ b/lib/src/fsrs_base.dart @@ -2,26 +2,39 @@ import 'dart:core'; import 'dart:math'; import './models.dart'; -class FSRS { - late Parameters p; - late final double decay; - late final double factor; +/// TODO: document +class ReviewCard { + const ReviewCard({ + required this.card, + required this.reviewLog, + }); + + /// TODO: document + final Card card; + + /// TODO: document + final ReviewLog reviewLog; +} +/// TODO: document +class FSRS { FSRS({ double? requestRetention, int? maximumInterval, List? w, - }) { - p = Parameters( - requestRetention: requestRetention, - maximumInterval: maximumInterval, - w: w, - ); - decay = -0.5; - factor = pow(0.9, 1 / decay) - 1; - } - - (Card card, ReviewLog reviewLog) reviewCard( + }) : p = Parameters( + requestRetention: requestRetention, + maximumInterval: maximumInterval, + w: w, + ), + factor = pow(0.9, 1 / decay) - 1; + + final Parameters p; + static const double decay = -0.5; + final double factor; + + /// TODO: document + ReviewCard reviewCard( Card card, Rating rating, DateTime? now, @@ -32,9 +45,10 @@ class FSRS { final reviewCard = schedulingCards[rating]!.card; final reviewLog = schedulingCards[rating]!.reviewLog; - return (reviewCard, reviewLog); + return ReviewCard(card: reviewCard, reviewLog: reviewLog); } + /// TODO: document Map repeat( Card card, [ DateTime? now, @@ -97,6 +111,7 @@ class FSRS { return s.recordLog(card, date); } + /// TODO: document void _initDS(SchedulingCards s) { s.again.difficulty = _initDifficulty(Rating.again); s.again.stability = _initStability(Rating.again.val); @@ -108,30 +123,38 @@ class FSRS { s.easy.stability = _initStability(Rating.easy.val); } + /// TODO: document double _initStability(int r) => max(p.w[r - 1], 0.1); + /// TODO: document double _initDifficulty(Rating r) => min(max(p.w[4] - exp(p.w[5] * (r.val - 1) + 1), 1), 10); + /// TODO: document double _forgettingCurve(int elapsedDays, double stability) => pow(1 + factor * elapsedDays / stability, decay).toDouble(); + /// TODO: document int _nextInterval(double s) { final newInterval = s / factor * (pow(p.requestRetention, 1 / decay) - 1); return min(max(newInterval.round(), 1), p.maximumInterval); } + /// TODO: document double _nextDifficulty(double d, Rating r) { final nextD = d - p.w[6] * (r.val - 3); return min(max(_meanReversion(_initDifficulty(Rating.easy), nextD), 1), 10); } + /// TODO: document double _shortTermStability(double stability, Rating rating) => stability * exp(p.w[17] * (rating.val - 3 + p.w[18])); + /// TODO: document double _meanReversion(double init, double current) => p.w[7] * init + (1 - p.w[7]) * current; + /// TODO: document double _nextRecallStability(double d, double s, double r, Rating rating) { final hardPenalty = rating == Rating.hard ? p.w[15] : 1; final easyBonus = rating == Rating.easy ? p.w[16] : 1; @@ -145,6 +168,7 @@ class FSRS { easyBonus); } + /// TODO: document double _nextForgetStability(double d, double s, double r) { return p.w[11] * pow(d, -p.w[12]) * @@ -152,6 +176,7 @@ class FSRS { exp((1 - r) * p.w[14]); } + /// TODO: document void _nextDS( SchedulingCards s, double lastD, diff --git a/lib/src/models.dart b/lib/src/models.dart index fd36441..46993f6 100644 --- a/lib/src/models.dart +++ b/lib/src/models.dart @@ -6,10 +6,18 @@ import 'package:freezed_annotation/freezed_annotation.dart'; part 'models.freezed.dart'; part 'models.g.dart'; +/// TODO: document enum State { + /// TODO: document newState(0), + + /// TODO: document learning(1), + + /// TODO: document review(2), + + /// TODO: document relearning(3); const State(this.val); @@ -17,10 +25,18 @@ enum State { final int val; } +/// TODO: document enum Rating { + /// TODO: document again(1), + + /// TODO: document hard(2), + + /// TODO: document good(3), + + /// TODO: document easy(4); const Rating(this.val); @@ -28,13 +44,8 @@ enum Rating { final int val; } +/// TODO: document class ReviewLog { - Rating rating; - int scheduledDays; - int elapsedDays; - DateTime review; - State state; - ReviewLog( this.rating, this.scheduledDays, @@ -43,6 +54,21 @@ class ReviewLog { this.state, ); + /// TODO: document + Rating rating; + + /// TODO: document + int scheduledDays; + + /// TODO: document + int elapsedDays; + + /// TODO: document + DateTime review; + + /// TODO: document + State state; + @override String toString() { return jsonEncode({ @@ -93,6 +119,7 @@ class Card with _$Card { } } +/// TODO: document /// Store card and review log info class SchedulingInfo { late Card card; @@ -101,6 +128,7 @@ class SchedulingInfo { SchedulingInfo(this.card, this.reviewLog); } +/// TODO: document /// Calculate next review class SchedulingCards { late Card again; @@ -115,6 +143,7 @@ class SchedulingCards { easy = card.copyWith(); } + /// TODO: document void updateState(State state) { switch (state) { case State.newState: @@ -137,6 +166,7 @@ class SchedulingCards { } } + /// TODO: document void schedule( DateTime now, int hardInterval, @@ -155,6 +185,7 @@ class SchedulingCards { easy.due = now.add(Duration(days: easyInterval)); } + /// TODO: document Map recordLog(Card card, DateTime now) => { Rating.again: SchedulingInfo( again, @@ -199,6 +230,7 @@ class SchedulingCards { }; } +/// TODO: document class Parameters { Parameters({ double? requestRetention, @@ -229,7 +261,12 @@ class Parameters { 0.6567, ]; + /// TODO: document double requestRetention; + + /// TODO: document int maximumInterval; + + /// TODO: document List w; } diff --git a/lib/src/models.freezed.dart b/lib/src/models.freezed.dart index 64a529a..23f5bd6 100644 --- a/lib/src/models.freezed.dart +++ b/lib/src/models.freezed.dart @@ -100,8 +100,13 @@ mixin _$Card { required TResult orElse(), }) => throw _privateConstructorUsedError; + + /// Serializes this Card to a JSON map. Map toJson() => throw _privateConstructorUsedError; - @JsonKey(ignore: true) + + /// Create a copy of Card + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) $CardCopyWith get copyWith => throw _privateConstructorUsedError; } @@ -132,6 +137,8 @@ class _$CardCopyWithImpl<$Res, $Val extends Card> // ignore: unused_field final $Res Function($Val) _then; + /// Create a copy of Card + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -212,6 +219,8 @@ class __$$CardImplCopyWithImpl<$Res> __$$CardImplCopyWithImpl(_$CardImpl _value, $Res Function(_$CardImpl) _then) : super(_value, _then); + /// Create a copy of Card + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -313,7 +322,9 @@ class _$CardImpl extends _Card { return 'Card.def(due: $due, lastReview: $lastReview, stability: $stability, difficulty: $difficulty, elapsedDays: $elapsedDays, scheduledDays: $scheduledDays, reps: $reps, lapses: $lapses, state: $state)'; } - @JsonKey(ignore: true) + /// Create a copy of Card + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$CardImplCopyWith<_$CardImpl> get copyWith => @@ -456,8 +467,11 @@ abstract class _Card extends Card { @override State get state; set state(State value); + + /// Create a copy of Card + /// with the given fields replaced by the non-null parameter values. @override - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) _$$CardImplCopyWith<_$CardImpl> get copyWith => throw _privateConstructorUsedError; } diff --git a/test/fsrs_test.dart b/test/fsrs_test.dart index 1616b25..3e9ebf3 100644 --- a/test/fsrs_test.dart +++ b/test/fsrs_test.dart @@ -27,7 +27,7 @@ void main() { test('Review Card', () { final f = FSRS(w: testW); var card = Card(); - var now = DateTime(2022, 11, 29, 12, 30).toUtc(); + var now = DateTime.utc(2022, 11, 29, 12, 30); const ratings = [ Rating.good, @@ -48,7 +48,8 @@ void main() { List ivlHistory = []; for (final rating in ratings) { - (card, _) = f.reviewCard(card, rating, now); + final reviewCard = f.reviewCard(card, rating, now); + card = reviewCard.card; final ivl = card.scheduledDays; ivlHistory.add(ivl); now = card.due; @@ -74,7 +75,7 @@ void main() { test('Memo State', () { final f = FSRS(w: testW); var card = Card(); - var now = DateTime(2022, 11, 29, 12, 30).toUtc(); + var now = DateTime.utc(2022, 11, 29, 12, 30); var schedulingCards = f.repeat(card, now); const reviews = [ @@ -163,7 +164,8 @@ void main() { final List ivlHistory = []; for (final rating in ratings) { - (card, _) = f.reviewCard(card, rating, now); + final reviewCard = f.reviewCard(card, rating, now); + card = reviewCard.card; final ivl = card.scheduledDays; ivlHistory.add(ivl); now = card.due; @@ -204,11 +206,12 @@ void main() { ); var card = Card(); - var now = DateTime(2022, 11, 29, 12, 30); + var now = DateTime.utc(2022, 11, 29, 12, 30); final List ivlHistory = []; for (final rating in ratings) { - (card, _) = f.reviewCard(card, rating, now); + final reviewCard = f.reviewCard(card, rating, now); + card = reviewCard.card; final ivl = card.scheduledDays; ivlHistory.add(ivl); now = card.due; From 9668158bc2f665e52b5bd0490eb1c4374b66af5f Mon Sep 17 00:00:00 2001 From: Brian Wo <45139213+brainwo@users.noreply.github.com> Date: Sun, 1 Sep 2024 16:07:13 +0800 Subject: [PATCH 8/8] refactor: replace ReviewCard with tuple --- lib/src/fsrs_base.dart | 50 +++++++++++++++--------------------------- test/fsrs_test.dart | 10 ++++----- 2 files changed, 23 insertions(+), 37 deletions(-) diff --git a/lib/src/fsrs_base.dart b/lib/src/fsrs_base.dart index 0eacde0..ec851c9 100644 --- a/lib/src/fsrs_base.dart +++ b/lib/src/fsrs_base.dart @@ -2,30 +2,16 @@ import 'dart:core'; import 'dart:math'; import './models.dart'; -/// TODO: document -class ReviewCard { - const ReviewCard({ - required this.card, - required this.reviewLog, - }); - - /// TODO: document - final Card card; - - /// TODO: document - final ReviewLog reviewLog; -} - /// TODO: document class FSRS { FSRS({ double? requestRetention, int? maximumInterval, - List? w, + List? weight, }) : p = Parameters( requestRetention: requestRetention, maximumInterval: maximumInterval, - w: w, + weight: weight, ), factor = pow(0.9, 1 / decay) - 1; @@ -34,7 +20,7 @@ class FSRS { final double factor; /// TODO: document - ReviewCard reviewCard( + ({Card card, ReviewLog reviewLog}) reviewCard( Card card, Rating rating, DateTime? now, @@ -45,7 +31,7 @@ class FSRS { final reviewCard = schedulingCards[rating]!.card; final reviewLog = schedulingCards[rating]!.reviewLog; - return ReviewCard(card: reviewCard, reviewLog: reviewLog); + return (card: reviewCard, reviewLog: reviewLog); } /// TODO: document @@ -124,11 +110,11 @@ class FSRS { } /// TODO: document - double _initStability(int r) => max(p.w[r - 1], 0.1); + double _initStability(int r) => max(p.weight[r - 1], 0.1); /// TODO: document double _initDifficulty(Rating r) => - min(max(p.w[4] - exp(p.w[5] * (r.val - 1) + 1), 1), 10); + min(max(p.weight[4] - exp(p.weight[5] * (r.val - 1) + 1), 1), 10); /// TODO: document double _forgettingCurve(int elapsedDays, double stability) => @@ -142,38 +128,38 @@ class FSRS { /// TODO: document double _nextDifficulty(double d, Rating r) { - final nextD = d - p.w[6] * (r.val - 3); + final nextD = d - p.weight[6] * (r.val - 3); return min(max(_meanReversion(_initDifficulty(Rating.easy), nextD), 1), 10); } /// TODO: document double _shortTermStability(double stability, Rating rating) => - stability * exp(p.w[17] * (rating.val - 3 + p.w[18])); + stability * exp(p.weight[17] * (rating.val - 3 + p.weight[18])); /// TODO: document double _meanReversion(double init, double current) => - p.w[7] * init + (1 - p.w[7]) * current; + p.weight[7] * init + (1 - p.weight[7]) * current; /// TODO: document double _nextRecallStability(double d, double s, double r, Rating rating) { - final hardPenalty = rating == Rating.hard ? p.w[15] : 1; - final easyBonus = rating == Rating.easy ? p.w[16] : 1; + final hardPenalty = rating == Rating.hard ? p.weight[15] : 1; + final easyBonus = rating == Rating.easy ? p.weight[16] : 1; return s * (1 + - exp(p.w[8]) * + exp(p.weight[8]) * (11 - d) * - pow(s, -p.w[9]) * - (exp((1 - r) * p.w[10]) - 1) * + pow(s, -p.weight[9]) * + (exp((1 - r) * p.weight[10]) - 1) * hardPenalty * easyBonus); } /// TODO: document double _nextForgetStability(double d, double s, double r) { - return p.w[11] * - pow(d, -p.w[12]) * - (pow(s + 1, p.w[13]) - 1) * - exp((1 - r) * p.w[14]); + return p.weight[11] * + pow(d, -p.weight[12]) * + (pow(s + 1, p.weight[13]) - 1) * + exp((1 - r) * p.weight[14]); } /// TODO: document diff --git a/test/fsrs_test.dart b/test/fsrs_test.dart index 3e9ebf3..e96a22c 100644 --- a/test/fsrs_test.dart +++ b/test/fsrs_test.dart @@ -25,7 +25,7 @@ const testW = [ void main() { test('Review Card', () { - final f = FSRS(w: testW); + final f = FSRS(weight: testW); var card = Card(); var now = DateTime.utc(2022, 11, 29, 12, 30); @@ -73,7 +73,7 @@ void main() { }); test('Memo State', () { - final f = FSRS(w: testW); + final f = FSRS(weight: testW); var card = Card(); var now = DateTime.utc(2022, 11, 29, 12, 30); @@ -136,7 +136,7 @@ void main() { final f = FSRS( requestRetention: 0.9, maximumInterval: 36500, - w: [ + weight: [ 0.4197, 1.1869, 3.0412, @@ -202,7 +202,7 @@ void main() { final f = FSRS( requestRetention: requestRetention, maximumInterval: maximumInterval, - w: w, + weight: w, ); var card = Card(); @@ -217,7 +217,7 @@ void main() { now = card.due; } - expect(f.p.w, w); + expect(f.p.weight, w); expect(f.p.requestRetention, requestRetention); expect(f.p.maximumInterval, maximumInterval); });