ts-fsrs 5.2.0 → 5.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,2 +1,1913 @@
1
- var u=(a=>(a[a.New=0]="New",a[a.Learning=1]="Learning",a[a.Review=2]="Review",a[a.Relearning=3]="Relearning",a))(u||{}),l=(a=>(a[a.Manual=0]="Manual",a[a.Again=1]="Again",a[a.Hard=2]="Hard",a[a.Good=3]="Good",a[a.Easy=4]="Easy",a))(l||{});class d{static card(t){return{...t,state:d.state(t.state),due:d.time(t.due),last_review:t.last_review?d.time(t.last_review):void 0}}static rating(t){if(typeof t=="string"){const e=t.charAt(0).toUpperCase(),i=t.slice(1).toLowerCase(),s=l[`${e}${i}`];if(s===void 0)throw new Error(`Invalid rating:[${t}]`);return s}else if(typeof t=="number")return t;throw new Error(`Invalid rating:[${t}]`)}static state(t){if(typeof t=="string"){const e=t.charAt(0).toUpperCase(),i=t.slice(1).toLowerCase(),s=u[`${e}${i}`];if(s===void 0)throw new Error(`Invalid state:[${t}]`);return s}else if(typeof t=="number")return t;throw new Error(`Invalid state:[${t}]`)}static time(t){if(typeof t=="object"&&t instanceof Date)return t;if(typeof t=="string"){const e=Date.parse(t);if(isNaN(e))throw new Error(`Invalid date:[${t}]`);return new Date(e)}else if(typeof t=="number")return new Date(t);throw new Error(`Invalid date:[${t}]`)}static review_log(t){return{...t,due:d.time(t.due),rating:d.rating(t.rating),state:d.state(t.state),review:d.time(t.review)}}}Date.prototype.scheduler=function(a,t){return m(this,a,t)},Date.prototype.diff=function(a,t){return v(this,a,t)},Date.prototype.format=function(){return C(this)},Date.prototype.dueFormat=function(a,t,e){return z(this,a,t,e)};function m(a,t,e){return new Date(e?d.time(a).getTime()+t*24*60*60*1e3:d.time(a).getTime()+t*60*1e3)}function v(a,t,e){if(!a||!t)throw new Error("Invalid date");const i=d.time(a).getTime()-d.time(t).getTime();let s=0;switch(e){case"days":s=Math.floor(i/(24*60*60*1e3));break;case"minutes":s=Math.floor(i/(60*1e3));break}return s}function C(a){const t=d.time(a),e=t.getFullYear(),i=t.getMonth()+1,s=t.getDate(),r=t.getHours(),n=t.getMinutes(),o=t.getSeconds();return`${e}-${b(i)}-${b(s)} ${b(r)}:${b(n)}:${b(o)}`}function b(a){return a<10?`0${a}`:`${a}`}const L=[60,60,24,31,12],N=["second","min","hour","day","month","year"];function z(a,t,e,i=N){a=d.time(a),t=d.time(t),i.length!==N.length&&(i=N);let s=a.getTime()-t.getTime(),r;for(s/=1e3,r=0;r<L.length&&!(s<L[r]);r++)s/=L[r];return`${Math.floor(s)}${e?i[r]:""}`}function st(a){return d.time(a)}function rt(a){return d.state(a)}function at(a){return d.rating(a)}const T=Object.freeze([l.Again,l.Hard,l.Good,l.Easy]),nt=[{start:2.5,end:7,factor:.15},{start:7,end:20,factor:.1},{start:20,end:1/0,factor:.05}];function U(a,t,e){let i=1;for(const n of nt)i+=n.factor*Math.max(Math.min(a,n.end)-n.start,0);a=Math.min(a,e);let s=Math.max(2,Math.round(a-i));const r=Math.min(Math.round(a+i),e);return a>t&&(s=Math.max(s,t+1)),s=Math.min(s,r),{min_ivl:s,max_ivl:r}}function g(a,t,e){return Math.min(Math.max(a,t),e)}function k(a,t){const e=Date.UTC(a.getUTCFullYear(),a.getUTCMonth(),a.getUTCDate()),i=Date.UTC(t.getUTCFullYear(),t.getUTCMonth(),t.getUTCDate());return Math.floor((i-e)/864e5)}const lt="5.2.0",q=.9,P=36500,j=!1,O=!0,Y=Object.freeze(["1m","10m"]),W=Object.freeze(["10m"]),ot=`v${lt} using FSRS-6.0`,p=.001,dt=36500,M=100,$=.5,B=.1542,D=Object.freeze([.212,1.2931,2.3065,8.2956,6.4133,.8334,3.0194,.001,1.8722,.1666,.796,1.4835,.0614,.2629,1.6483,.6014,1.8729,.5425,.0912,.0658,B]),X=2,V=a=>[[p,M],[p,M],[p,M],[p,M],[1,10],[.001,4],[.001,4],[.001,.75],[0,4.5],[0,.8],[.001,3.5],[.001,5],[.001,.25],[.001,.9],[0,4],[0,1],[1,6],[0,a],[0,a],[0,.8],[.1,.8]],E=(a,t)=>{let e=X;if(Math.max(0,t)>1){const i=-(Math.log(a[11])+Math.log(Math.pow(2,a[13])-1)+a[14]*.3)/t;e=g(+i.toFixed(8),.01,2)}return V(e).map(([i,s],r)=>g(a[r],i,s))},ht=a=>{if(a.find(t=>!isFinite(t)&&!isNaN(t))!==void 0)throw Error(`Non-finite or NaN value in parameters ${a}`);if(![17,19,21].includes(a.length))throw Error(`Invalid parameter length: ${a.length}. Must be 17, 19 or 21 for FSRSv4, 5 and 6 respectively.`);return a},R=a=>{if(a===void 0)return[...D];switch(a.length){case 21:return[...a];case 19:return console.debug("[FSRS-6]auto fill w from 19 to 21 length"),[...a,0,$];case 17:{const t=[...a];return t[4]=+(t[5]*2+t[4]).toFixed(8),t[5]=+(Math.log(t[5]*3+1)/3).toFixed(8),t[6]=+(t[6]+.5).toFixed(8),console.debug("[FSRS-6]auto fill w from 17 to 21 length"),t.concat([0,0,0,$])}default:return console.warn("[FSRS]Invalid parameters length, using default parameters"),[...D]}},I=a=>{const t=Array.isArray(a?.learning_steps)?a.learning_steps:Y,e=Array.isArray(a?.relearning_steps)?a.relearning_steps:W,i=E(R(a?.w),e.length);return{request_retention:a?.request_retention||q,maximum_interval:a?.maximum_interval||P,w:i,enable_fuzz:a?.enable_fuzz??j,enable_short_term:a?.enable_short_term??O,learning_steps:t,relearning_steps:e}};function F(a,t){const e={due:a?d.time(a):new Date,stability:0,difficulty:0,elapsed_days:0,scheduled_days:0,reps:0,lapses:0,learning_steps:0,state:u.New,last_review:void 0};return t&&typeof t=="function"?t(e):e}class ut{c;s0;s1;s2;constructor(t){const e=ct();this.c=1,this.s0=e(" "),this.s1=e(" "),this.s2=e(" "),t==null&&(t=+new Date),this.s0-=e(t),this.s0<0&&(this.s0+=1),this.s1-=e(t),this.s1<0&&(this.s1+=1),this.s2-=e(t),this.s2<0&&(this.s2+=1)}next(){const t=2091639*this.s0+this.c*23283064365386963e-26;return this.s0=this.s1,this.s1=this.s2,this.s2=t-(this.c=t|0),this.s2}set state(t){this.c=t.c,this.s0=t.s0,this.s1=t.s1,this.s2=t.s2}get state(){return{c:this.c,s0:this.s0,s1:this.s1,s2:this.s2}}}function ct(){let a=4022871197;return function(t){t=String(t);for(let e=0;e<t.length;e++){a+=t.charCodeAt(e);let i=.02519603282416938*a;a=i>>>0,i-=a,i*=a,a=i>>>0,i-=a,a+=i*4294967296}return(a>>>0)*23283064365386963e-26}}function _t(a){const t=new ut(a),e=()=>t.next();return e.int32=()=>t.next()*4294967296|0,e.double=()=>e()+(e()*2097152|0)*11102230246251565e-32,e.state=()=>t.state,e.importState=i=>(t.state=i,e),e}const H=a=>{const t=typeof a=="number"?-a:-a[20],e=Math.exp(Math.pow(t,-1)*Math.log(.9))-1;return{decay:t,factor:+e.toFixed(8)}};function A(a,t,e){const{decay:i,factor:s}=H(a);return+Math.pow(1+s*t/e,i).toFixed(8)}class J{param;intervalModifier;_seed;constructor(t){this.param=new Proxy(I(t),this.params_handler_proxy()),this.intervalModifier=this.calculate_interval_modifier(this.param.request_retention),this.forgetting_curve=A.bind(this,this.param.w)}get interval_modifier(){return this.intervalModifier}set seed(t){this._seed=t}calculate_interval_modifier(t){if(t<=0||t>1)throw new Error("Requested retention rate should be in the range (0,1]");const{decay:e,factor:i}=H(this.param.w);return+((Math.pow(t,1/e)-1)/i).toFixed(8)}get parameters(){return this.param}set parameters(t){this.update_parameters(t)}params_handler_proxy(){const t=this;return{set:function(e,i,s){return i==="request_retention"&&Number.isFinite(s)?t.intervalModifier=t.calculate_interval_modifier(Number(s)):i==="w"&&(s=E(R(s),e.relearning_steps.length),t.forgetting_curve=A.bind(this,s),t.intervalModifier=t.calculate_interval_modifier(Number(e.request_retention))),Reflect.set(e,i,s),!0}}}update_parameters(t){const e=I(t);for(const i in e)if(i in this.param){const s=i;this.param[s]=e[s]}}init_stability(t){return Math.max(this.param.w[t-1],.1)}init_difficulty(t){return+(this.param.w[4]-Math.exp((t-1)*this.param.w[5])+1).toFixed(8)}apply_fuzz(t,e){if(!this.param.enable_fuzz||t<2.5)return Math.round(t);const i=_t(this._seed)(),{min_ivl:s,max_ivl:r}=U(t,e,this.param.maximum_interval);return Math.floor(i*(r-s+1)+s)}next_interval(t,e){const i=Math.min(Math.max(1,Math.round(t*this.intervalModifier)),this.param.maximum_interval);return this.apply_fuzz(i,e)}linear_damping(t,e){return+(t*(10-e)/9).toFixed(8)}next_difficulty(t,e){const i=-this.param.w[6]*(e-3),s=t+this.linear_damping(i,t);return g(this.mean_reversion(this.init_difficulty(l.Easy),s),1,10)}mean_reversion(t,e){return+(this.param.w[7]*t+(1-this.param.w[7])*e).toFixed(8)}next_recall_stability(t,e,i,s){const r=l.Hard===s?this.param.w[15]:1,n=l.Easy===s?this.param.w[16]:1;return+g(e*(1+Math.exp(this.param.w[8])*(11-t)*Math.pow(e,-this.param.w[9])*(Math.exp((1-i)*this.param.w[10])-1)*r*n),p,36500).toFixed(8)}next_forget_stability(t,e,i){return+g(this.param.w[11]*Math.pow(t,-this.param.w[12])*(Math.pow(e+1,this.param.w[13])-1)*Math.exp((1-i)*this.param.w[14]),p,36500).toFixed(8)}next_short_term_stability(t,e){const i=Math.pow(t,-this.param.w[19])*Math.exp(this.param.w[17]*(e-3+this.param.w[18])),s=e>=3?Math.max(i,1):i;return+g(t*s,p,36500).toFixed(8)}forgetting_curve;next_state(t,e,i){const{difficulty:s,stability:r}=t??{difficulty:0,stability:0};if(e<0)throw new Error(`Invalid delta_t "${e}"`);if(i<0||i>4)throw new Error(`Invalid grade "${i}"`);if(s===0&&r===0)return{difficulty:g(this.init_difficulty(i),1,10),stability:this.init_stability(i)};if(i===0)return{difficulty:s,stability:r};if(s<1||r<p)throw new Error(`Invalid memory state { difficulty: ${s}, stability: ${r} }`);const n=this.forgetting_curve(e,r),o=this.next_recall_stability(s,r,n,i),h=this.next_forget_stability(s,r,n),c=this.next_short_term_stability(r,i);let _=o;if(i===1){let[y,w]=[0,0];this.param.enable_short_term&&(y=this.param.w[17],w=this.param.w[18]);const f=r/Math.exp(y*w);_=g(+f.toFixed(8),p,h)}return e===0&&this.param.enable_short_term&&(_=c),{difficulty:this.next_difficulty(s,i),stability:_}}}function K(){const a=this.review_time.getTime(),t=this.current.reps,e=this.current.difficulty*this.current.stability;return`${a}_${t}_${e}`}function ft(a){return function(){const t=Reflect.get(this.current,a)??0,e=this.current.reps;return String(t+e||0)}}const Q=a=>{const t=a.slice(-1),e=parseInt(a.slice(0,-1),10);if(isNaN(e)||!Number.isFinite(e)||e<0)throw new Error(`Invalid step value: ${a}`);switch(t){case"m":return e;case"h":return e*60;case"d":return e*1440;default:throw new Error(`Invalid step unit: ${a}, expected m/h/d`)}},Z=(a,t,e)=>{const i=t===u.Relearning||t===u.Review?a.relearning_steps:a.learning_steps,s=i.length;if(s===0||e>=s)return{};const r=i[0],n=Q,o=()=>n(r),h=()=>{if(s===1)return Math.round(n(r)*1.5);const f=i[1];return Math.round((n(r)+n(f))/2)},c=f=>f<0||f>=s?null:i[f],_=f=>n(f),y={},w=c(Math.max(0,e));if(t===u.Review)return y[l.Again]={scheduled_minutes:n(w),next_step:0},y;{y[l.Again]={scheduled_minutes:o(),next_step:0},y[l.Hard]={scheduled_minutes:h(),next_step:e};const f=c(e+1);if(f){const x=_(f);x&&(y[l.Good]={scheduled_minutes:Math.round(x),next_step:e+1})}}return y};var S=(a=>(a.SCHEDULER="Scheduler",a.LEARNING_STEPS="LearningSteps",a.SEED="Seed",a))(S||{});class G{last;current;review_time;next=new Map;algorithm;strategies;elapsed_days=0;constructor(t,e,i,s){this.algorithm=i,this.last=d.card(t),this.current=d.card(t),this.review_time=d.time(e),this.strategies=s,this.init()}checkGrade(t){if(!Number.isFinite(t)||t<0||t>4)throw new Error(`Invalid grade "${t}",expected 1-4`)}init(){const{state:t,last_review:e}=this.current;let i=0;t!==u.New&&e&&(i=k(e,this.review_time)),this.current.last_review=this.review_time,this.elapsed_days=i,this.current.elapsed_days=i,this.current.reps+=1;let s=K;if(this.strategies){const r=this.strategies.get(S.SEED);r&&(s=r)}this.algorithm.seed=s.call(this)}preview(){return{[l.Again]:this.review(l.Again),[l.Hard]:this.review(l.Hard),[l.Good]:this.review(l.Good),[l.Easy]:this.review(l.Easy),[Symbol.iterator]:this.previewIterator.bind(this)}}*previewIterator(){for(const t of T)yield this.review(t)}review(t){const{state:e}=this.last;let i;switch(this.checkGrade(t),e){case u.New:i=this.newState(t);break;case u.Learning:case u.Relearning:i=this.learningState(t);break;case u.Review:i=this.reviewState(t);break}return i}buildLog(t){const{last_review:e,due:i,elapsed_days:s}=this.last;return{rating:t,state:this.current.state,due:e||i,stability:this.current.stability,difficulty:this.current.difficulty,elapsed_days:this.elapsed_days,last_elapsed_days:s,scheduled_days:this.current.scheduled_days,learning_steps:this.current.learning_steps,review:this.review_time}}}class tt extends G{learningStepsStrategy;constructor(t,e,i,s){super(t,e,i,s);let r=Z;if(this.strategies){const n=this.strategies.get(S.LEARNING_STEPS);n&&(r=n)}this.learningStepsStrategy=r}getLearningInfo(t,e){const i=this.algorithm.parameters;t.learning_steps=t.learning_steps||0;const s=this.learningStepsStrategy(i,t.state,this.current.state===u.Learning?t.learning_steps+1:t.learning_steps),r=Math.max(0,s[e]?.scheduled_minutes??0),n=Math.max(0,s[e]?.next_step??0);return{scheduled_minutes:r,next_steps:n}}applyLearningSteps(t,e,i){const{scheduled_minutes:s,next_steps:r}=this.getLearningInfo(this.current,e);if(s>0&&s<1440)t.learning_steps=r,t.scheduled_days=0,t.state=i,t.due=m(this.review_time,Math.round(s),!1);else if(t.state=u.Review,s>=1440)t.learning_steps=r,t.due=m(this.review_time,Math.round(s),!1),t.scheduled_days=Math.floor(s/1440);else{t.learning_steps=0;const n=this.algorithm.next_interval(t.stability,this.elapsed_days);t.scheduled_days=n,t.due=m(this.review_time,n,!0)}}newState(t){const e=this.next.get(t);if(e)return e;const i=d.card(this.current);i.difficulty=g(this.algorithm.init_difficulty(t),1,10),i.stability=this.algorithm.init_stability(t),this.applyLearningSteps(i,t,u.Learning);const s={card:i,log:this.buildLog(t)};return this.next.set(t,s),s}learningState(t){const e=this.next.get(t);if(e)return e;const{state:i,difficulty:s,stability:r}=this.last,n=d.card(this.current);n.difficulty=this.algorithm.next_difficulty(s,t),n.stability=this.algorithm.next_short_term_stability(r,t),this.applyLearningSteps(n,t,i);const o={card:n,log:this.buildLog(t)};return this.next.set(t,o),o}reviewState(t){const e=this.next.get(t);if(e)return e;const i=this.elapsed_days,{difficulty:s,stability:r}=this.last,n=this.algorithm.forgetting_curve(i,r),o=d.card(this.current),h=d.card(this.current),c=d.card(this.current),_=d.card(this.current);this.next_ds(o,h,c,_,s,r,n),this.next_interval(h,c,_,i),this.next_state(h,c,_),this.applyLearningSteps(o,l.Again,u.Relearning),o.lapses+=1;const y={card:o,log:this.buildLog(l.Again)},w={card:h,log:super.buildLog(l.Hard)},f={card:c,log:super.buildLog(l.Good)},x={card:_,log:super.buildLog(l.Easy)};return this.next.set(l.Again,y),this.next.set(l.Hard,w),this.next.set(l.Good,f),this.next.set(l.Easy,x),this.next.get(t)}next_ds(t,e,i,s,r,n,o){t.difficulty=this.algorithm.next_difficulty(r,l.Again);const h=n/Math.exp(this.algorithm.parameters.w[17]*this.algorithm.parameters.w[18]),c=this.algorithm.next_forget_stability(r,n,o);t.stability=g(+h.toFixed(8),p,c),e.difficulty=this.algorithm.next_difficulty(r,l.Hard),e.stability=this.algorithm.next_recall_stability(r,n,o,l.Hard),i.difficulty=this.algorithm.next_difficulty(r,l.Good),i.stability=this.algorithm.next_recall_stability(r,n,o,l.Good),s.difficulty=this.algorithm.next_difficulty(r,l.Easy),s.stability=this.algorithm.next_recall_stability(r,n,o,l.Easy)}next_interval(t,e,i,s){let r,n;r=this.algorithm.next_interval(t.stability,s),n=this.algorithm.next_interval(e.stability,s),r=Math.min(r,n),n=Math.max(n,r+1);const o=Math.max(this.algorithm.next_interval(i.stability,s),n+1);t.scheduled_days=r,t.due=m(this.review_time,r,!0),e.scheduled_days=n,e.due=m(this.review_time,n,!0),i.scheduled_days=o,i.due=m(this.review_time,o,!0)}next_state(t,e,i){t.state=u.Review,t.learning_steps=0,e.state=u.Review,e.learning_steps=0,i.state=u.Review,i.learning_steps=0}}class et extends G{newState(t){const e=this.next.get(t);if(e)return e;this.current.scheduled_days=0,this.current.elapsed_days=0;const i=d.card(this.current),s=d.card(this.current),r=d.card(this.current),n=d.card(this.current);return this.init_ds(i,s,r,n),this.next_interval(i,s,r,n,0),this.next_state(i,s,r,n),this.update_next(i,s,r,n),this.next.get(t)}init_ds(t,e,i,s){t.difficulty=g(this.algorithm.init_difficulty(l.Again),1,10),t.stability=this.algorithm.init_stability(l.Again),e.difficulty=g(this.algorithm.init_difficulty(l.Hard),1,10),e.stability=this.algorithm.init_stability(l.Hard),i.difficulty=g(this.algorithm.init_difficulty(l.Good),1,10),i.stability=this.algorithm.init_stability(l.Good),s.difficulty=g(this.algorithm.init_difficulty(l.Easy),1,10),s.stability=this.algorithm.init_stability(l.Easy)}learningState(t){return this.reviewState(t)}reviewState(t){const e=this.next.get(t);if(e)return e;const i=this.elapsed_days,{difficulty:s,stability:r}=this.last,n=this.algorithm.forgetting_curve(i,r),o=d.card(this.current),h=d.card(this.current),c=d.card(this.current),_=d.card(this.current);return this.next_ds(o,h,c,_,s,r,n),this.next_interval(o,h,c,_,i),this.next_state(o,h,c,_),o.lapses+=1,this.update_next(o,h,c,_),this.next.get(t)}next_ds(t,e,i,s,r,n,o){t.difficulty=this.algorithm.next_difficulty(r,l.Again);const h=this.algorithm.next_forget_stability(r,n,o);t.stability=g(n,p,h),e.difficulty=this.algorithm.next_difficulty(r,l.Hard),e.stability=this.algorithm.next_recall_stability(r,n,o,l.Hard),i.difficulty=this.algorithm.next_difficulty(r,l.Good),i.stability=this.algorithm.next_recall_stability(r,n,o,l.Good),s.difficulty=this.algorithm.next_difficulty(r,l.Easy),s.stability=this.algorithm.next_recall_stability(r,n,o,l.Easy)}next_interval(t,e,i,s,r){let n,o,h,c;n=this.algorithm.next_interval(t.stability,r),o=this.algorithm.next_interval(e.stability,r),h=this.algorithm.next_interval(i.stability,r),c=this.algorithm.next_interval(s.stability,r),n=Math.min(n,o),o=Math.max(o,n+1),h=Math.max(h,o+1),c=Math.max(c,h+1),t.scheduled_days=n,t.due=m(this.review_time,n,!0),e.scheduled_days=o,e.due=m(this.review_time,o,!0),i.scheduled_days=h,i.due=m(this.review_time,h,!0),s.scheduled_days=c,s.due=m(this.review_time,c,!0)}next_state(t,e,i,s){t.state=u.Review,t.learning_steps=0,e.state=u.Review,e.learning_steps=0,i.state=u.Review,i.learning_steps=0,s.state=u.Review,s.learning_steps=0}update_next(t,e,i,s){const r={card:t,log:this.buildLog(l.Again)},n={card:e,log:super.buildLog(l.Hard)},o={card:i,log:super.buildLog(l.Good)},h={card:s,log:super.buildLog(l.Easy)};this.next.set(l.Again,r),this.next.set(l.Hard,n),this.next.set(l.Good,o),this.next.set(l.Easy,h)}}class gt{fsrs;constructor(t){this.fsrs=t}replay(t,e,i){return this.fsrs.next(t,e,i)}handleManualRating(t,e,i,s,r,n,o){if(typeof e>"u")throw new Error("reschedule: state is required for manual rating");let h,c;if(e===u.New)h={rating:l.Manual,state:e,due:o??i,stability:t.stability,difficulty:t.difficulty,elapsed_days:s,last_elapsed_days:t.elapsed_days,scheduled_days:t.scheduled_days,learning_steps:t.learning_steps,review:i},c=F(i),c.last_review=i;else{if(typeof o>"u")throw new Error("reschedule: due is required for manual rating");const _=v(o,i,"days");h={rating:l.Manual,state:t.state,due:t.last_review||t.due,stability:t.stability,difficulty:t.difficulty,elapsed_days:s,last_elapsed_days:t.elapsed_days,scheduled_days:t.scheduled_days,learning_steps:t.learning_steps,review:i},c={...t,state:e,due:o,last_review:i,stability:r||t.stability,difficulty:n||t.difficulty,elapsed_days:s,scheduled_days:_,reps:t.reps+1}}return{card:c,log:h}}reschedule(t,e){const i=[];let s=F(t.due);for(const r of e){let n;if(r.review=d.time(r.review),r.rating===l.Manual){let o=0;s.state!==u.New&&s.last_review&&(o=v(r.review,s.last_review,"days")),n=this.handleManualRating(s,r.state,r.review,o,r.stability,r.difficulty,r.due?d.time(r.due):void 0)}else n=this.replay(s,r.review,r.rating);i.push(n),s=n.card}return i}calculateManualRecord(t,e,i,s){if(!i)return null;const{card:r,log:n}=i,o=d.card(t);return o.due.getTime()===r.due.getTime()?null:(o.scheduled_days=v(r.due,o.due,"days"),this.handleManualRating(o,r.state,d.time(e),n.elapsed_days,s?r.stability:void 0,s?r.difficulty:void 0,r.due))}}class it extends J{strategyHandler=new Map;Scheduler;constructor(t){super(t);const{enable_short_term:e}=this.parameters;this.Scheduler=e?tt:et}params_handler_proxy(){const t=this;return{set:function(e,i,s){return i==="request_retention"&&Number.isFinite(s)?t.intervalModifier=t.calculate_interval_modifier(Number(s)):i==="enable_short_term"?t.Scheduler=s===!0?tt:et:i==="w"&&(s=E(R(s),e.relearning_steps.length),t.forgetting_curve=A.bind(this,s),t.intervalModifier=t.calculate_interval_modifier(Number(e.request_retention))),Reflect.set(e,i,s),!0}}}useStrategy(t,e){return this.strategyHandler.set(t,e),this}clearStrategy(t){return t?this.strategyHandler.delete(t):this.strategyHandler.clear(),this}getScheduler(t,e){const i=this.strategyHandler.get(S.SCHEDULER)||this.Scheduler;return new i(t,e,this,this.strategyHandler)}repeat(t,e,i){const s=this.getScheduler(t,e).preview();return i&&typeof i=="function"?i(s):s}next(t,e,i,s){const r=this.getScheduler(t,e),n=d.rating(i);if(n===l.Manual)throw new Error("Cannot review a manual rating");const o=r.review(n);return s&&typeof s=="function"?s(o):o}get_retrievability(t,e,i=!0){const s=d.card(t);e=e?d.time(e):new Date;const r=s.state!==u.New?Math.max(v(e,s.last_review,"days"),0):0,n=s.state!==u.New?this.forgetting_curve(r,+s.stability.toFixed(8)):0;return i?`${(n*100).toFixed(2)}%`:n}rollback(t,e,i){const s=d.card(t),r=d.review_log(e);if(r.rating===l.Manual)throw new Error("Cannot rollback a manual rating");let n,o,h;switch(r.state){case u.New:n=r.due,o=void 0,h=0;break;case u.Learning:case u.Relearning:case u.Review:n=r.review,o=r.due,h=s.lapses-(r.rating===l.Again&&r.state===u.Review?1:0);break}const c={...s,due:n,stability:r.stability,difficulty:r.difficulty,elapsed_days:r.last_elapsed_days,scheduled_days:r.scheduled_days,reps:Math.max(0,s.reps-1),lapses:Math.max(0,h),learning_steps:r.learning_steps,state:r.state,last_review:o};return i&&typeof i=="function"?i(c):c}forget(t,e,i=!1,s){const r=d.card(t);e=d.time(e);const n=r.state===u.New?0:v(e,r.due,"days"),o={rating:l.Manual,state:r.state,due:r.due,stability:r.stability,difficulty:r.difficulty,elapsed_days:0,last_elapsed_days:r.elapsed_days,scheduled_days:n,learning_steps:r.learning_steps,review:e},h={card:{...r,due:e,stability:0,difficulty:0,elapsed_days:0,scheduled_days:0,reps:i?0:r.reps,lapses:i?0:r.lapses,learning_steps:0,state:u.New,last_review:r.last_review},log:o};return s&&typeof s=="function"?s(h):h}reschedule(t,e=[],i={}){const{recordLogHandler:s,reviewsOrderBy:r,skipManual:n=!0,now:o=new Date,update_memory_state:h=!1}=i;r&&typeof r=="function"&&e.sort(r),n&&(e=e.filter(x=>x.rating!==l.Manual));const c=new gt(this),_=c.reschedule(i.first_card||F(),e),y=_.length,w=d.card(t),f=c.calculateManualRecord(w,o,y?_[y-1]:void 0,h);return s&&typeof s=="function"?{collections:_.map(s),reschedule_item:f?s(f):null}:{collections:_,reschedule_item:f}}}const yt=a=>new it(a||{});export{G as AbstractScheduler,Z as BasicLearningStepsStrategy,V as CLAMP_PARAMETERS,Q as ConvertStepUnitToMinutes,K as DefaultInitSeedStrategy,it as FSRS,$ as FSRS5_DEFAULT_DECAY,B as FSRS6_DEFAULT_DECAY,J as FSRSAlgorithm,ot as FSRSVersion,ft as GenSeedStrategyWithCardId,T as Grades,M as INIT_S_MAX,l as Rating,dt as S_MAX,p as S_MIN,u as State,S as StrategyMode,d as TypeConvert,X as W17_W18_Ceiling,ht as checkParameters,g as clamp,E as clipParameters,H as computeDecayFactor,F as createEmptyCard,k as dateDiffInDays,v as date_diff,m as date_scheduler,j as default_enable_fuzz,O as default_enable_short_term,Y as default_learning_steps,P as default_maximum_interval,W as default_relearning_steps,q as default_request_retention,D as default_w,st as fixDate,at as fixRating,rt as fixState,A as forgetting_curve,C as formatDate,yt as fsrs,I as generatorParameters,U as get_fuzz_range,R as migrateParameters,z as show_diff_message};
1
+ var State = /* @__PURE__ */ ((State2) => {
2
+ State2[State2["New"] = 0] = "New";
3
+ State2[State2["Learning"] = 1] = "Learning";
4
+ State2[State2["Review"] = 2] = "Review";
5
+ State2[State2["Relearning"] = 3] = "Relearning";
6
+ return State2;
7
+ })(State || {});
8
+ var Rating = /* @__PURE__ */ ((Rating2) => {
9
+ Rating2[Rating2["Manual"] = 0] = "Manual";
10
+ Rating2[Rating2["Again"] = 1] = "Again";
11
+ Rating2[Rating2["Hard"] = 2] = "Hard";
12
+ Rating2[Rating2["Good"] = 3] = "Good";
13
+ Rating2[Rating2["Easy"] = 4] = "Easy";
14
+ return Rating2;
15
+ })(Rating || {});
16
+
17
+ class TypeConvert {
18
+ static card(card) {
19
+ return {
20
+ ...card,
21
+ state: TypeConvert.state(card.state),
22
+ due: TypeConvert.time(card.due),
23
+ last_review: card.last_review ? TypeConvert.time(card.last_review) : void 0
24
+ };
25
+ }
26
+ static rating(value) {
27
+ if (typeof value === "string") {
28
+ const firstLetter = value.charAt(0).toUpperCase();
29
+ const restOfString = value.slice(1).toLowerCase();
30
+ const ret = Rating[`${firstLetter}${restOfString}`];
31
+ if (ret === void 0) {
32
+ throw new Error(`Invalid rating:[${value}]`);
33
+ }
34
+ return ret;
35
+ } else if (typeof value === "number") {
36
+ return value;
37
+ }
38
+ throw new Error(`Invalid rating:[${value}]`);
39
+ }
40
+ static state(value) {
41
+ if (typeof value === "string") {
42
+ const firstLetter = value.charAt(0).toUpperCase();
43
+ const restOfString = value.slice(1).toLowerCase();
44
+ const ret = State[`${firstLetter}${restOfString}`];
45
+ if (ret === void 0) {
46
+ throw new Error(`Invalid state:[${value}]`);
47
+ }
48
+ return ret;
49
+ } else if (typeof value === "number") {
50
+ return value;
51
+ }
52
+ throw new Error(`Invalid state:[${value}]`);
53
+ }
54
+ static time(value) {
55
+ if (typeof value === "object" && value instanceof Date) {
56
+ return value;
57
+ } else if (typeof value === "string") {
58
+ const timestamp = Date.parse(value);
59
+ if (!Number.isNaN(timestamp)) {
60
+ return new Date(timestamp);
61
+ } else {
62
+ throw new Error(`Invalid date:[${value}]`);
63
+ }
64
+ } else if (typeof value === "number") {
65
+ return new Date(value);
66
+ }
67
+ throw new Error(`Invalid date:[${value}]`);
68
+ }
69
+ static review_log(log) {
70
+ return {
71
+ ...log,
72
+ due: TypeConvert.time(log.due),
73
+ rating: TypeConvert.rating(log.rating),
74
+ state: TypeConvert.state(log.state),
75
+ review: TypeConvert.time(log.review)
76
+ };
77
+ }
78
+ }
79
+
80
+ Date.prototype.scheduler = function(t, isDay) {
81
+ return date_scheduler(this, t, isDay);
82
+ };
83
+ Date.prototype.diff = function(pre, unit) {
84
+ return date_diff(this, pre, unit);
85
+ };
86
+ Date.prototype.format = function() {
87
+ return formatDate(this);
88
+ };
89
+ Date.prototype.dueFormat = function(last_review, unit, timeUnit) {
90
+ return show_diff_message(this, last_review, unit, timeUnit);
91
+ };
92
+ function date_scheduler(now, t, isDay) {
93
+ return new Date(
94
+ isDay ? TypeConvert.time(now).getTime() + t * 24 * 60 * 60 * 1e3 : TypeConvert.time(now).getTime() + t * 60 * 1e3
95
+ );
96
+ }
97
+ function date_diff(now, pre, unit) {
98
+ if (!now || !pre) {
99
+ throw new Error("Invalid date");
100
+ }
101
+ const diff = TypeConvert.time(now).getTime() - TypeConvert.time(pre).getTime();
102
+ let r = 0;
103
+ switch (unit) {
104
+ case "days":
105
+ r = Math.floor(diff / (24 * 60 * 60 * 1e3));
106
+ break;
107
+ case "minutes":
108
+ r = Math.floor(diff / (60 * 1e3));
109
+ break;
110
+ }
111
+ return r;
112
+ }
113
+ function formatDate(dateInput) {
114
+ const date = TypeConvert.time(dateInput);
115
+ const year = date.getFullYear();
116
+ const month = date.getMonth() + 1;
117
+ const day = date.getDate();
118
+ const hours = date.getHours();
119
+ const minutes = date.getMinutes();
120
+ const seconds = date.getSeconds();
121
+ return `${year}-${padZero(month)}-${padZero(day)} ${padZero(hours)}:${padZero(
122
+ minutes
123
+ )}:${padZero(seconds)}`;
124
+ }
125
+ function padZero(num) {
126
+ return num < 10 ? `0${num}` : `${num}`;
127
+ }
128
+ const TIMEUNIT = [60, 60, 24, 31, 12];
129
+ const TIMEUNITFORMAT = ["second", "min", "hour", "day", "month", "year"];
130
+ function show_diff_message(due, last_review, unit, timeUnit = TIMEUNITFORMAT) {
131
+ due = TypeConvert.time(due);
132
+ last_review = TypeConvert.time(last_review);
133
+ if (timeUnit.length !== TIMEUNITFORMAT.length) {
134
+ timeUnit = TIMEUNITFORMAT;
135
+ }
136
+ let diff = due.getTime() - last_review.getTime();
137
+ let i = 0;
138
+ diff /= 1e3;
139
+ for (i = 0; i < TIMEUNIT.length; i++) {
140
+ if (diff < TIMEUNIT[i]) {
141
+ break;
142
+ } else {
143
+ diff /= TIMEUNIT[i];
144
+ }
145
+ }
146
+ return `${Math.floor(diff)}${unit ? timeUnit[i] : ""}`;
147
+ }
148
+ function fixDate(value) {
149
+ return TypeConvert.time(value);
150
+ }
151
+ function fixState(value) {
152
+ return TypeConvert.state(value);
153
+ }
154
+ function fixRating(value) {
155
+ return TypeConvert.rating(value);
156
+ }
157
+ const Grades = Object.freeze([
158
+ Rating.Again,
159
+ Rating.Hard,
160
+ Rating.Good,
161
+ Rating.Easy
162
+ ]);
163
+ const FUZZ_RANGES = [
164
+ {
165
+ start: 2.5,
166
+ end: 7,
167
+ factor: 0.15
168
+ },
169
+ {
170
+ start: 7,
171
+ end: 20,
172
+ factor: 0.1
173
+ },
174
+ {
175
+ start: 20,
176
+ end: Infinity,
177
+ factor: 0.05
178
+ }
179
+ ];
180
+ function get_fuzz_range(interval, elapsed_days, maximum_interval) {
181
+ let delta = 1;
182
+ for (const range of FUZZ_RANGES) {
183
+ delta += range.factor * Math.max(Math.min(interval, range.end) - range.start, 0);
184
+ }
185
+ interval = Math.min(interval, maximum_interval);
186
+ let min_ivl = Math.max(2, Math.round(interval - delta));
187
+ const max_ivl = Math.min(Math.round(interval + delta), maximum_interval);
188
+ if (interval > elapsed_days) {
189
+ min_ivl = Math.max(min_ivl, elapsed_days + 1);
190
+ }
191
+ min_ivl = Math.min(min_ivl, max_ivl);
192
+ return { min_ivl, max_ivl };
193
+ }
194
+ function clamp(value, min, max) {
195
+ return Math.min(Math.max(value, min), max);
196
+ }
197
+ function dateDiffInDays(last, cur) {
198
+ const utc1 = Date.UTC(
199
+ last.getUTCFullYear(),
200
+ last.getUTCMonth(),
201
+ last.getUTCDate()
202
+ );
203
+ const utc2 = Date.UTC(
204
+ cur.getUTCFullYear(),
205
+ cur.getUTCMonth(),
206
+ cur.getUTCDate()
207
+ );
208
+ return Math.floor(
209
+ (utc2 - utc1) / 864e5
210
+ /** 1000 * 60 * 60 * 24*/
211
+ );
212
+ }
213
+
214
+ const ConvertStepUnitToMinutes = (step) => {
215
+ const unit = step.slice(-1);
216
+ const value = parseInt(step.slice(0, -1), 10);
217
+ if (Number.isNaN(value) || !Number.isFinite(value) || value < 0) {
218
+ throw new Error(`Invalid step value: ${step}`);
219
+ }
220
+ switch (unit) {
221
+ case "m":
222
+ return value;
223
+ case "h":
224
+ return value * 60;
225
+ case "d":
226
+ return value * 1440;
227
+ default:
228
+ throw new Error(`Invalid step unit: ${step}, expected m/h/d`);
229
+ }
230
+ };
231
+ const BasicLearningStepsStrategy = (params, state, cur_step) => {
232
+ const learning_steps = state === State.Relearning || state === State.Review ? params.relearning_steps : params.learning_steps;
233
+ const steps_length = learning_steps.length;
234
+ if (steps_length === 0 || cur_step >= steps_length) return {};
235
+ const firstStep = learning_steps[0];
236
+ const toMinutes = ConvertStepUnitToMinutes;
237
+ const getAgainInterval = () => {
238
+ return toMinutes(firstStep);
239
+ };
240
+ const getHardInterval = () => {
241
+ if (steps_length === 1) return Math.round(toMinutes(firstStep) * 1.5);
242
+ const nextStep = learning_steps[1];
243
+ return Math.round((toMinutes(firstStep) + toMinutes(nextStep)) / 2);
244
+ };
245
+ const getStepInfo = (index) => {
246
+ if (index < 0 || index >= steps_length) {
247
+ return null;
248
+ } else {
249
+ return learning_steps[index];
250
+ }
251
+ };
252
+ const getGoodMinutes = (step) => {
253
+ return toMinutes(step);
254
+ };
255
+ const result = {};
256
+ const step_info = getStepInfo(Math.max(0, cur_step));
257
+ if (state === State.Review) {
258
+ result[Rating.Again] = {
259
+ scheduled_minutes: toMinutes(step_info),
260
+ next_step: 0
261
+ };
262
+ return result;
263
+ } else {
264
+ result[Rating.Again] = {
265
+ scheduled_minutes: getAgainInterval(),
266
+ next_step: 0
267
+ };
268
+ result[Rating.Hard] = {
269
+ scheduled_minutes: getHardInterval(),
270
+ next_step: cur_step
271
+ };
272
+ const next_info = getStepInfo(cur_step + 1);
273
+ if (next_info) {
274
+ const nextMin = getGoodMinutes(next_info);
275
+ if (nextMin) {
276
+ result[Rating.Good] = {
277
+ scheduled_minutes: Math.round(nextMin),
278
+ next_step: cur_step + 1
279
+ };
280
+ }
281
+ }
282
+ }
283
+ return result;
284
+ };
285
+
286
+ function DefaultInitSeedStrategy() {
287
+ const time = this.review_time.getTime();
288
+ const reps = this.current.reps;
289
+ const mul = this.current.difficulty * this.current.stability;
290
+ return `${time}_${reps}_${mul}`;
291
+ }
292
+ function GenSeedStrategyWithCardId(card_id_field) {
293
+ return function() {
294
+ const card_id = Reflect.get(this.current, card_id_field) ?? 0;
295
+ const reps = this.current.reps;
296
+ return String(card_id + reps || 0);
297
+ };
298
+ }
299
+
300
+ var StrategyMode = /* @__PURE__ */ ((StrategyMode2) => {
301
+ StrategyMode2["SCHEDULER"] = "Scheduler";
302
+ StrategyMode2["LEARNING_STEPS"] = "LearningSteps";
303
+ StrategyMode2["SEED"] = "Seed";
304
+ return StrategyMode2;
305
+ })(StrategyMode || {});
306
+
307
+ class AbstractScheduler {
308
+ last;
309
+ current;
310
+ review_time;
311
+ next = /* @__PURE__ */ new Map();
312
+ algorithm;
313
+ strategies;
314
+ elapsed_days = 0;
315
+ // init
316
+ constructor(card, now, algorithm, strategies) {
317
+ this.algorithm = algorithm;
318
+ this.last = TypeConvert.card(card);
319
+ this.current = TypeConvert.card(card);
320
+ this.review_time = TypeConvert.time(now);
321
+ this.strategies = strategies;
322
+ this.init();
323
+ }
324
+ checkGrade(grade) {
325
+ if (!Number.isFinite(grade) || grade < 0 || grade > 4) {
326
+ throw new Error(`Invalid grade "${grade}",expected 1-4`);
327
+ }
328
+ }
329
+ init() {
330
+ const { state, last_review } = this.current;
331
+ let interval = 0;
332
+ if (state !== State.New && last_review) {
333
+ interval = dateDiffInDays(last_review, this.review_time);
334
+ }
335
+ this.current.last_review = this.review_time;
336
+ this.elapsed_days = interval;
337
+ this.current.elapsed_days = interval;
338
+ this.current.reps += 1;
339
+ let seed_strategy = DefaultInitSeedStrategy;
340
+ if (this.strategies) {
341
+ const custom_strategy = this.strategies.get(StrategyMode.SEED);
342
+ if (custom_strategy) {
343
+ seed_strategy = custom_strategy;
344
+ }
345
+ }
346
+ this.algorithm.seed = seed_strategy.call(this);
347
+ }
348
+ preview() {
349
+ return {
350
+ [Rating.Again]: this.review(Rating.Again),
351
+ [Rating.Hard]: this.review(Rating.Hard),
352
+ [Rating.Good]: this.review(Rating.Good),
353
+ [Rating.Easy]: this.review(Rating.Easy),
354
+ [Symbol.iterator]: this.previewIterator.bind(this)
355
+ };
356
+ }
357
+ *previewIterator() {
358
+ for (const grade of Grades) {
359
+ yield this.review(grade);
360
+ }
361
+ }
362
+ review(grade) {
363
+ const { state } = this.last;
364
+ let item;
365
+ this.checkGrade(grade);
366
+ switch (state) {
367
+ case State.New:
368
+ item = this.newState(grade);
369
+ break;
370
+ case State.Learning:
371
+ case State.Relearning:
372
+ item = this.learningState(grade);
373
+ break;
374
+ case State.Review:
375
+ item = this.reviewState(grade);
376
+ break;
377
+ }
378
+ return item;
379
+ }
380
+ buildLog(rating) {
381
+ const { last_review, due, elapsed_days } = this.last;
382
+ return {
383
+ rating,
384
+ state: this.current.state,
385
+ due: last_review || due,
386
+ stability: this.current.stability,
387
+ difficulty: this.current.difficulty,
388
+ elapsed_days: this.elapsed_days,
389
+ last_elapsed_days: elapsed_days,
390
+ scheduled_days: this.current.scheduled_days,
391
+ learning_steps: this.current.learning_steps,
392
+ review: this.review_time
393
+ };
394
+ }
395
+ }
396
+
397
+ class Alea {
398
+ c;
399
+ s0;
400
+ s1;
401
+ s2;
402
+ constructor(seed) {
403
+ const mash = Mash();
404
+ this.c = 1;
405
+ this.s0 = mash(" ");
406
+ this.s1 = mash(" ");
407
+ this.s2 = mash(" ");
408
+ if (seed == null) seed = Date.now();
409
+ this.s0 -= mash(seed);
410
+ if (this.s0 < 0) this.s0 += 1;
411
+ this.s1 -= mash(seed);
412
+ if (this.s1 < 0) this.s1 += 1;
413
+ this.s2 -= mash(seed);
414
+ if (this.s2 < 0) this.s2 += 1;
415
+ }
416
+ next() {
417
+ const t = 2091639 * this.s0 + this.c * 23283064365386963e-26;
418
+ this.s0 = this.s1;
419
+ this.s1 = this.s2;
420
+ this.c = t | 0;
421
+ this.s2 = t - this.c;
422
+ return this.s2;
423
+ }
424
+ set state(state) {
425
+ this.c = state.c;
426
+ this.s0 = state.s0;
427
+ this.s1 = state.s1;
428
+ this.s2 = state.s2;
429
+ }
430
+ get state() {
431
+ return {
432
+ c: this.c,
433
+ s0: this.s0,
434
+ s1: this.s1,
435
+ s2: this.s2
436
+ };
437
+ }
438
+ }
439
+ function Mash() {
440
+ let n = 4022871197;
441
+ return function mash(data) {
442
+ data = String(data);
443
+ for (let i = 0; i < data.length; i++) {
444
+ n += data.charCodeAt(i);
445
+ let h = 0.02519603282416938 * n;
446
+ n = h >>> 0;
447
+ h -= n;
448
+ h *= n;
449
+ n = h >>> 0;
450
+ h -= n;
451
+ n += h * 4294967296;
452
+ }
453
+ return (n >>> 0) * 23283064365386963e-26;
454
+ };
455
+ }
456
+ function alea(seed) {
457
+ const xg = new Alea(seed);
458
+ const prng = () => xg.next();
459
+ prng.int32 = () => xg.next() * 4294967296 | 0;
460
+ prng.double = () => prng() + (prng() * 2097152 | 0) * 11102230246251565e-32;
461
+ prng.state = () => xg.state;
462
+ prng.importState = (state) => {
463
+ xg.state = state;
464
+ return prng;
465
+ };
466
+ return prng;
467
+ }
468
+
469
+ const version="5.2.1";
470
+
471
+ const default_request_retention = 0.9;
472
+ const default_maximum_interval = 36500;
473
+ const default_enable_fuzz = false;
474
+ const default_enable_short_term = true;
475
+ const default_learning_steps = Object.freeze([
476
+ "1m",
477
+ "10m"
478
+ ]);
479
+ const default_relearning_steps = Object.freeze([
480
+ "10m"
481
+ ]);
482
+ const FSRSVersion = `v${version} using FSRS-6.0`;
483
+ const S_MIN = 1e-3;
484
+ const S_MAX = 36500;
485
+ const INIT_S_MAX = 100;
486
+ const FSRS5_DEFAULT_DECAY = 0.5;
487
+ const FSRS6_DEFAULT_DECAY = 0.1542;
488
+ const default_w = Object.freeze([
489
+ 0.212,
490
+ 1.2931,
491
+ 2.3065,
492
+ 8.2956,
493
+ 6.4133,
494
+ 0.8334,
495
+ 3.0194,
496
+ 1e-3,
497
+ 1.8722,
498
+ 0.1666,
499
+ 0.796,
500
+ 1.4835,
501
+ 0.0614,
502
+ 0.2629,
503
+ 1.6483,
504
+ 0.6014,
505
+ 1.8729,
506
+ 0.5425,
507
+ 0.0912,
508
+ 0.0658,
509
+ FSRS6_DEFAULT_DECAY
510
+ ]);
511
+ const W17_W18_Ceiling = 2;
512
+ const CLAMP_PARAMETERS = (w17_w18_ceiling) => [
513
+ [S_MIN, INIT_S_MAX],
514
+ [S_MIN, INIT_S_MAX],
515
+ [S_MIN, INIT_S_MAX],
516
+ [S_MIN, INIT_S_MAX],
517
+ [1, 10],
518
+ [1e-3, 4],
519
+ [1e-3, 4],
520
+ [1e-3, 0.75],
521
+ [0, 4.5],
522
+ [0, 0.8],
523
+ [1e-3, 3.5],
524
+ [1e-3, 5],
525
+ [1e-3, 0.25],
526
+ [1e-3, 0.9],
527
+ [0, 4],
528
+ [0, 1],
529
+ [1, 6],
530
+ [0, w17_w18_ceiling],
531
+ [0, w17_w18_ceiling],
532
+ [0, 0.8],
533
+ [0.1, 0.8]
534
+ ];
535
+
536
+ const clipParameters = (parameters, numRelearningSteps) => {
537
+ let w17_w18_ceiling = W17_W18_Ceiling;
538
+ if (Math.max(0, numRelearningSteps) > 1) {
539
+ const value = -(Math.log(parameters[11]) + Math.log(Math.pow(2, parameters[13]) - 1) + parameters[14] * 0.3) / numRelearningSteps;
540
+ w17_w18_ceiling = clamp(+value.toFixed(8), 0.01, 2);
541
+ }
542
+ const clip = CLAMP_PARAMETERS(w17_w18_ceiling);
543
+ return clip.map(([min, max], index) => clamp(parameters[index], min, max));
544
+ };
545
+ const checkParameters = (parameters) => {
546
+ const invalid = parameters.find(
547
+ (param) => !Number.isFinite(param) && !Number.isNaN(param)
548
+ );
549
+ if (invalid !== void 0) {
550
+ throw Error(`Non-finite or NaN value in parameters ${parameters}`);
551
+ } else if (![17, 19, 21].includes(parameters.length)) {
552
+ throw Error(
553
+ `Invalid parameter length: ${parameters.length}. Must be 17, 19 or 21 for FSRSv4, 5 and 6 respectively.`
554
+ );
555
+ }
556
+ return parameters;
557
+ };
558
+ const migrateParameters = (parameters) => {
559
+ if (parameters === void 0) {
560
+ return [...default_w];
561
+ }
562
+ switch (parameters.length) {
563
+ case 21:
564
+ return [...parameters];
565
+ case 19:
566
+ console.debug("[FSRS-6]auto fill w from 19 to 21 length");
567
+ return [...parameters, 0, FSRS5_DEFAULT_DECAY];
568
+ case 17: {
569
+ const w = [...parameters];
570
+ w[4] = +(w[5] * 2 + w[4]).toFixed(8);
571
+ w[5] = +(Math.log(w[5] * 3 + 1) / 3).toFixed(8);
572
+ w[6] = +(w[6] + 0.5).toFixed(8);
573
+ console.debug("[FSRS-6]auto fill w from 17 to 21 length");
574
+ return w.concat([0, 0, 0, FSRS5_DEFAULT_DECAY]);
575
+ }
576
+ default:
577
+ console.warn("[FSRS]Invalid parameters length, using default parameters");
578
+ return [...default_w];
579
+ }
580
+ };
581
+ const generatorParameters = (props) => {
582
+ const learning_steps = Array.isArray(props?.learning_steps) ? props.learning_steps : default_learning_steps;
583
+ const relearning_steps = Array.isArray(props?.relearning_steps) ? props.relearning_steps : default_relearning_steps;
584
+ const w = clipParameters(migrateParameters(props?.w), relearning_steps.length);
585
+ return {
586
+ request_retention: props?.request_retention || default_request_retention,
587
+ maximum_interval: props?.maximum_interval || default_maximum_interval,
588
+ w,
589
+ enable_fuzz: props?.enable_fuzz ?? default_enable_fuzz,
590
+ enable_short_term: props?.enable_short_term ?? default_enable_short_term,
591
+ learning_steps,
592
+ relearning_steps
593
+ };
594
+ };
595
+ function createEmptyCard(now, afterHandler) {
596
+ const emptyCard = {
597
+ due: now ? TypeConvert.time(now) : /* @__PURE__ */ new Date(),
598
+ stability: 0,
599
+ difficulty: 0,
600
+ elapsed_days: 0,
601
+ scheduled_days: 0,
602
+ reps: 0,
603
+ lapses: 0,
604
+ learning_steps: 0,
605
+ state: State.New,
606
+ last_review: void 0
607
+ };
608
+ if (afterHandler && typeof afterHandler === "function") {
609
+ return afterHandler(emptyCard);
610
+ } else {
611
+ return emptyCard;
612
+ }
613
+ }
614
+
615
+ const computeDecayFactor = (decayOrParams) => {
616
+ const decay = typeof decayOrParams === "number" ? -decayOrParams : -decayOrParams[20];
617
+ const factor = Math.exp(Math.pow(decay, -1) * Math.log(0.9)) - 1;
618
+ return { decay, factor: +factor.toFixed(8) };
619
+ };
620
+ function forgetting_curve(decayOrParams, elapsed_days, stability) {
621
+ const { decay, factor } = computeDecayFactor(decayOrParams);
622
+ return +Math.pow(1 + factor * elapsed_days / stability, decay).toFixed(8);
623
+ }
624
+ class FSRSAlgorithm {
625
+ param;
626
+ intervalModifier;
627
+ _seed;
628
+ constructor(params) {
629
+ this.param = new Proxy(
630
+ generatorParameters(params),
631
+ this.params_handler_proxy()
632
+ );
633
+ this.intervalModifier = this.calculate_interval_modifier(
634
+ this.param.request_retention
635
+ );
636
+ this.forgetting_curve = forgetting_curve.bind(this, this.param.w);
637
+ }
638
+ get interval_modifier() {
639
+ return this.intervalModifier;
640
+ }
641
+ set seed(seed) {
642
+ this._seed = seed;
643
+ }
644
+ /**
645
+ * @see https://github.com/open-spaced-repetition/fsrs4anki/wiki/The-Algorithm#fsrs-5
646
+ *
647
+ * The formula used is: $$I(r,s) = (r^{\frac{1}{DECAY}} - 1) / FACTOR \times s$$
648
+ * @param request_retention 0<request_retention<=1,Requested retention rate
649
+ * @throws {Error} Requested retention rate should be in the range (0,1]
650
+ */
651
+ calculate_interval_modifier(request_retention) {
652
+ if (request_retention <= 0 || request_retention > 1) {
653
+ throw new Error("Requested retention rate should be in the range (0,1]");
654
+ }
655
+ const { decay, factor } = computeDecayFactor(this.param.w);
656
+ return +((Math.pow(request_retention, 1 / decay) - 1) / factor).toFixed(8);
657
+ }
658
+ /**
659
+ * Get the parameters of the algorithm.
660
+ */
661
+ get parameters() {
662
+ return this.param;
663
+ }
664
+ /**
665
+ * Set the parameters of the algorithm.
666
+ * @param params Partial<FSRSParameters>
667
+ */
668
+ set parameters(params) {
669
+ this.update_parameters(params);
670
+ }
671
+ params_handler_proxy() {
672
+ const _this = this;
673
+ return {
674
+ set: function(target, prop, value) {
675
+ if (prop === "request_retention" && Number.isFinite(value)) {
676
+ _this.intervalModifier = _this.calculate_interval_modifier(
677
+ Number(value)
678
+ );
679
+ } else if (prop === "w") {
680
+ value = clipParameters(
681
+ migrateParameters(value),
682
+ target.relearning_steps.length
683
+ );
684
+ _this.forgetting_curve = forgetting_curve.bind(this, value);
685
+ _this.intervalModifier = _this.calculate_interval_modifier(
686
+ Number(target.request_retention)
687
+ );
688
+ }
689
+ Reflect.set(target, prop, value);
690
+ return true;
691
+ }
692
+ };
693
+ }
694
+ update_parameters(params) {
695
+ const _params = generatorParameters(params);
696
+ for (const key in _params) {
697
+ const paramKey = key;
698
+ this.param[paramKey] = _params[paramKey];
699
+ }
700
+ }
701
+ /**
702
+ * The formula used is :
703
+ * $$ S_0(G) = w_{G-1}$$
704
+ * $$S_0 = \max \lbrace S_0,0.1\rbrace $$
705
+
706
+ * @param g Grade (rating at Anki) [1.again,2.hard,3.good,4.easy]
707
+ * @return Stability (interval when R=90%)
708
+ */
709
+ init_stability(g) {
710
+ return Math.max(this.param.w[g - 1], 0.1);
711
+ }
712
+ /**
713
+ * The formula used is :
714
+ * $$D_0(G) = w_4 - e^{(G-1) \cdot w_5} + 1 $$
715
+ * $$D_0 = \min \lbrace \max \lbrace D_0(G),1 \rbrace,10 \rbrace$$
716
+ * where the $$D_0(1)=w_4$$ when the first rating is good.
717
+ *
718
+ * @param {Grade} g Grade (rating at Anki) [1.again,2.hard,3.good,4.easy]
719
+ * @return {number} Difficulty $$D \in [1,10]$$
720
+ */
721
+ init_difficulty(g) {
722
+ const d = this.param.w[4] - Math.exp((g - 1) * this.param.w[5]) + 1;
723
+ return +d.toFixed(8);
724
+ }
725
+ /**
726
+ * If fuzzing is disabled or ivl is less than 2.5, it returns the original interval.
727
+ * @param {number} ivl - The interval to be fuzzed.
728
+ * @param {number} elapsed_days t days since the last review
729
+ * @return {number} - The fuzzed interval.
730
+ **/
731
+ apply_fuzz(ivl, elapsed_days) {
732
+ if (!this.param.enable_fuzz || ivl < 2.5) return Math.round(ivl);
733
+ const generator = alea(this._seed);
734
+ const fuzz_factor = generator();
735
+ const { min_ivl, max_ivl } = get_fuzz_range(
736
+ ivl,
737
+ elapsed_days,
738
+ this.param.maximum_interval
739
+ );
740
+ return Math.floor(fuzz_factor * (max_ivl - min_ivl + 1) + min_ivl);
741
+ }
742
+ /**
743
+ * @see The formula used is : {@link FSRSAlgorithm.calculate_interval_modifier}
744
+ * @param {number} s - Stability (interval when R=90%)
745
+ * @param {number} elapsed_days t days since the last review
746
+ */
747
+ next_interval(s, elapsed_days) {
748
+ const newInterval = Math.min(
749
+ Math.max(1, Math.round(s * this.intervalModifier)),
750
+ this.param.maximum_interval
751
+ );
752
+ return this.apply_fuzz(newInterval, elapsed_days);
753
+ }
754
+ /**
755
+ * @see https://github.com/open-spaced-repetition/fsrs4anki/issues/697
756
+ */
757
+ linear_damping(delta_d, old_d) {
758
+ return +(delta_d * (10 - old_d) / 9).toFixed(8);
759
+ }
760
+ /**
761
+ * The formula used is :
762
+ * $$\text{delta}_d = -w_6 \cdot (g - 3)$$
763
+ * $$\text{next}_d = D + \text{linear damping}(\text{delta}_d , D)$$
764
+ * $$D^\prime(D,R) = w_7 \cdot D_0(4) +(1 - w_7) \cdot \text{next}_d$$
765
+ * @param {number} d Difficulty $$D \in [1,10]$$
766
+ * @param {Grade} g Grade (rating at Anki) [1.again,2.hard,3.good,4.easy]
767
+ * @return {number} $$\text{next}_D$$
768
+ */
769
+ next_difficulty(d, g) {
770
+ const delta_d = -this.param.w[6] * (g - 3);
771
+ const next_d = d + this.linear_damping(delta_d, d);
772
+ return clamp(
773
+ this.mean_reversion(this.init_difficulty(Rating.Easy), next_d),
774
+ 1,
775
+ 10
776
+ );
777
+ }
778
+ /**
779
+ * The formula used is :
780
+ * $$w_7 \cdot \text{init} +(1 - w_7) \cdot \text{current}$$
781
+ * @param {number} init $$w_2 : D_0(3) = w_2 + (R-2) \cdot w_3= w_2$$
782
+ * @param {number} current $$D - w_6 \cdot (R - 2)$$
783
+ * @return {number} difficulty
784
+ */
785
+ mean_reversion(init, current) {
786
+ return +(this.param.w[7] * init + (1 - this.param.w[7]) * current).toFixed(
787
+ 8
788
+ );
789
+ }
790
+ /**
791
+ * The formula used is :
792
+ * $$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}(\text{if} G=2) \cdot w_{16}(\text{if} G=4)+1)$$
793
+ * @param {number} d Difficulty D \in [1,10]
794
+ * @param {number} s Stability (interval when R=90%)
795
+ * @param {number} r Retrievability (probability of recall)
796
+ * @param {Grade} g Grade (Rating[0.again,1.hard,2.good,3.easy])
797
+ * @return {number} S^\prime_r new stability after recall
798
+ */
799
+ next_recall_stability(d, s, r, g) {
800
+ const hard_penalty = Rating.Hard === g ? this.param.w[15] : 1;
801
+ const easy_bound = Rating.Easy === g ? this.param.w[16] : 1;
802
+ return +clamp(
803
+ 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),
804
+ S_MIN,
805
+ 36500
806
+ ).toFixed(8);
807
+ }
808
+ /**
809
+ * The formula used is :
810
+ * $$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)}$$
811
+ * enable_short_term = true : $$S^\prime_f \in \min \lbrace \max \lbrace S^\prime_f,0.01\rbrace, \frac{S}{e^{w_{17} \cdot w_{18}}} \rbrace$$
812
+ * enable_short_term = false : $$S^\prime_f \in \min \lbrace \max \lbrace S^\prime_f,0.01\rbrace, S \rbrace$$
813
+ * @param {number} d Difficulty D \in [1,10]
814
+ * @param {number} s Stability (interval when R=90%)
815
+ * @param {number} r Retrievability (probability of recall)
816
+ * @return {number} S^\prime_f new stability after forgetting
817
+ */
818
+ next_forget_stability(d, s, r) {
819
+ return +clamp(
820
+ 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]),
821
+ S_MIN,
822
+ 36500
823
+ ).toFixed(8);
824
+ }
825
+ /**
826
+ * The formula used is :
827
+ * $$S^\prime_s(S,G) = S \cdot e^{w_{17} \cdot (G-3+w_{18})}$$
828
+ * @param {number} s Stability (interval when R=90%)
829
+ * @param {Grade} g Grade (Rating[0.again,1.hard,2.good,3.easy])
830
+ */
831
+ next_short_term_stability(s, g) {
832
+ const sinc = Math.pow(s, -this.param.w[19]) * Math.exp(this.param.w[17] * (g - 3 + this.param.w[18]));
833
+ const maskedSinc = g >= 3 ? Math.max(sinc, 1) : sinc;
834
+ return +clamp(s * maskedSinc, S_MIN, 36500).toFixed(8);
835
+ }
836
+ /**
837
+ * The formula used is :
838
+ * $$R(t,S) = (1 + \text{FACTOR} \times \frac{t}{9 \cdot S})^{\text{DECAY}}$$
839
+ * @param {number} elapsed_days t days since the last review
840
+ * @param {number} stability Stability (interval when R=90%)
841
+ * @return {number} r Retrievability (probability of recall)
842
+ */
843
+ forgetting_curve;
844
+ /**
845
+ * Calculates the next state of memory based on the current state, time elapsed, and grade.
846
+ *
847
+ * @param memory_state - The current state of memory, which can be null.
848
+ * @param t - The time elapsed since the last review.
849
+ * @param {Rating} g Grade (Rating[0.Manual,1.Again,2.Hard,3.Good,4.Easy])
850
+ * @returns The next state of memory with updated difficulty and stability.
851
+ */
852
+ next_state(memory_state, t, g) {
853
+ const { difficulty: d, stability: s } = memory_state ?? {
854
+ difficulty: 0,
855
+ stability: 0
856
+ };
857
+ if (t < 0) {
858
+ throw new Error(`Invalid delta_t "${t}"`);
859
+ }
860
+ if (g < 0 || g > 4) {
861
+ throw new Error(`Invalid grade "${g}"`);
862
+ }
863
+ if (d === 0 && s === 0) {
864
+ return {
865
+ difficulty: clamp(this.init_difficulty(g), 1, 10),
866
+ stability: this.init_stability(g)
867
+ };
868
+ }
869
+ if (g === 0) {
870
+ return {
871
+ difficulty: d,
872
+ stability: s
873
+ };
874
+ }
875
+ if (d < 1 || s < S_MIN) {
876
+ throw new Error(
877
+ `Invalid memory state { difficulty: ${d}, stability: ${s} }`
878
+ );
879
+ }
880
+ const r = this.forgetting_curve(t, s);
881
+ const s_after_success = this.next_recall_stability(d, s, r, g);
882
+ const s_after_fail = this.next_forget_stability(d, s, r);
883
+ const s_after_short_term = this.next_short_term_stability(s, g);
884
+ let new_s = s_after_success;
885
+ if (g === 1) {
886
+ let [w_17, w_18] = [0, 0];
887
+ if (this.param.enable_short_term) {
888
+ w_17 = this.param.w[17];
889
+ w_18 = this.param.w[18];
890
+ }
891
+ const next_s_min = s / Math.exp(w_17 * w_18);
892
+ new_s = clamp(+next_s_min.toFixed(8), S_MIN, s_after_fail);
893
+ }
894
+ if (t === 0 && this.param.enable_short_term) {
895
+ new_s = s_after_short_term;
896
+ }
897
+ const new_d = this.next_difficulty(d, g);
898
+ return { difficulty: new_d, stability: new_s };
899
+ }
900
+ }
901
+
902
+ class BasicScheduler extends AbstractScheduler {
903
+ learningStepsStrategy;
904
+ constructor(card, now, algorithm, strategies) {
905
+ super(card, now, algorithm, strategies);
906
+ let learningStepStrategy = BasicLearningStepsStrategy;
907
+ if (this.strategies) {
908
+ const custom_strategy = this.strategies.get(StrategyMode.LEARNING_STEPS);
909
+ if (custom_strategy) {
910
+ learningStepStrategy = custom_strategy;
911
+ }
912
+ }
913
+ this.learningStepsStrategy = learningStepStrategy;
914
+ }
915
+ getLearningInfo(card, grade) {
916
+ const parameters = this.algorithm.parameters;
917
+ card.learning_steps = card.learning_steps || 0;
918
+ const steps_strategy = this.learningStepsStrategy(
919
+ parameters,
920
+ card.state,
921
+ // In the original learning steps setup (Again = 5m, Hard = 10m, Good = FSRS),
922
+ // not adding 1 can cause slight variations in the memory state’s ds.
923
+ this.current.state === State.Learning && grade !== Rating.Again && grade !== Rating.Hard ? card.learning_steps + 1 : card.learning_steps
924
+ );
925
+ const scheduled_minutes = Math.max(
926
+ 0,
927
+ steps_strategy[grade]?.scheduled_minutes ?? 0
928
+ );
929
+ const next_steps = Math.max(0, steps_strategy[grade]?.next_step ?? 0);
930
+ return {
931
+ scheduled_minutes,
932
+ next_steps
933
+ };
934
+ }
935
+ /**
936
+ * @description This function applies the learning steps based on the current card's state and grade.
937
+ */
938
+ applyLearningSteps(nextCard, grade, to_state) {
939
+ const { scheduled_minutes, next_steps } = this.getLearningInfo(
940
+ this.current,
941
+ grade
942
+ );
943
+ if (scheduled_minutes > 0 && scheduled_minutes < 1440) {
944
+ nextCard.learning_steps = next_steps;
945
+ nextCard.scheduled_days = 0;
946
+ nextCard.state = to_state;
947
+ nextCard.due = date_scheduler(
948
+ this.review_time,
949
+ Math.round(scheduled_minutes),
950
+ false
951
+ /** true:days false: minute */
952
+ );
953
+ } else {
954
+ nextCard.state = State.Review;
955
+ if (scheduled_minutes >= 1440) {
956
+ nextCard.learning_steps = next_steps;
957
+ nextCard.due = date_scheduler(
958
+ this.review_time,
959
+ Math.round(scheduled_minutes),
960
+ false
961
+ /** true:days false: minute */
962
+ );
963
+ nextCard.scheduled_days = Math.floor(scheduled_minutes / 1440);
964
+ } else {
965
+ nextCard.learning_steps = 0;
966
+ const interval = this.algorithm.next_interval(
967
+ nextCard.stability,
968
+ this.elapsed_days
969
+ );
970
+ nextCard.scheduled_days = interval;
971
+ nextCard.due = date_scheduler(this.review_time, interval, true);
972
+ }
973
+ }
974
+ }
975
+ newState(grade) {
976
+ const exist = this.next.get(grade);
977
+ if (exist) {
978
+ return exist;
979
+ }
980
+ const next = TypeConvert.card(this.current);
981
+ next.difficulty = clamp(this.algorithm.init_difficulty(grade), 1, 10);
982
+ next.stability = this.algorithm.init_stability(grade);
983
+ this.applyLearningSteps(next, grade, State.Learning);
984
+ const item = {
985
+ card: next,
986
+ log: this.buildLog(grade)
987
+ };
988
+ this.next.set(grade, item);
989
+ return item;
990
+ }
991
+ learningState(grade) {
992
+ const exist = this.next.get(grade);
993
+ if (exist) {
994
+ return exist;
995
+ }
996
+ const { state, difficulty, stability } = this.last;
997
+ const next = TypeConvert.card(this.current);
998
+ next.difficulty = this.algorithm.next_difficulty(difficulty, grade);
999
+ next.stability = this.algorithm.next_short_term_stability(stability, grade);
1000
+ this.applyLearningSteps(
1001
+ next,
1002
+ grade,
1003
+ state
1004
+ /** Learning or Relearning */
1005
+ );
1006
+ const item = {
1007
+ card: next,
1008
+ log: this.buildLog(grade)
1009
+ };
1010
+ this.next.set(grade, item);
1011
+ return item;
1012
+ }
1013
+ reviewState(grade) {
1014
+ const exist = this.next.get(grade);
1015
+ if (exist) {
1016
+ return exist;
1017
+ }
1018
+ const interval = this.elapsed_days;
1019
+ const { difficulty, stability } = this.last;
1020
+ const retrievability = this.algorithm.forgetting_curve(interval, stability);
1021
+ const next_again = TypeConvert.card(this.current);
1022
+ const next_hard = TypeConvert.card(this.current);
1023
+ const next_good = TypeConvert.card(this.current);
1024
+ const next_easy = TypeConvert.card(this.current);
1025
+ this.next_ds(
1026
+ next_again,
1027
+ next_hard,
1028
+ next_good,
1029
+ next_easy,
1030
+ difficulty,
1031
+ stability,
1032
+ retrievability
1033
+ );
1034
+ this.next_interval(next_hard, next_good, next_easy, interval);
1035
+ this.next_state(next_hard, next_good, next_easy);
1036
+ this.applyLearningSteps(next_again, Rating.Again, State.Relearning);
1037
+ next_again.lapses += 1;
1038
+ const item_again = {
1039
+ card: next_again,
1040
+ log: this.buildLog(Rating.Again)
1041
+ };
1042
+ const item_hard = {
1043
+ card: next_hard,
1044
+ log: super.buildLog(Rating.Hard)
1045
+ };
1046
+ const item_good = {
1047
+ card: next_good,
1048
+ log: super.buildLog(Rating.Good)
1049
+ };
1050
+ const item_easy = {
1051
+ card: next_easy,
1052
+ log: super.buildLog(Rating.Easy)
1053
+ };
1054
+ this.next.set(Rating.Again, item_again);
1055
+ this.next.set(Rating.Hard, item_hard);
1056
+ this.next.set(Rating.Good, item_good);
1057
+ this.next.set(Rating.Easy, item_easy);
1058
+ return this.next.get(grade);
1059
+ }
1060
+ /**
1061
+ * Review next_ds
1062
+ */
1063
+ next_ds(next_again, next_hard, next_good, next_easy, difficulty, stability, retrievability) {
1064
+ next_again.difficulty = this.algorithm.next_difficulty(
1065
+ difficulty,
1066
+ Rating.Again
1067
+ );
1068
+ const nextSMin = stability / Math.exp(
1069
+ this.algorithm.parameters.w[17] * this.algorithm.parameters.w[18]
1070
+ );
1071
+ const s_after_fail = this.algorithm.next_forget_stability(
1072
+ difficulty,
1073
+ stability,
1074
+ retrievability
1075
+ );
1076
+ next_again.stability = clamp(+nextSMin.toFixed(8), S_MIN, s_after_fail);
1077
+ next_hard.difficulty = this.algorithm.next_difficulty(
1078
+ difficulty,
1079
+ Rating.Hard
1080
+ );
1081
+ next_hard.stability = this.algorithm.next_recall_stability(
1082
+ difficulty,
1083
+ stability,
1084
+ retrievability,
1085
+ Rating.Hard
1086
+ );
1087
+ next_good.difficulty = this.algorithm.next_difficulty(
1088
+ difficulty,
1089
+ Rating.Good
1090
+ );
1091
+ next_good.stability = this.algorithm.next_recall_stability(
1092
+ difficulty,
1093
+ stability,
1094
+ retrievability,
1095
+ Rating.Good
1096
+ );
1097
+ next_easy.difficulty = this.algorithm.next_difficulty(
1098
+ difficulty,
1099
+ Rating.Easy
1100
+ );
1101
+ next_easy.stability = this.algorithm.next_recall_stability(
1102
+ difficulty,
1103
+ stability,
1104
+ retrievability,
1105
+ Rating.Easy
1106
+ );
1107
+ }
1108
+ /**
1109
+ * Review next_interval
1110
+ */
1111
+ next_interval(next_hard, next_good, next_easy, interval) {
1112
+ let hard_interval, good_interval;
1113
+ hard_interval = this.algorithm.next_interval(next_hard.stability, interval);
1114
+ good_interval = this.algorithm.next_interval(next_good.stability, interval);
1115
+ hard_interval = Math.min(hard_interval, good_interval);
1116
+ good_interval = Math.max(good_interval, hard_interval + 1);
1117
+ const easy_interval = Math.max(
1118
+ this.algorithm.next_interval(next_easy.stability, interval),
1119
+ good_interval + 1
1120
+ );
1121
+ next_hard.scheduled_days = hard_interval;
1122
+ next_hard.due = date_scheduler(this.review_time, hard_interval, true);
1123
+ next_good.scheduled_days = good_interval;
1124
+ next_good.due = date_scheduler(this.review_time, good_interval, true);
1125
+ next_easy.scheduled_days = easy_interval;
1126
+ next_easy.due = date_scheduler(this.review_time, easy_interval, true);
1127
+ }
1128
+ /**
1129
+ * Review next_state
1130
+ */
1131
+ next_state(next_hard, next_good, next_easy) {
1132
+ next_hard.state = State.Review;
1133
+ next_hard.learning_steps = 0;
1134
+ next_good.state = State.Review;
1135
+ next_good.learning_steps = 0;
1136
+ next_easy.state = State.Review;
1137
+ next_easy.learning_steps = 0;
1138
+ }
1139
+ }
1140
+
1141
+ class LongTermScheduler extends AbstractScheduler {
1142
+ newState(grade) {
1143
+ const exist = this.next.get(grade);
1144
+ if (exist) {
1145
+ return exist;
1146
+ }
1147
+ this.current.scheduled_days = 0;
1148
+ this.current.elapsed_days = 0;
1149
+ const next_again = TypeConvert.card(this.current);
1150
+ const next_hard = TypeConvert.card(this.current);
1151
+ const next_good = TypeConvert.card(this.current);
1152
+ const next_easy = TypeConvert.card(this.current);
1153
+ this.init_ds(next_again, next_hard, next_good, next_easy);
1154
+ const first_interval = 0;
1155
+ this.next_interval(
1156
+ next_again,
1157
+ next_hard,
1158
+ next_good,
1159
+ next_easy,
1160
+ first_interval
1161
+ );
1162
+ this.next_state(next_again, next_hard, next_good, next_easy);
1163
+ this.update_next(next_again, next_hard, next_good, next_easy);
1164
+ return this.next.get(grade);
1165
+ }
1166
+ init_ds(next_again, next_hard, next_good, next_easy) {
1167
+ next_again.difficulty = clamp(
1168
+ this.algorithm.init_difficulty(Rating.Again),
1169
+ 1,
1170
+ 10
1171
+ );
1172
+ next_again.stability = this.algorithm.init_stability(Rating.Again);
1173
+ next_hard.difficulty = clamp(
1174
+ this.algorithm.init_difficulty(Rating.Hard),
1175
+ 1,
1176
+ 10
1177
+ );
1178
+ next_hard.stability = this.algorithm.init_stability(Rating.Hard);
1179
+ next_good.difficulty = clamp(
1180
+ this.algorithm.init_difficulty(Rating.Good),
1181
+ 1,
1182
+ 10
1183
+ );
1184
+ next_good.stability = this.algorithm.init_stability(Rating.Good);
1185
+ next_easy.difficulty = clamp(
1186
+ this.algorithm.init_difficulty(Rating.Easy),
1187
+ 1,
1188
+ 10
1189
+ );
1190
+ next_easy.stability = this.algorithm.init_stability(Rating.Easy);
1191
+ }
1192
+ /**
1193
+ * @see https://github.com/open-spaced-repetition/ts-fsrs/issues/98#issuecomment-2241923194
1194
+ */
1195
+ learningState(grade) {
1196
+ return this.reviewState(grade);
1197
+ }
1198
+ reviewState(grade) {
1199
+ const exist = this.next.get(grade);
1200
+ if (exist) {
1201
+ return exist;
1202
+ }
1203
+ const interval = this.elapsed_days;
1204
+ const { difficulty, stability } = this.last;
1205
+ const retrievability = this.algorithm.forgetting_curve(interval, stability);
1206
+ const next_again = TypeConvert.card(this.current);
1207
+ const next_hard = TypeConvert.card(this.current);
1208
+ const next_good = TypeConvert.card(this.current);
1209
+ const next_easy = TypeConvert.card(this.current);
1210
+ this.next_ds(
1211
+ next_again,
1212
+ next_hard,
1213
+ next_good,
1214
+ next_easy,
1215
+ difficulty,
1216
+ stability,
1217
+ retrievability
1218
+ );
1219
+ this.next_interval(next_again, next_hard, next_good, next_easy, interval);
1220
+ this.next_state(next_again, next_hard, next_good, next_easy);
1221
+ next_again.lapses += 1;
1222
+ this.update_next(next_again, next_hard, next_good, next_easy);
1223
+ return this.next.get(grade);
1224
+ }
1225
+ /**
1226
+ * Review next_ds
1227
+ */
1228
+ next_ds(next_again, next_hard, next_good, next_easy, difficulty, stability, retrievability) {
1229
+ next_again.difficulty = this.algorithm.next_difficulty(
1230
+ difficulty,
1231
+ Rating.Again
1232
+ );
1233
+ const s_after_fail = this.algorithm.next_forget_stability(
1234
+ difficulty,
1235
+ stability,
1236
+ retrievability
1237
+ );
1238
+ next_again.stability = clamp(stability, S_MIN, s_after_fail);
1239
+ next_hard.difficulty = this.algorithm.next_difficulty(
1240
+ difficulty,
1241
+ Rating.Hard
1242
+ );
1243
+ next_hard.stability = this.algorithm.next_recall_stability(
1244
+ difficulty,
1245
+ stability,
1246
+ retrievability,
1247
+ Rating.Hard
1248
+ );
1249
+ next_good.difficulty = this.algorithm.next_difficulty(
1250
+ difficulty,
1251
+ Rating.Good
1252
+ );
1253
+ next_good.stability = this.algorithm.next_recall_stability(
1254
+ difficulty,
1255
+ stability,
1256
+ retrievability,
1257
+ Rating.Good
1258
+ );
1259
+ next_easy.difficulty = this.algorithm.next_difficulty(
1260
+ difficulty,
1261
+ Rating.Easy
1262
+ );
1263
+ next_easy.stability = this.algorithm.next_recall_stability(
1264
+ difficulty,
1265
+ stability,
1266
+ retrievability,
1267
+ Rating.Easy
1268
+ );
1269
+ }
1270
+ /**
1271
+ * Review/New next_interval
1272
+ */
1273
+ next_interval(next_again, next_hard, next_good, next_easy, interval) {
1274
+ let again_interval, hard_interval, good_interval, easy_interval;
1275
+ again_interval = this.algorithm.next_interval(
1276
+ next_again.stability,
1277
+ interval
1278
+ );
1279
+ hard_interval = this.algorithm.next_interval(next_hard.stability, interval);
1280
+ good_interval = this.algorithm.next_interval(next_good.stability, interval);
1281
+ easy_interval = this.algorithm.next_interval(next_easy.stability, interval);
1282
+ again_interval = Math.min(again_interval, hard_interval);
1283
+ hard_interval = Math.max(hard_interval, again_interval + 1);
1284
+ good_interval = Math.max(good_interval, hard_interval + 1);
1285
+ easy_interval = Math.max(easy_interval, good_interval + 1);
1286
+ next_again.scheduled_days = again_interval;
1287
+ next_again.due = date_scheduler(this.review_time, again_interval, true);
1288
+ next_hard.scheduled_days = hard_interval;
1289
+ next_hard.due = date_scheduler(this.review_time, hard_interval, true);
1290
+ next_good.scheduled_days = good_interval;
1291
+ next_good.due = date_scheduler(this.review_time, good_interval, true);
1292
+ next_easy.scheduled_days = easy_interval;
1293
+ next_easy.due = date_scheduler(this.review_time, easy_interval, true);
1294
+ }
1295
+ /**
1296
+ * Review/New next_state
1297
+ */
1298
+ next_state(next_again, next_hard, next_good, next_easy) {
1299
+ next_again.state = State.Review;
1300
+ next_again.learning_steps = 0;
1301
+ next_hard.state = State.Review;
1302
+ next_hard.learning_steps = 0;
1303
+ next_good.state = State.Review;
1304
+ next_good.learning_steps = 0;
1305
+ next_easy.state = State.Review;
1306
+ next_easy.learning_steps = 0;
1307
+ }
1308
+ update_next(next_again, next_hard, next_good, next_easy) {
1309
+ const item_again = {
1310
+ card: next_again,
1311
+ log: this.buildLog(Rating.Again)
1312
+ };
1313
+ const item_hard = {
1314
+ card: next_hard,
1315
+ log: super.buildLog(Rating.Hard)
1316
+ };
1317
+ const item_good = {
1318
+ card: next_good,
1319
+ log: super.buildLog(Rating.Good)
1320
+ };
1321
+ const item_easy = {
1322
+ card: next_easy,
1323
+ log: super.buildLog(Rating.Easy)
1324
+ };
1325
+ this.next.set(Rating.Again, item_again);
1326
+ this.next.set(Rating.Hard, item_hard);
1327
+ this.next.set(Rating.Good, item_good);
1328
+ this.next.set(Rating.Easy, item_easy);
1329
+ }
1330
+ }
1331
+
1332
+ class Reschedule {
1333
+ fsrs;
1334
+ /**
1335
+ * Creates an instance of the `Reschedule` class.
1336
+ * @param fsrs - An instance of the FSRS class used for scheduling.
1337
+ */
1338
+ constructor(fsrs) {
1339
+ this.fsrs = fsrs;
1340
+ }
1341
+ /**
1342
+ * Replays a review for a card and determines the next review date based on the given rating.
1343
+ * @param card - The card being reviewed.
1344
+ * @param reviewed - The date the card was reviewed.
1345
+ * @param rating - The grade given to the card during the review.
1346
+ * @returns A `RecordLogItem` containing the updated card and review log.
1347
+ */
1348
+ replay(card, reviewed, rating) {
1349
+ return this.fsrs.next(card, reviewed, rating);
1350
+ }
1351
+ /**
1352
+ * Processes a manual review for a card, allowing for custom state, stability, difficulty, and due date.
1353
+ * @param card - The card being reviewed.
1354
+ * @param state - The state of the card after the review.
1355
+ * @param reviewed - The date the card was reviewed.
1356
+ * @param elapsed_days - The number of days since the last review.
1357
+ * @param stability - (Optional) The stability of the card.
1358
+ * @param difficulty - (Optional) The difficulty of the card.
1359
+ * @param due - (Optional) The due date for the next review.
1360
+ * @returns A `RecordLogItem` containing the updated card and review log.
1361
+ * @throws Will throw an error if the state or due date is not provided when required.
1362
+ */
1363
+ handleManualRating(card, state, reviewed, elapsed_days, stability, difficulty, due) {
1364
+ if (typeof state === "undefined") {
1365
+ throw new Error("reschedule: state is required for manual rating");
1366
+ }
1367
+ let log;
1368
+ let next_card;
1369
+ if (state === State.New) {
1370
+ log = {
1371
+ rating: Rating.Manual,
1372
+ state,
1373
+ due: due ?? reviewed,
1374
+ stability: card.stability,
1375
+ difficulty: card.difficulty,
1376
+ elapsed_days,
1377
+ last_elapsed_days: card.elapsed_days,
1378
+ scheduled_days: card.scheduled_days,
1379
+ learning_steps: card.learning_steps,
1380
+ review: reviewed
1381
+ };
1382
+ next_card = createEmptyCard(reviewed);
1383
+ next_card.last_review = reviewed;
1384
+ } else {
1385
+ if (typeof due === "undefined") {
1386
+ throw new Error("reschedule: due is required for manual rating");
1387
+ }
1388
+ const scheduled_days = date_diff(due, reviewed, "days");
1389
+ log = {
1390
+ rating: Rating.Manual,
1391
+ state: card.state,
1392
+ due: card.last_review || card.due,
1393
+ stability: card.stability,
1394
+ difficulty: card.difficulty,
1395
+ elapsed_days,
1396
+ last_elapsed_days: card.elapsed_days,
1397
+ scheduled_days: card.scheduled_days,
1398
+ learning_steps: card.learning_steps,
1399
+ review: reviewed
1400
+ };
1401
+ next_card = {
1402
+ ...card,
1403
+ state,
1404
+ due,
1405
+ last_review: reviewed,
1406
+ stability: stability || card.stability,
1407
+ difficulty: difficulty || card.difficulty,
1408
+ elapsed_days,
1409
+ scheduled_days,
1410
+ reps: card.reps + 1
1411
+ };
1412
+ }
1413
+ return { card: next_card, log };
1414
+ }
1415
+ /**
1416
+ * Reschedules a card based on its review history.
1417
+ *
1418
+ * @param current_card - The card to be rescheduled.
1419
+ * @param reviews - An array of review history objects.
1420
+ * @returns An array of record log items representing the rescheduling process.
1421
+ */
1422
+ reschedule(current_card, reviews) {
1423
+ const collections = [];
1424
+ let cur_card = createEmptyCard(current_card.due);
1425
+ for (const review of reviews) {
1426
+ let item;
1427
+ review.review = TypeConvert.time(review.review);
1428
+ if (review.rating === Rating.Manual) {
1429
+ let interval = 0;
1430
+ if (cur_card.state !== State.New && cur_card.last_review) {
1431
+ interval = date_diff(review.review, cur_card.last_review, "days");
1432
+ }
1433
+ item = this.handleManualRating(
1434
+ cur_card,
1435
+ review.state,
1436
+ review.review,
1437
+ interval,
1438
+ review.stability,
1439
+ review.difficulty,
1440
+ review.due ? TypeConvert.time(review.due) : void 0
1441
+ );
1442
+ } else {
1443
+ item = this.replay(cur_card, review.review, review.rating);
1444
+ }
1445
+ collections.push(item);
1446
+ cur_card = item.card;
1447
+ }
1448
+ return collections;
1449
+ }
1450
+ calculateManualRecord(current_card, now, record_log_item, update_memory) {
1451
+ if (!record_log_item) {
1452
+ return null;
1453
+ }
1454
+ const { card: reschedule_card, log } = record_log_item;
1455
+ const cur_card = TypeConvert.card(current_card);
1456
+ if (cur_card.due.getTime() === reschedule_card.due.getTime()) {
1457
+ return null;
1458
+ }
1459
+ cur_card.scheduled_days = date_diff(
1460
+ reschedule_card.due,
1461
+ cur_card.due,
1462
+ "days"
1463
+ );
1464
+ return this.handleManualRating(
1465
+ cur_card,
1466
+ reschedule_card.state,
1467
+ TypeConvert.time(now),
1468
+ log.elapsed_days,
1469
+ update_memory ? reschedule_card.stability : void 0,
1470
+ update_memory ? reschedule_card.difficulty : void 0,
1471
+ reschedule_card.due
1472
+ );
1473
+ }
1474
+ }
1475
+
1476
+ class FSRS extends FSRSAlgorithm {
1477
+ strategyHandler = /* @__PURE__ */ new Map();
1478
+ Scheduler;
1479
+ constructor(param) {
1480
+ super(param);
1481
+ const { enable_short_term } = this.parameters;
1482
+ this.Scheduler = enable_short_term ? BasicScheduler : LongTermScheduler;
1483
+ }
1484
+ params_handler_proxy() {
1485
+ const _this = this;
1486
+ return {
1487
+ set: function(target, prop, value) {
1488
+ if (prop === "request_retention" && Number.isFinite(value)) {
1489
+ _this.intervalModifier = _this.calculate_interval_modifier(
1490
+ Number(value)
1491
+ );
1492
+ } else if (prop === "enable_short_term") {
1493
+ _this.Scheduler = value === true ? BasicScheduler : LongTermScheduler;
1494
+ } else if (prop === "w") {
1495
+ value = clipParameters(
1496
+ migrateParameters(value),
1497
+ target.relearning_steps.length
1498
+ );
1499
+ _this.forgetting_curve = forgetting_curve.bind(this, value);
1500
+ _this.intervalModifier = _this.calculate_interval_modifier(
1501
+ Number(target.request_retention)
1502
+ );
1503
+ }
1504
+ Reflect.set(target, prop, value);
1505
+ return true;
1506
+ }
1507
+ };
1508
+ }
1509
+ useStrategy(mode, handler) {
1510
+ this.strategyHandler.set(mode, handler);
1511
+ return this;
1512
+ }
1513
+ clearStrategy(mode) {
1514
+ if (mode) {
1515
+ this.strategyHandler.delete(mode);
1516
+ } else {
1517
+ this.strategyHandler.clear();
1518
+ }
1519
+ return this;
1520
+ }
1521
+ getScheduler(card, now) {
1522
+ const schedulerStrategy = this.strategyHandler.get(
1523
+ StrategyMode.SCHEDULER
1524
+ );
1525
+ const Scheduler = schedulerStrategy || this.Scheduler;
1526
+ const instance = new Scheduler(card, now, this, this.strategyHandler);
1527
+ return instance;
1528
+ }
1529
+ /**
1530
+ * Display the collection of cards and logs for the four scenarios after scheduling the card at the current time.
1531
+ * @param card Card to be processed
1532
+ * @param now Current time or scheduled time
1533
+ * @param afterHandler Convert the result to another type. (Optional)
1534
+ * @example
1535
+ * ```typescript
1536
+ * const card: Card = createEmptyCard(new Date());
1537
+ * const f = fsrs();
1538
+ * const recordLog = f.repeat(card, new Date());
1539
+ * ```
1540
+ * @example
1541
+ * ```typescript
1542
+ * interface RevLogUnchecked
1543
+ * extends Omit<ReviewLog, "due" | "review" | "state" | "rating"> {
1544
+ * cid: string;
1545
+ * due: Date | number;
1546
+ * state: StateType;
1547
+ * review: Date | number;
1548
+ * rating: RatingType;
1549
+ * }
1550
+ *
1551
+ * interface RepeatRecordLog {
1552
+ * card: CardUnChecked; //see method: createEmptyCard
1553
+ * log: RevLogUnchecked;
1554
+ * }
1555
+ *
1556
+ * function repeatAfterHandler(recordLog: RecordLog) {
1557
+ * const record: { [key in Grade]: RepeatRecordLog } = {} as {
1558
+ * [key in Grade]: RepeatRecordLog;
1559
+ * };
1560
+ * for (const grade of Grades) {
1561
+ * record[grade] = {
1562
+ * card: {
1563
+ * ...(recordLog[grade].card as Card & { cid: string }),
1564
+ * due: recordLog[grade].card.due.getTime(),
1565
+ * state: State[recordLog[grade].card.state] as StateType,
1566
+ * last_review: recordLog[grade].card.last_review
1567
+ * ? recordLog[grade].card.last_review!.getTime()
1568
+ * : null,
1569
+ * },
1570
+ * log: {
1571
+ * ...recordLog[grade].log,
1572
+ * cid: (recordLog[grade].card as Card & { cid: string }).cid,
1573
+ * due: recordLog[grade].log.due.getTime(),
1574
+ * review: recordLog[grade].log.review.getTime(),
1575
+ * state: State[recordLog[grade].log.state] as StateType,
1576
+ * rating: Rating[recordLog[grade].log.rating] as RatingType,
1577
+ * },
1578
+ * };
1579
+ * }
1580
+ * return record;
1581
+ * }
1582
+ * const card: Card = createEmptyCard(new Date(), cardAfterHandler); //see method: createEmptyCard
1583
+ * const f = fsrs();
1584
+ * const recordLog = f.repeat(card, new Date(), repeatAfterHandler);
1585
+ * ```
1586
+ */
1587
+ repeat(card, now, afterHandler) {
1588
+ const instance = this.getScheduler(card, now);
1589
+ const recordLog = instance.preview();
1590
+ if (afterHandler && typeof afterHandler === "function") {
1591
+ return afterHandler(recordLog);
1592
+ } else {
1593
+ return recordLog;
1594
+ }
1595
+ }
1596
+ /**
1597
+ * Display the collection of cards and logs for the card scheduled at the current time, after applying a specific grade rating.
1598
+ * @param card Card to be processed
1599
+ * @param now Current time or scheduled time
1600
+ * @param grade Rating of the review (Again, Hard, Good, Easy)
1601
+ * @param afterHandler Convert the result to another type. (Optional)
1602
+ * @example
1603
+ * ```typescript
1604
+ * const card: Card = createEmptyCard(new Date());
1605
+ * const f = fsrs();
1606
+ * const recordLogItem = f.next(card, new Date(), Rating.Again);
1607
+ * ```
1608
+ * @example
1609
+ * ```typescript
1610
+ * interface RevLogUnchecked
1611
+ * extends Omit<ReviewLog, "due" | "review" | "state" | "rating"> {
1612
+ * cid: string;
1613
+ * due: Date | number;
1614
+ * state: StateType;
1615
+ * review: Date | number;
1616
+ * rating: RatingType;
1617
+ * }
1618
+ *
1619
+ * interface NextRecordLog {
1620
+ * card: CardUnChecked; //see method: createEmptyCard
1621
+ * log: RevLogUnchecked;
1622
+ * }
1623
+ *
1624
+ function nextAfterHandler(recordLogItem: RecordLogItem) {
1625
+ const recordItem = {
1626
+ card: {
1627
+ ...(recordLogItem.card as Card & { cid: string }),
1628
+ due: recordLogItem.card.due.getTime(),
1629
+ state: State[recordLogItem.card.state] as StateType,
1630
+ last_review: recordLogItem.card.last_review
1631
+ ? recordLogItem.card.last_review!.getTime()
1632
+ : null,
1633
+ },
1634
+ log: {
1635
+ ...recordLogItem.log,
1636
+ cid: (recordLogItem.card as Card & { cid: string }).cid,
1637
+ due: recordLogItem.log.due.getTime(),
1638
+ review: recordLogItem.log.review.getTime(),
1639
+ state: State[recordLogItem.log.state] as StateType,
1640
+ rating: Rating[recordLogItem.log.rating] as RatingType,
1641
+ },
1642
+ };
1643
+ return recordItem
1644
+ }
1645
+ * const card: Card = createEmptyCard(new Date(), cardAfterHandler); //see method: createEmptyCard
1646
+ * const f = fsrs();
1647
+ * const recordLogItem = f.repeat(card, new Date(), Rating.Again, nextAfterHandler);
1648
+ * ```
1649
+ */
1650
+ next(card, now, grade, afterHandler) {
1651
+ const instance = this.getScheduler(card, now);
1652
+ const g = TypeConvert.rating(grade);
1653
+ if (g === Rating.Manual) {
1654
+ throw new Error("Cannot review a manual rating");
1655
+ }
1656
+ const recordLogItem = instance.review(g);
1657
+ if (afterHandler && typeof afterHandler === "function") {
1658
+ return afterHandler(recordLogItem);
1659
+ } else {
1660
+ return recordLogItem;
1661
+ }
1662
+ }
1663
+ /**
1664
+ * Get the retrievability of the card
1665
+ * @param card Card to be processed
1666
+ * @param now Current time or scheduled time
1667
+ * @param format default:true , Convert the result to another type. (Optional)
1668
+ * @returns The retrievability of the card,if format is true, the result is a string, otherwise it is a number
1669
+ */
1670
+ get_retrievability(card, now, format = true) {
1671
+ const processedCard = TypeConvert.card(card);
1672
+ now = now ? TypeConvert.time(now) : /* @__PURE__ */ new Date();
1673
+ const t = processedCard.state !== State.New ? Math.max(date_diff(now, processedCard.last_review, "days"), 0) : 0;
1674
+ const r = processedCard.state !== State.New ? this.forgetting_curve(t, +processedCard.stability.toFixed(8)) : 0;
1675
+ return format ? `${(r * 100).toFixed(2)}%` : r;
1676
+ }
1677
+ /**
1678
+ *
1679
+ * @param card Card to be processed
1680
+ * @param log last review log
1681
+ * @param afterHandler Convert the result to another type. (Optional)
1682
+ * @example
1683
+ * ```typescript
1684
+ * const now = new Date();
1685
+ * const f = fsrs();
1686
+ * const emptyCardFormAfterHandler = createEmptyCard(now);
1687
+ * const repeatFormAfterHandler = f.repeat(emptyCardFormAfterHandler, now);
1688
+ * const { card, log } = repeatFormAfterHandler[Rating.Hard];
1689
+ * const rollbackFromAfterHandler = f.rollback(card, log);
1690
+ * ```
1691
+ *
1692
+ * @example
1693
+ * ```typescript
1694
+ * const now = new Date();
1695
+ * const f = fsrs();
1696
+ * const emptyCardFormAfterHandler = createEmptyCard(now, cardAfterHandler); //see method: createEmptyCard
1697
+ * const repeatFormAfterHandler = f.repeat(emptyCardFormAfterHandler, now, repeatAfterHandler); //see method: fsrs.repeat()
1698
+ * const { card, log } = repeatFormAfterHandler[Rating.Hard];
1699
+ * const rollbackFromAfterHandler = f.rollback(card, log, cardAfterHandler);
1700
+ * ```
1701
+ */
1702
+ rollback(card, log, afterHandler) {
1703
+ const processedCard = TypeConvert.card(card);
1704
+ const processedLog = TypeConvert.review_log(log);
1705
+ if (processedLog.rating === Rating.Manual) {
1706
+ throw new Error("Cannot rollback a manual rating");
1707
+ }
1708
+ let last_due;
1709
+ let last_review;
1710
+ let last_lapses;
1711
+ switch (processedLog.state) {
1712
+ case State.New:
1713
+ last_due = processedLog.due;
1714
+ last_review = void 0;
1715
+ last_lapses = 0;
1716
+ break;
1717
+ case State.Learning:
1718
+ case State.Relearning:
1719
+ case State.Review:
1720
+ last_due = processedLog.review;
1721
+ last_review = processedLog.due;
1722
+ last_lapses = processedCard.lapses - (processedLog.rating === Rating.Again && processedLog.state === State.Review ? 1 : 0);
1723
+ break;
1724
+ }
1725
+ const prevCard = {
1726
+ ...processedCard,
1727
+ due: last_due,
1728
+ stability: processedLog.stability,
1729
+ difficulty: processedLog.difficulty,
1730
+ elapsed_days: processedLog.last_elapsed_days,
1731
+ scheduled_days: processedLog.scheduled_days,
1732
+ reps: Math.max(0, processedCard.reps - 1),
1733
+ lapses: Math.max(0, last_lapses),
1734
+ learning_steps: processedLog.learning_steps,
1735
+ state: processedLog.state,
1736
+ last_review
1737
+ };
1738
+ if (afterHandler && typeof afterHandler === "function") {
1739
+ return afterHandler(prevCard);
1740
+ } else {
1741
+ return prevCard;
1742
+ }
1743
+ }
1744
+ /**
1745
+ *
1746
+ * @param card Card to be processed
1747
+ * @param now Current time or scheduled time
1748
+ * @param reset_count Should the review count information(reps,lapses) be reset. (Optional)
1749
+ * @param afterHandler Convert the result to another type. (Optional)
1750
+ * @example
1751
+ * ```typescript
1752
+ * const now = new Date();
1753
+ * const f = fsrs();
1754
+ * const emptyCard = createEmptyCard(now);
1755
+ * const scheduling_cards = f.repeat(emptyCard, now);
1756
+ * const { card, log } = scheduling_cards[Rating.Hard];
1757
+ * const forgetCard = f.forget(card, new Date(), true);
1758
+ * ```
1759
+ *
1760
+ * @example
1761
+ * ```typescript
1762
+ * interface RepeatRecordLog {
1763
+ * card: CardUnChecked; //see method: createEmptyCard
1764
+ * log: RevLogUnchecked; //see method: fsrs.repeat()
1765
+ * }
1766
+ *
1767
+ * function forgetAfterHandler(recordLogItem: RecordLogItem): RepeatRecordLog {
1768
+ * return {
1769
+ * card: {
1770
+ * ...(recordLogItem.card as Card & { cid: string }),
1771
+ * due: recordLogItem.card.due.getTime(),
1772
+ * state: State[recordLogItem.card.state] as StateType,
1773
+ * last_review: recordLogItem.card.last_review
1774
+ * ? recordLogItem.card.last_review!.getTime()
1775
+ * : null,
1776
+ * },
1777
+ * log: {
1778
+ * ...recordLogItem.log,
1779
+ * cid: (recordLogItem.card as Card & { cid: string }).cid,
1780
+ * due: recordLogItem.log.due.getTime(),
1781
+ * review: recordLogItem.log.review.getTime(),
1782
+ * state: State[recordLogItem.log.state] as StateType,
1783
+ * rating: Rating[recordLogItem.log.rating] as RatingType,
1784
+ * },
1785
+ * };
1786
+ * }
1787
+ * const now = new Date();
1788
+ * const f = fsrs();
1789
+ * const emptyCardFormAfterHandler = createEmptyCard(now, cardAfterHandler); //see method: createEmptyCard
1790
+ * const repeatFormAfterHandler = f.repeat(emptyCardFormAfterHandler, now, repeatAfterHandler); //see method: fsrs.repeat()
1791
+ * const { card } = repeatFormAfterHandler[Rating.Hard];
1792
+ * const forgetFromAfterHandler = f.forget(card, date_scheduler(now, 1, true), false, forgetAfterHandler);
1793
+ * ```
1794
+ */
1795
+ forget(card, now, reset_count = false, afterHandler) {
1796
+ const processedCard = TypeConvert.card(card);
1797
+ now = TypeConvert.time(now);
1798
+ const scheduled_days = processedCard.state === State.New ? 0 : date_diff(now, processedCard.due, "days");
1799
+ const forget_log = {
1800
+ rating: Rating.Manual,
1801
+ state: processedCard.state,
1802
+ due: processedCard.due,
1803
+ stability: processedCard.stability,
1804
+ difficulty: processedCard.difficulty,
1805
+ elapsed_days: 0,
1806
+ last_elapsed_days: processedCard.elapsed_days,
1807
+ scheduled_days,
1808
+ learning_steps: processedCard.learning_steps,
1809
+ review: now
1810
+ };
1811
+ const forget_card = {
1812
+ ...processedCard,
1813
+ due: now,
1814
+ stability: 0,
1815
+ difficulty: 0,
1816
+ elapsed_days: 0,
1817
+ scheduled_days: 0,
1818
+ reps: reset_count ? 0 : processedCard.reps,
1819
+ lapses: reset_count ? 0 : processedCard.lapses,
1820
+ learning_steps: 0,
1821
+ state: State.New,
1822
+ last_review: processedCard.last_review
1823
+ };
1824
+ const recordLogItem = { card: forget_card, log: forget_log };
1825
+ if (afterHandler && typeof afterHandler === "function") {
1826
+ return afterHandler(recordLogItem);
1827
+ } else {
1828
+ return recordLogItem;
1829
+ }
1830
+ }
1831
+ /**
1832
+ * Reschedules the current card and returns the rescheduled collections and reschedule item.
1833
+ *
1834
+ * @template T - The type of the record log item.
1835
+ * @param {CardInput | Card} current_card - The current card to be rescheduled.
1836
+ * @param {Array<FSRSHistory>} reviews - The array of FSRSHistory objects representing the reviews.
1837
+ * @param {Partial<RescheduleOptions<T>>} options - The optional reschedule options.
1838
+ * @returns {IReschedule<T>} - The rescheduled collections and reschedule item.
1839
+ *
1840
+ * @example
1841
+ * ```typescript
1842
+ * const f = fsrs()
1843
+ * const grades: Grade[] = [Rating.Good, Rating.Good, Rating.Good, Rating.Good]
1844
+ * const reviews_at = [
1845
+ * new Date(2024, 8, 13),
1846
+ * new Date(2024, 8, 13),
1847
+ * new Date(2024, 8, 17),
1848
+ * new Date(2024, 8, 28),
1849
+ * ]
1850
+ *
1851
+ * const reviews: FSRSHistory[] = []
1852
+ * for (let i = 0; i < grades.length; i++) {
1853
+ * reviews.push({
1854
+ * rating: grades[i],
1855
+ * review: reviews_at[i],
1856
+ * })
1857
+ * }
1858
+ *
1859
+ * const results_short = scheduler.reschedule(
1860
+ * createEmptyCard(),
1861
+ * reviews,
1862
+ * {
1863
+ * skipManual: false,
1864
+ * }
1865
+ * )
1866
+ * console.log(results_short)
1867
+ * ```
1868
+ */
1869
+ reschedule(current_card, reviews = [], options = {}) {
1870
+ const {
1871
+ recordLogHandler,
1872
+ reviewsOrderBy,
1873
+ skipManual = true,
1874
+ now = /* @__PURE__ */ new Date(),
1875
+ update_memory_state: updateMemoryState = false
1876
+ } = options;
1877
+ if (reviewsOrderBy && typeof reviewsOrderBy === "function") {
1878
+ reviews.sort(reviewsOrderBy);
1879
+ }
1880
+ if (skipManual) {
1881
+ reviews = reviews.filter((review) => review.rating !== Rating.Manual);
1882
+ }
1883
+ const rescheduleSvc = new Reschedule(this);
1884
+ const collections = rescheduleSvc.reschedule(
1885
+ options.first_card || createEmptyCard(),
1886
+ reviews
1887
+ );
1888
+ const len = collections.length;
1889
+ const cur_card = TypeConvert.card(current_card);
1890
+ const manual_item = rescheduleSvc.calculateManualRecord(
1891
+ cur_card,
1892
+ now,
1893
+ len ? collections[len - 1] : void 0,
1894
+ updateMemoryState
1895
+ );
1896
+ if (recordLogHandler && typeof recordLogHandler === "function") {
1897
+ return {
1898
+ collections: collections.map(recordLogHandler),
1899
+ reschedule_item: manual_item ? recordLogHandler(manual_item) : null
1900
+ };
1901
+ }
1902
+ return {
1903
+ collections,
1904
+ reschedule_item: manual_item
1905
+ };
1906
+ }
1907
+ }
1908
+ const fsrs = (params) => {
1909
+ return new FSRS(params || {});
1910
+ };
1911
+
1912
+ export { AbstractScheduler, BasicLearningStepsStrategy, CLAMP_PARAMETERS, ConvertStepUnitToMinutes, DefaultInitSeedStrategy, FSRS, FSRS5_DEFAULT_DECAY, FSRS6_DEFAULT_DECAY, FSRSAlgorithm, FSRSVersion, GenSeedStrategyWithCardId, Grades, INIT_S_MAX, Rating, S_MAX, S_MIN, State, StrategyMode, TypeConvert, W17_W18_Ceiling, checkParameters, clamp, clipParameters, computeDecayFactor, createEmptyCard, dateDiffInDays, date_diff, date_scheduler, default_enable_fuzz, default_enable_short_term, default_learning_steps, default_maximum_interval, default_relearning_steps, default_request_retention, default_w, fixDate, fixRating, fixState, forgetting_curve, formatDate, fsrs, generatorParameters, get_fuzz_range, migrateParameters, show_diff_message };
2
1913
  //# sourceMappingURL=index.mjs.map