ts-fsrs 5.2.0 → 5.2.2

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,1940 @@
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.2";
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, enable_short_term = default_enable_short_term) => [
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
+ [
533
+ enable_short_term ? 0.01 : 0,
534
+ 0.8
535
+ ],
536
+ [0.1, 0.8]
537
+ ];
538
+
539
+ const clipParameters = (parameters, numRelearningSteps, enableShortTerm = default_enable_short_term) => {
540
+ let w17_w18_ceiling = W17_W18_Ceiling;
541
+ if (Math.max(0, numRelearningSteps) > 1) {
542
+ const value = -(Math.log(parameters[11]) + Math.log(Math.pow(2, parameters[13]) - 1) + parameters[14] * 0.3) / numRelearningSteps;
543
+ w17_w18_ceiling = clamp(+value.toFixed(8), 0.01, 2);
544
+ }
545
+ const clip = CLAMP_PARAMETERS(w17_w18_ceiling, enableShortTerm).slice(
546
+ 0,
547
+ parameters.length
548
+ );
549
+ return clip.map(
550
+ ([min, max], index) => clamp(parameters[index] || 0, min, max)
551
+ );
552
+ };
553
+ const checkParameters = (parameters) => {
554
+ const invalid = parameters.find(
555
+ (param) => !Number.isFinite(param) && !Number.isNaN(param)
556
+ );
557
+ if (invalid !== void 0) {
558
+ throw Error(`Non-finite or NaN value in parameters ${parameters}`);
559
+ } else if (![17, 19, 21].includes(parameters.length)) {
560
+ throw Error(
561
+ `Invalid parameter length: ${parameters.length}. Must be 17, 19 or 21 for FSRSv4, 5 and 6 respectively.`
562
+ );
563
+ }
564
+ return parameters;
565
+ };
566
+ const migrateParameters = (parameters, numRelearningSteps = 0, enableShortTerm = default_enable_short_term) => {
567
+ if (parameters === void 0) {
568
+ return [...default_w];
569
+ }
570
+ switch (parameters.length) {
571
+ case 21:
572
+ return clipParameters(
573
+ Array.from(parameters),
574
+ numRelearningSteps,
575
+ enableShortTerm
576
+ );
577
+ case 19:
578
+ console.debug("[FSRS-6]auto fill w from 19 to 21 length");
579
+ return clipParameters(
580
+ Array.from(parameters),
581
+ numRelearningSteps,
582
+ enableShortTerm
583
+ ).concat([0, FSRS5_DEFAULT_DECAY]);
584
+ case 17: {
585
+ const w = clipParameters(
586
+ Array.from(parameters),
587
+ numRelearningSteps,
588
+ enableShortTerm
589
+ );
590
+ w[4] = +(w[5] * 2 + w[4]).toFixed(8);
591
+ w[5] = +(Math.log(w[5] * 3 + 1) / 3).toFixed(8);
592
+ w[6] = +(w[6] + 0.5).toFixed(8);
593
+ console.debug("[FSRS-6]auto fill w from 17 to 21 length");
594
+ return w.concat([0, 0, 0, FSRS5_DEFAULT_DECAY]);
595
+ }
596
+ default:
597
+ console.warn("[FSRS]Invalid parameters length, using default parameters");
598
+ return [...default_w];
599
+ }
600
+ };
601
+ const generatorParameters = (props) => {
602
+ const learning_steps = Array.isArray(props?.learning_steps) ? props.learning_steps : default_learning_steps;
603
+ const relearning_steps = Array.isArray(props?.relearning_steps) ? props.relearning_steps : default_relearning_steps;
604
+ const enable_short_term = props?.enable_short_term ?? default_enable_short_term;
605
+ const w = migrateParameters(
606
+ props?.w,
607
+ relearning_steps.length,
608
+ enable_short_term
609
+ );
610
+ return {
611
+ request_retention: props?.request_retention || default_request_retention,
612
+ maximum_interval: props?.maximum_interval || default_maximum_interval,
613
+ w,
614
+ enable_fuzz: props?.enable_fuzz ?? default_enable_fuzz,
615
+ enable_short_term,
616
+ learning_steps,
617
+ relearning_steps
618
+ };
619
+ };
620
+ function createEmptyCard(now, afterHandler) {
621
+ const emptyCard = {
622
+ due: now ? TypeConvert.time(now) : /* @__PURE__ */ new Date(),
623
+ stability: 0,
624
+ difficulty: 0,
625
+ elapsed_days: 0,
626
+ scheduled_days: 0,
627
+ reps: 0,
628
+ lapses: 0,
629
+ learning_steps: 0,
630
+ state: State.New,
631
+ last_review: void 0
632
+ };
633
+ if (afterHandler && typeof afterHandler === "function") {
634
+ return afterHandler(emptyCard);
635
+ } else {
636
+ return emptyCard;
637
+ }
638
+ }
639
+
640
+ const computeDecayFactor = (decayOrParams) => {
641
+ const decay = typeof decayOrParams === "number" ? -decayOrParams : -decayOrParams[20];
642
+ const factor = Math.exp(Math.pow(decay, -1) * Math.log(0.9)) - 1;
643
+ return { decay, factor: +factor.toFixed(8) };
644
+ };
645
+ function forgetting_curve(decayOrParams, elapsed_days, stability) {
646
+ const { decay, factor } = computeDecayFactor(decayOrParams);
647
+ return +Math.pow(1 + factor * elapsed_days / stability, decay).toFixed(8);
648
+ }
649
+ class FSRSAlgorithm {
650
+ param;
651
+ intervalModifier;
652
+ _seed;
653
+ constructor(params) {
654
+ this.param = new Proxy(
655
+ generatorParameters(params),
656
+ this.params_handler_proxy()
657
+ );
658
+ this.intervalModifier = this.calculate_interval_modifier(
659
+ this.param.request_retention
660
+ );
661
+ this.forgetting_curve = forgetting_curve.bind(this, this.param.w);
662
+ }
663
+ get interval_modifier() {
664
+ return this.intervalModifier;
665
+ }
666
+ set seed(seed) {
667
+ this._seed = seed;
668
+ }
669
+ /**
670
+ * @see https://github.com/open-spaced-repetition/fsrs4anki/wiki/The-Algorithm#fsrs-5
671
+ *
672
+ * The formula used is: $$I(r,s) = (r^{\frac{1}{DECAY}} - 1) / FACTOR \times s$$
673
+ * @param request_retention 0<request_retention<=1,Requested retention rate
674
+ * @throws {Error} Requested retention rate should be in the range (0,1]
675
+ */
676
+ calculate_interval_modifier(request_retention) {
677
+ if (request_retention <= 0 || request_retention > 1) {
678
+ throw new Error("Requested retention rate should be in the range (0,1]");
679
+ }
680
+ const { decay, factor } = computeDecayFactor(this.param.w);
681
+ return +((Math.pow(request_retention, 1 / decay) - 1) / factor).toFixed(8);
682
+ }
683
+ /**
684
+ * Get the parameters of the algorithm.
685
+ */
686
+ get parameters() {
687
+ return this.param;
688
+ }
689
+ /**
690
+ * Set the parameters of the algorithm.
691
+ * @param params Partial<FSRSParameters>
692
+ */
693
+ set parameters(params) {
694
+ this.update_parameters(params);
695
+ }
696
+ params_handler_proxy() {
697
+ const _this = this;
698
+ return {
699
+ set: function(target, prop, value) {
700
+ if (prop === "request_retention" && Number.isFinite(value)) {
701
+ _this.intervalModifier = _this.calculate_interval_modifier(
702
+ Number(value)
703
+ );
704
+ } else if (prop === "w") {
705
+ value = migrateParameters(
706
+ value,
707
+ target.relearning_steps.length,
708
+ target.enable_short_term
709
+ );
710
+ _this.forgetting_curve = forgetting_curve.bind(this, value);
711
+ _this.intervalModifier = _this.calculate_interval_modifier(
712
+ Number(target.request_retention)
713
+ );
714
+ }
715
+ Reflect.set(target, prop, value);
716
+ return true;
717
+ }
718
+ };
719
+ }
720
+ update_parameters(params) {
721
+ const _params = generatorParameters(params);
722
+ for (const key in _params) {
723
+ const paramKey = key;
724
+ this.param[paramKey] = _params[paramKey];
725
+ }
726
+ }
727
+ /**
728
+ * The formula used is :
729
+ * $$ S_0(G) = w_{G-1}$$
730
+ * $$S_0 = \max \lbrace S_0,0.1\rbrace $$
731
+
732
+ * @param g Grade (rating at Anki) [1.again,2.hard,3.good,4.easy]
733
+ * @return Stability (interval when R=90%)
734
+ */
735
+ init_stability(g) {
736
+ return Math.max(this.param.w[g - 1], 0.1);
737
+ }
738
+ /**
739
+ * The formula used is :
740
+ * $$D_0(G) = w_4 - e^{(G-1) \cdot w_5} + 1 $$
741
+ * $$D_0 = \min \lbrace \max \lbrace D_0(G),1 \rbrace,10 \rbrace$$
742
+ * where the $$D_0(1)=w_4$$ when the first rating is good.
743
+ *
744
+ * @param {Grade} g Grade (rating at Anki) [1.again,2.hard,3.good,4.easy]
745
+ * @return {number} Difficulty $$D \in [1,10]$$
746
+ */
747
+ init_difficulty(g) {
748
+ const d = this.param.w[4] - Math.exp((g - 1) * this.param.w[5]) + 1;
749
+ return +d.toFixed(8);
750
+ }
751
+ /**
752
+ * If fuzzing is disabled or ivl is less than 2.5, it returns the original interval.
753
+ * @param {number} ivl - The interval to be fuzzed.
754
+ * @param {number} elapsed_days t days since the last review
755
+ * @return {number} - The fuzzed interval.
756
+ **/
757
+ apply_fuzz(ivl, elapsed_days) {
758
+ if (!this.param.enable_fuzz || ivl < 2.5) return Math.round(ivl);
759
+ const generator = alea(this._seed);
760
+ const fuzz_factor = generator();
761
+ const { min_ivl, max_ivl } = get_fuzz_range(
762
+ ivl,
763
+ elapsed_days,
764
+ this.param.maximum_interval
765
+ );
766
+ return Math.floor(fuzz_factor * (max_ivl - min_ivl + 1) + min_ivl);
767
+ }
768
+ /**
769
+ * @see The formula used is : {@link FSRSAlgorithm.calculate_interval_modifier}
770
+ * @param {number} s - Stability (interval when R=90%)
771
+ * @param {number} elapsed_days t days since the last review
772
+ */
773
+ next_interval(s, elapsed_days) {
774
+ const newInterval = Math.min(
775
+ Math.max(1, Math.round(s * this.intervalModifier)),
776
+ this.param.maximum_interval
777
+ );
778
+ return this.apply_fuzz(newInterval, elapsed_days);
779
+ }
780
+ /**
781
+ * @see https://github.com/open-spaced-repetition/fsrs4anki/issues/697
782
+ */
783
+ linear_damping(delta_d, old_d) {
784
+ return +(delta_d * (10 - old_d) / 9).toFixed(8);
785
+ }
786
+ /**
787
+ * The formula used is :
788
+ * $$\text{delta}_d = -w_6 \cdot (g - 3)$$
789
+ * $$\text{next}_d = D + \text{linear damping}(\text{delta}_d , D)$$
790
+ * $$D^\prime(D,R) = w_7 \cdot D_0(4) +(1 - w_7) \cdot \text{next}_d$$
791
+ * @param {number} d Difficulty $$D \in [1,10]$$
792
+ * @param {Grade} g Grade (rating at Anki) [1.again,2.hard,3.good,4.easy]
793
+ * @return {number} $$\text{next}_D$$
794
+ */
795
+ next_difficulty(d, g) {
796
+ const delta_d = -this.param.w[6] * (g - 3);
797
+ const next_d = d + this.linear_damping(delta_d, d);
798
+ return clamp(
799
+ this.mean_reversion(this.init_difficulty(Rating.Easy), next_d),
800
+ 1,
801
+ 10
802
+ );
803
+ }
804
+ /**
805
+ * The formula used is :
806
+ * $$w_7 \cdot \text{init} +(1 - w_7) \cdot \text{current}$$
807
+ * @param {number} init $$w_2 : D_0(3) = w_2 + (R-2) \cdot w_3= w_2$$
808
+ * @param {number} current $$D - w_6 \cdot (R - 2)$$
809
+ * @return {number} difficulty
810
+ */
811
+ mean_reversion(init, current) {
812
+ return +(this.param.w[7] * init + (1 - this.param.w[7]) * current).toFixed(
813
+ 8
814
+ );
815
+ }
816
+ /**
817
+ * The formula used is :
818
+ * $$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)$$
819
+ * @param {number} d Difficulty D \in [1,10]
820
+ * @param {number} s Stability (interval when R=90%)
821
+ * @param {number} r Retrievability (probability of recall)
822
+ * @param {Grade} g Grade (Rating[0.again,1.hard,2.good,3.easy])
823
+ * @return {number} S^\prime_r new stability after recall
824
+ */
825
+ next_recall_stability(d, s, r, g) {
826
+ const hard_penalty = Rating.Hard === g ? this.param.w[15] : 1;
827
+ const easy_bound = Rating.Easy === g ? this.param.w[16] : 1;
828
+ return +clamp(
829
+ 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),
830
+ S_MIN,
831
+ 36500
832
+ ).toFixed(8);
833
+ }
834
+ /**
835
+ * The formula used is :
836
+ * $$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)}$$
837
+ * 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$$
838
+ * enable_short_term = false : $$S^\prime_f \in \min \lbrace \max \lbrace S^\prime_f,0.01\rbrace, S \rbrace$$
839
+ * @param {number} d Difficulty D \in [1,10]
840
+ * @param {number} s Stability (interval when R=90%)
841
+ * @param {number} r Retrievability (probability of recall)
842
+ * @return {number} S^\prime_f new stability after forgetting
843
+ */
844
+ next_forget_stability(d, s, r) {
845
+ return +clamp(
846
+ 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]),
847
+ S_MIN,
848
+ 36500
849
+ ).toFixed(8);
850
+ }
851
+ /**
852
+ * The formula used is :
853
+ * $$S^\prime_s(S,G) = S \cdot e^{w_{17} \cdot (G-3+w_{18})}$$
854
+ * @param {number} s Stability (interval when R=90%)
855
+ * @param {Grade} g Grade (Rating[0.again,1.hard,2.good,3.easy])
856
+ */
857
+ next_short_term_stability(s, g) {
858
+ const sinc = Math.pow(s, -this.param.w[19]) * Math.exp(this.param.w[17] * (g - 3 + this.param.w[18]));
859
+ const maskedSinc = g >= 3 ? Math.max(sinc, 1) : sinc;
860
+ return +clamp(s * maskedSinc, S_MIN, 36500).toFixed(8);
861
+ }
862
+ /**
863
+ * The formula used is :
864
+ * $$R(t,S) = (1 + \text{FACTOR} \times \frac{t}{9 \cdot S})^{\text{DECAY}}$$
865
+ * @param {number} elapsed_days t days since the last review
866
+ * @param {number} stability Stability (interval when R=90%)
867
+ * @return {number} r Retrievability (probability of recall)
868
+ */
869
+ forgetting_curve;
870
+ /**
871
+ * Calculates the next state of memory based on the current state, time elapsed, and grade.
872
+ *
873
+ * @param memory_state - The current state of memory, which can be null.
874
+ * @param t - The time elapsed since the last review.
875
+ * @param {Rating} g Grade (Rating[0.Manual,1.Again,2.Hard,3.Good,4.Easy])
876
+ * @returns The next state of memory with updated difficulty and stability.
877
+ */
878
+ next_state(memory_state, t, g) {
879
+ const { difficulty: d, stability: s } = memory_state ?? {
880
+ difficulty: 0,
881
+ stability: 0
882
+ };
883
+ if (t < 0) {
884
+ throw new Error(`Invalid delta_t "${t}"`);
885
+ }
886
+ if (g < 0 || g > 4) {
887
+ throw new Error(`Invalid grade "${g}"`);
888
+ }
889
+ if (d === 0 && s === 0) {
890
+ return {
891
+ difficulty: clamp(this.init_difficulty(g), 1, 10),
892
+ stability: this.init_stability(g)
893
+ };
894
+ }
895
+ if (g === 0) {
896
+ return {
897
+ difficulty: d,
898
+ stability: s
899
+ };
900
+ }
901
+ if (d < 1 || s < S_MIN) {
902
+ throw new Error(
903
+ `Invalid memory state { difficulty: ${d}, stability: ${s} }`
904
+ );
905
+ }
906
+ const r = this.forgetting_curve(t, s);
907
+ const s_after_success = this.next_recall_stability(d, s, r, g);
908
+ const s_after_fail = this.next_forget_stability(d, s, r);
909
+ const s_after_short_term = this.next_short_term_stability(s, g);
910
+ let new_s = s_after_success;
911
+ if (g === 1) {
912
+ let [w_17, w_18] = [0, 0];
913
+ if (this.param.enable_short_term) {
914
+ w_17 = this.param.w[17];
915
+ w_18 = this.param.w[18];
916
+ }
917
+ const next_s_min = s / Math.exp(w_17 * w_18);
918
+ new_s = clamp(+next_s_min.toFixed(8), S_MIN, s_after_fail);
919
+ }
920
+ if (t === 0 && this.param.enable_short_term) {
921
+ new_s = s_after_short_term;
922
+ }
923
+ const new_d = this.next_difficulty(d, g);
924
+ return { difficulty: new_d, stability: new_s };
925
+ }
926
+ }
927
+
928
+ class BasicScheduler extends AbstractScheduler {
929
+ learningStepsStrategy;
930
+ constructor(card, now, algorithm, strategies) {
931
+ super(card, now, algorithm, strategies);
932
+ let learningStepStrategy = BasicLearningStepsStrategy;
933
+ if (this.strategies) {
934
+ const custom_strategy = this.strategies.get(StrategyMode.LEARNING_STEPS);
935
+ if (custom_strategy) {
936
+ learningStepStrategy = custom_strategy;
937
+ }
938
+ }
939
+ this.learningStepsStrategy = learningStepStrategy;
940
+ }
941
+ getLearningInfo(card, grade) {
942
+ const parameters = this.algorithm.parameters;
943
+ card.learning_steps = card.learning_steps || 0;
944
+ const steps_strategy = this.learningStepsStrategy(
945
+ parameters,
946
+ card.state,
947
+ // In the original learning steps setup (Again = 5m, Hard = 10m, Good = FSRS),
948
+ // not adding 1 can cause slight variations in the memory state’s ds.
949
+ this.current.state === State.Learning && grade !== Rating.Again && grade !== Rating.Hard ? card.learning_steps + 1 : card.learning_steps
950
+ );
951
+ const scheduled_minutes = Math.max(
952
+ 0,
953
+ steps_strategy[grade]?.scheduled_minutes ?? 0
954
+ );
955
+ const next_steps = Math.max(0, steps_strategy[grade]?.next_step ?? 0);
956
+ return {
957
+ scheduled_minutes,
958
+ next_steps
959
+ };
960
+ }
961
+ /**
962
+ * @description This function applies the learning steps based on the current card's state and grade.
963
+ */
964
+ applyLearningSteps(nextCard, grade, to_state) {
965
+ const { scheduled_minutes, next_steps } = this.getLearningInfo(
966
+ this.current,
967
+ grade
968
+ );
969
+ if (scheduled_minutes > 0 && scheduled_minutes < 1440) {
970
+ nextCard.learning_steps = next_steps;
971
+ nextCard.scheduled_days = 0;
972
+ nextCard.state = to_state;
973
+ nextCard.due = date_scheduler(
974
+ this.review_time,
975
+ Math.round(scheduled_minutes),
976
+ false
977
+ /** true:days false: minute */
978
+ );
979
+ } else {
980
+ nextCard.state = State.Review;
981
+ if (scheduled_minutes >= 1440) {
982
+ nextCard.learning_steps = next_steps;
983
+ nextCard.due = date_scheduler(
984
+ this.review_time,
985
+ Math.round(scheduled_minutes),
986
+ false
987
+ /** true:days false: minute */
988
+ );
989
+ nextCard.scheduled_days = Math.floor(scheduled_minutes / 1440);
990
+ } else {
991
+ nextCard.learning_steps = 0;
992
+ const interval = this.algorithm.next_interval(
993
+ nextCard.stability,
994
+ this.elapsed_days
995
+ );
996
+ nextCard.scheduled_days = interval;
997
+ nextCard.due = date_scheduler(this.review_time, interval, true);
998
+ }
999
+ }
1000
+ }
1001
+ newState(grade) {
1002
+ const exist = this.next.get(grade);
1003
+ if (exist) {
1004
+ return exist;
1005
+ }
1006
+ const next = TypeConvert.card(this.current);
1007
+ next.difficulty = clamp(this.algorithm.init_difficulty(grade), 1, 10);
1008
+ next.stability = this.algorithm.init_stability(grade);
1009
+ this.applyLearningSteps(next, grade, State.Learning);
1010
+ const item = {
1011
+ card: next,
1012
+ log: this.buildLog(grade)
1013
+ };
1014
+ this.next.set(grade, item);
1015
+ return item;
1016
+ }
1017
+ learningState(grade) {
1018
+ const exist = this.next.get(grade);
1019
+ if (exist) {
1020
+ return exist;
1021
+ }
1022
+ const { state, difficulty, stability } = this.last;
1023
+ const next = TypeConvert.card(this.current);
1024
+ next.difficulty = this.algorithm.next_difficulty(difficulty, grade);
1025
+ next.stability = this.algorithm.next_short_term_stability(stability, grade);
1026
+ this.applyLearningSteps(
1027
+ next,
1028
+ grade,
1029
+ state
1030
+ /** Learning or Relearning */
1031
+ );
1032
+ const item = {
1033
+ card: next,
1034
+ log: this.buildLog(grade)
1035
+ };
1036
+ this.next.set(grade, item);
1037
+ return item;
1038
+ }
1039
+ reviewState(grade) {
1040
+ const exist = this.next.get(grade);
1041
+ if (exist) {
1042
+ return exist;
1043
+ }
1044
+ const interval = this.elapsed_days;
1045
+ const { difficulty, stability } = this.last;
1046
+ const retrievability = this.algorithm.forgetting_curve(interval, stability);
1047
+ const next_again = TypeConvert.card(this.current);
1048
+ const next_hard = TypeConvert.card(this.current);
1049
+ const next_good = TypeConvert.card(this.current);
1050
+ const next_easy = TypeConvert.card(this.current);
1051
+ this.next_ds(
1052
+ next_again,
1053
+ next_hard,
1054
+ next_good,
1055
+ next_easy,
1056
+ difficulty,
1057
+ stability,
1058
+ retrievability
1059
+ );
1060
+ this.next_interval(next_hard, next_good, next_easy, interval);
1061
+ this.next_state(next_hard, next_good, next_easy);
1062
+ this.applyLearningSteps(next_again, Rating.Again, State.Relearning);
1063
+ next_again.lapses += 1;
1064
+ const item_again = {
1065
+ card: next_again,
1066
+ log: this.buildLog(Rating.Again)
1067
+ };
1068
+ const item_hard = {
1069
+ card: next_hard,
1070
+ log: super.buildLog(Rating.Hard)
1071
+ };
1072
+ const item_good = {
1073
+ card: next_good,
1074
+ log: super.buildLog(Rating.Good)
1075
+ };
1076
+ const item_easy = {
1077
+ card: next_easy,
1078
+ log: super.buildLog(Rating.Easy)
1079
+ };
1080
+ this.next.set(Rating.Again, item_again);
1081
+ this.next.set(Rating.Hard, item_hard);
1082
+ this.next.set(Rating.Good, item_good);
1083
+ this.next.set(Rating.Easy, item_easy);
1084
+ return this.next.get(grade);
1085
+ }
1086
+ /**
1087
+ * Review next_ds
1088
+ */
1089
+ next_ds(next_again, next_hard, next_good, next_easy, difficulty, stability, retrievability) {
1090
+ next_again.difficulty = this.algorithm.next_difficulty(
1091
+ difficulty,
1092
+ Rating.Again
1093
+ );
1094
+ const nextSMin = stability / Math.exp(
1095
+ this.algorithm.parameters.w[17] * this.algorithm.parameters.w[18]
1096
+ );
1097
+ const s_after_fail = this.algorithm.next_forget_stability(
1098
+ difficulty,
1099
+ stability,
1100
+ retrievability
1101
+ );
1102
+ next_again.stability = clamp(+nextSMin.toFixed(8), S_MIN, s_after_fail);
1103
+ next_hard.difficulty = this.algorithm.next_difficulty(
1104
+ difficulty,
1105
+ Rating.Hard
1106
+ );
1107
+ next_hard.stability = this.algorithm.next_recall_stability(
1108
+ difficulty,
1109
+ stability,
1110
+ retrievability,
1111
+ Rating.Hard
1112
+ );
1113
+ next_good.difficulty = this.algorithm.next_difficulty(
1114
+ difficulty,
1115
+ Rating.Good
1116
+ );
1117
+ next_good.stability = this.algorithm.next_recall_stability(
1118
+ difficulty,
1119
+ stability,
1120
+ retrievability,
1121
+ Rating.Good
1122
+ );
1123
+ next_easy.difficulty = this.algorithm.next_difficulty(
1124
+ difficulty,
1125
+ Rating.Easy
1126
+ );
1127
+ next_easy.stability = this.algorithm.next_recall_stability(
1128
+ difficulty,
1129
+ stability,
1130
+ retrievability,
1131
+ Rating.Easy
1132
+ );
1133
+ }
1134
+ /**
1135
+ * Review next_interval
1136
+ */
1137
+ next_interval(next_hard, next_good, next_easy, interval) {
1138
+ let hard_interval, good_interval;
1139
+ hard_interval = this.algorithm.next_interval(next_hard.stability, interval);
1140
+ good_interval = this.algorithm.next_interval(next_good.stability, interval);
1141
+ hard_interval = Math.min(hard_interval, good_interval);
1142
+ good_interval = Math.max(good_interval, hard_interval + 1);
1143
+ const easy_interval = Math.max(
1144
+ this.algorithm.next_interval(next_easy.stability, interval),
1145
+ good_interval + 1
1146
+ );
1147
+ next_hard.scheduled_days = hard_interval;
1148
+ next_hard.due = date_scheduler(this.review_time, hard_interval, true);
1149
+ next_good.scheduled_days = good_interval;
1150
+ next_good.due = date_scheduler(this.review_time, good_interval, true);
1151
+ next_easy.scheduled_days = easy_interval;
1152
+ next_easy.due = date_scheduler(this.review_time, easy_interval, true);
1153
+ }
1154
+ /**
1155
+ * Review next_state
1156
+ */
1157
+ next_state(next_hard, next_good, next_easy) {
1158
+ next_hard.state = State.Review;
1159
+ next_hard.learning_steps = 0;
1160
+ next_good.state = State.Review;
1161
+ next_good.learning_steps = 0;
1162
+ next_easy.state = State.Review;
1163
+ next_easy.learning_steps = 0;
1164
+ }
1165
+ }
1166
+
1167
+ class LongTermScheduler extends AbstractScheduler {
1168
+ newState(grade) {
1169
+ const exist = this.next.get(grade);
1170
+ if (exist) {
1171
+ return exist;
1172
+ }
1173
+ this.current.scheduled_days = 0;
1174
+ this.current.elapsed_days = 0;
1175
+ const next_again = TypeConvert.card(this.current);
1176
+ const next_hard = TypeConvert.card(this.current);
1177
+ const next_good = TypeConvert.card(this.current);
1178
+ const next_easy = TypeConvert.card(this.current);
1179
+ this.init_ds(next_again, next_hard, next_good, next_easy);
1180
+ const first_interval = 0;
1181
+ this.next_interval(
1182
+ next_again,
1183
+ next_hard,
1184
+ next_good,
1185
+ next_easy,
1186
+ first_interval
1187
+ );
1188
+ this.next_state(next_again, next_hard, next_good, next_easy);
1189
+ this.update_next(next_again, next_hard, next_good, next_easy);
1190
+ return this.next.get(grade);
1191
+ }
1192
+ init_ds(next_again, next_hard, next_good, next_easy) {
1193
+ next_again.difficulty = clamp(
1194
+ this.algorithm.init_difficulty(Rating.Again),
1195
+ 1,
1196
+ 10
1197
+ );
1198
+ next_again.stability = this.algorithm.init_stability(Rating.Again);
1199
+ next_hard.difficulty = clamp(
1200
+ this.algorithm.init_difficulty(Rating.Hard),
1201
+ 1,
1202
+ 10
1203
+ );
1204
+ next_hard.stability = this.algorithm.init_stability(Rating.Hard);
1205
+ next_good.difficulty = clamp(
1206
+ this.algorithm.init_difficulty(Rating.Good),
1207
+ 1,
1208
+ 10
1209
+ );
1210
+ next_good.stability = this.algorithm.init_stability(Rating.Good);
1211
+ next_easy.difficulty = clamp(
1212
+ this.algorithm.init_difficulty(Rating.Easy),
1213
+ 1,
1214
+ 10
1215
+ );
1216
+ next_easy.stability = this.algorithm.init_stability(Rating.Easy);
1217
+ }
1218
+ /**
1219
+ * @see https://github.com/open-spaced-repetition/ts-fsrs/issues/98#issuecomment-2241923194
1220
+ */
1221
+ learningState(grade) {
1222
+ return this.reviewState(grade);
1223
+ }
1224
+ reviewState(grade) {
1225
+ const exist = this.next.get(grade);
1226
+ if (exist) {
1227
+ return exist;
1228
+ }
1229
+ const interval = this.elapsed_days;
1230
+ const { difficulty, stability } = this.last;
1231
+ const retrievability = this.algorithm.forgetting_curve(interval, stability);
1232
+ const next_again = TypeConvert.card(this.current);
1233
+ const next_hard = TypeConvert.card(this.current);
1234
+ const next_good = TypeConvert.card(this.current);
1235
+ const next_easy = TypeConvert.card(this.current);
1236
+ this.next_ds(
1237
+ next_again,
1238
+ next_hard,
1239
+ next_good,
1240
+ next_easy,
1241
+ difficulty,
1242
+ stability,
1243
+ retrievability
1244
+ );
1245
+ this.next_interval(next_again, next_hard, next_good, next_easy, interval);
1246
+ this.next_state(next_again, next_hard, next_good, next_easy);
1247
+ next_again.lapses += 1;
1248
+ this.update_next(next_again, next_hard, next_good, next_easy);
1249
+ return this.next.get(grade);
1250
+ }
1251
+ /**
1252
+ * Review next_ds
1253
+ */
1254
+ next_ds(next_again, next_hard, next_good, next_easy, difficulty, stability, retrievability) {
1255
+ next_again.difficulty = this.algorithm.next_difficulty(
1256
+ difficulty,
1257
+ Rating.Again
1258
+ );
1259
+ const s_after_fail = this.algorithm.next_forget_stability(
1260
+ difficulty,
1261
+ stability,
1262
+ retrievability
1263
+ );
1264
+ next_again.stability = clamp(stability, S_MIN, s_after_fail);
1265
+ next_hard.difficulty = this.algorithm.next_difficulty(
1266
+ difficulty,
1267
+ Rating.Hard
1268
+ );
1269
+ next_hard.stability = this.algorithm.next_recall_stability(
1270
+ difficulty,
1271
+ stability,
1272
+ retrievability,
1273
+ Rating.Hard
1274
+ );
1275
+ next_good.difficulty = this.algorithm.next_difficulty(
1276
+ difficulty,
1277
+ Rating.Good
1278
+ );
1279
+ next_good.stability = this.algorithm.next_recall_stability(
1280
+ difficulty,
1281
+ stability,
1282
+ retrievability,
1283
+ Rating.Good
1284
+ );
1285
+ next_easy.difficulty = this.algorithm.next_difficulty(
1286
+ difficulty,
1287
+ Rating.Easy
1288
+ );
1289
+ next_easy.stability = this.algorithm.next_recall_stability(
1290
+ difficulty,
1291
+ stability,
1292
+ retrievability,
1293
+ Rating.Easy
1294
+ );
1295
+ }
1296
+ /**
1297
+ * Review/New next_interval
1298
+ */
1299
+ next_interval(next_again, next_hard, next_good, next_easy, interval) {
1300
+ let again_interval, hard_interval, good_interval, easy_interval;
1301
+ again_interval = this.algorithm.next_interval(
1302
+ next_again.stability,
1303
+ interval
1304
+ );
1305
+ hard_interval = this.algorithm.next_interval(next_hard.stability, interval);
1306
+ good_interval = this.algorithm.next_interval(next_good.stability, interval);
1307
+ easy_interval = this.algorithm.next_interval(next_easy.stability, interval);
1308
+ again_interval = Math.min(again_interval, hard_interval);
1309
+ hard_interval = Math.max(hard_interval, again_interval + 1);
1310
+ good_interval = Math.max(good_interval, hard_interval + 1);
1311
+ easy_interval = Math.max(easy_interval, good_interval + 1);
1312
+ next_again.scheduled_days = again_interval;
1313
+ next_again.due = date_scheduler(this.review_time, again_interval, true);
1314
+ next_hard.scheduled_days = hard_interval;
1315
+ next_hard.due = date_scheduler(this.review_time, hard_interval, true);
1316
+ next_good.scheduled_days = good_interval;
1317
+ next_good.due = date_scheduler(this.review_time, good_interval, true);
1318
+ next_easy.scheduled_days = easy_interval;
1319
+ next_easy.due = date_scheduler(this.review_time, easy_interval, true);
1320
+ }
1321
+ /**
1322
+ * Review/New next_state
1323
+ */
1324
+ next_state(next_again, next_hard, next_good, next_easy) {
1325
+ next_again.state = State.Review;
1326
+ next_again.learning_steps = 0;
1327
+ next_hard.state = State.Review;
1328
+ next_hard.learning_steps = 0;
1329
+ next_good.state = State.Review;
1330
+ next_good.learning_steps = 0;
1331
+ next_easy.state = State.Review;
1332
+ next_easy.learning_steps = 0;
1333
+ }
1334
+ update_next(next_again, next_hard, next_good, next_easy) {
1335
+ const item_again = {
1336
+ card: next_again,
1337
+ log: this.buildLog(Rating.Again)
1338
+ };
1339
+ const item_hard = {
1340
+ card: next_hard,
1341
+ log: super.buildLog(Rating.Hard)
1342
+ };
1343
+ const item_good = {
1344
+ card: next_good,
1345
+ log: super.buildLog(Rating.Good)
1346
+ };
1347
+ const item_easy = {
1348
+ card: next_easy,
1349
+ log: super.buildLog(Rating.Easy)
1350
+ };
1351
+ this.next.set(Rating.Again, item_again);
1352
+ this.next.set(Rating.Hard, item_hard);
1353
+ this.next.set(Rating.Good, item_good);
1354
+ this.next.set(Rating.Easy, item_easy);
1355
+ }
1356
+ }
1357
+
1358
+ class Reschedule {
1359
+ fsrs;
1360
+ /**
1361
+ * Creates an instance of the `Reschedule` class.
1362
+ * @param fsrs - An instance of the FSRS class used for scheduling.
1363
+ */
1364
+ constructor(fsrs) {
1365
+ this.fsrs = fsrs;
1366
+ }
1367
+ /**
1368
+ * Replays a review for a card and determines the next review date based on the given rating.
1369
+ * @param card - The card being reviewed.
1370
+ * @param reviewed - The date the card was reviewed.
1371
+ * @param rating - The grade given to the card during the review.
1372
+ * @returns A `RecordLogItem` containing the updated card and review log.
1373
+ */
1374
+ replay(card, reviewed, rating) {
1375
+ return this.fsrs.next(card, reviewed, rating);
1376
+ }
1377
+ /**
1378
+ * Processes a manual review for a card, allowing for custom state, stability, difficulty, and due date.
1379
+ * @param card - The card being reviewed.
1380
+ * @param state - The state of the card after the review.
1381
+ * @param reviewed - The date the card was reviewed.
1382
+ * @param elapsed_days - The number of days since the last review.
1383
+ * @param stability - (Optional) The stability of the card.
1384
+ * @param difficulty - (Optional) The difficulty of the card.
1385
+ * @param due - (Optional) The due date for the next review.
1386
+ * @returns A `RecordLogItem` containing the updated card and review log.
1387
+ * @throws Will throw an error if the state or due date is not provided when required.
1388
+ */
1389
+ handleManualRating(card, state, reviewed, elapsed_days, stability, difficulty, due) {
1390
+ if (typeof state === "undefined") {
1391
+ throw new Error("reschedule: state is required for manual rating");
1392
+ }
1393
+ let log;
1394
+ let next_card;
1395
+ if (state === State.New) {
1396
+ log = {
1397
+ rating: Rating.Manual,
1398
+ state,
1399
+ due: due ?? reviewed,
1400
+ stability: card.stability,
1401
+ difficulty: card.difficulty,
1402
+ elapsed_days,
1403
+ last_elapsed_days: card.elapsed_days,
1404
+ scheduled_days: card.scheduled_days,
1405
+ learning_steps: card.learning_steps,
1406
+ review: reviewed
1407
+ };
1408
+ next_card = createEmptyCard(reviewed);
1409
+ next_card.last_review = reviewed;
1410
+ } else {
1411
+ if (typeof due === "undefined") {
1412
+ throw new Error("reschedule: due is required for manual rating");
1413
+ }
1414
+ const scheduled_days = date_diff(due, reviewed, "days");
1415
+ log = {
1416
+ rating: Rating.Manual,
1417
+ state: card.state,
1418
+ due: card.last_review || card.due,
1419
+ stability: card.stability,
1420
+ difficulty: card.difficulty,
1421
+ elapsed_days,
1422
+ last_elapsed_days: card.elapsed_days,
1423
+ scheduled_days: card.scheduled_days,
1424
+ learning_steps: card.learning_steps,
1425
+ review: reviewed
1426
+ };
1427
+ next_card = {
1428
+ ...card,
1429
+ state,
1430
+ due,
1431
+ last_review: reviewed,
1432
+ stability: stability || card.stability,
1433
+ difficulty: difficulty || card.difficulty,
1434
+ elapsed_days,
1435
+ scheduled_days,
1436
+ reps: card.reps + 1
1437
+ };
1438
+ }
1439
+ return { card: next_card, log };
1440
+ }
1441
+ /**
1442
+ * Reschedules a card based on its review history.
1443
+ *
1444
+ * @param current_card - The card to be rescheduled.
1445
+ * @param reviews - An array of review history objects.
1446
+ * @returns An array of record log items representing the rescheduling process.
1447
+ */
1448
+ reschedule(current_card, reviews) {
1449
+ const collections = [];
1450
+ let cur_card = createEmptyCard(current_card.due);
1451
+ for (const review of reviews) {
1452
+ let item;
1453
+ review.review = TypeConvert.time(review.review);
1454
+ if (review.rating === Rating.Manual) {
1455
+ let interval = 0;
1456
+ if (cur_card.state !== State.New && cur_card.last_review) {
1457
+ interval = date_diff(review.review, cur_card.last_review, "days");
1458
+ }
1459
+ item = this.handleManualRating(
1460
+ cur_card,
1461
+ review.state,
1462
+ review.review,
1463
+ interval,
1464
+ review.stability,
1465
+ review.difficulty,
1466
+ review.due ? TypeConvert.time(review.due) : void 0
1467
+ );
1468
+ } else {
1469
+ item = this.replay(cur_card, review.review, review.rating);
1470
+ }
1471
+ collections.push(item);
1472
+ cur_card = item.card;
1473
+ }
1474
+ return collections;
1475
+ }
1476
+ calculateManualRecord(current_card, now, record_log_item, update_memory) {
1477
+ if (!record_log_item) {
1478
+ return null;
1479
+ }
1480
+ const { card: reschedule_card, log } = record_log_item;
1481
+ const cur_card = TypeConvert.card(current_card);
1482
+ if (cur_card.due.getTime() === reschedule_card.due.getTime()) {
1483
+ return null;
1484
+ }
1485
+ cur_card.scheduled_days = date_diff(
1486
+ reschedule_card.due,
1487
+ cur_card.due,
1488
+ "days"
1489
+ );
1490
+ return this.handleManualRating(
1491
+ cur_card,
1492
+ reschedule_card.state,
1493
+ TypeConvert.time(now),
1494
+ log.elapsed_days,
1495
+ update_memory ? reschedule_card.stability : void 0,
1496
+ update_memory ? reschedule_card.difficulty : void 0,
1497
+ reschedule_card.due
1498
+ );
1499
+ }
1500
+ }
1501
+
1502
+ class FSRS extends FSRSAlgorithm {
1503
+ strategyHandler = /* @__PURE__ */ new Map();
1504
+ Scheduler;
1505
+ constructor(param) {
1506
+ super(param);
1507
+ const { enable_short_term } = this.parameters;
1508
+ this.Scheduler = enable_short_term ? BasicScheduler : LongTermScheduler;
1509
+ }
1510
+ params_handler_proxy() {
1511
+ const _this = this;
1512
+ return {
1513
+ set: function(target, prop, value) {
1514
+ if (prop === "request_retention" && Number.isFinite(value)) {
1515
+ _this.intervalModifier = _this.calculate_interval_modifier(
1516
+ Number(value)
1517
+ );
1518
+ } else if (prop === "enable_short_term") {
1519
+ _this.Scheduler = value === true ? BasicScheduler : LongTermScheduler;
1520
+ } else if (prop === "w") {
1521
+ value = migrateParameters(
1522
+ value,
1523
+ target.relearning_steps.length,
1524
+ target.enable_short_term
1525
+ );
1526
+ _this.forgetting_curve = forgetting_curve.bind(this, value);
1527
+ _this.intervalModifier = _this.calculate_interval_modifier(
1528
+ Number(target.request_retention)
1529
+ );
1530
+ }
1531
+ Reflect.set(target, prop, value);
1532
+ return true;
1533
+ }
1534
+ };
1535
+ }
1536
+ useStrategy(mode, handler) {
1537
+ this.strategyHandler.set(mode, handler);
1538
+ return this;
1539
+ }
1540
+ clearStrategy(mode) {
1541
+ if (mode) {
1542
+ this.strategyHandler.delete(mode);
1543
+ } else {
1544
+ this.strategyHandler.clear();
1545
+ }
1546
+ return this;
1547
+ }
1548
+ getScheduler(card, now) {
1549
+ const schedulerStrategy = this.strategyHandler.get(
1550
+ StrategyMode.SCHEDULER
1551
+ );
1552
+ const Scheduler = schedulerStrategy || this.Scheduler;
1553
+ const instance = new Scheduler(card, now, this, this.strategyHandler);
1554
+ return instance;
1555
+ }
1556
+ /**
1557
+ * Display the collection of cards and logs for the four scenarios after scheduling the card at the current time.
1558
+ * @param card Card to be processed
1559
+ * @param now Current time or scheduled time
1560
+ * @param afterHandler Convert the result to another type. (Optional)
1561
+ * @example
1562
+ * ```typescript
1563
+ * const card: Card = createEmptyCard(new Date());
1564
+ * const f = fsrs();
1565
+ * const recordLog = f.repeat(card, new Date());
1566
+ * ```
1567
+ * @example
1568
+ * ```typescript
1569
+ * interface RevLogUnchecked
1570
+ * extends Omit<ReviewLog, "due" | "review" | "state" | "rating"> {
1571
+ * cid: string;
1572
+ * due: Date | number;
1573
+ * state: StateType;
1574
+ * review: Date | number;
1575
+ * rating: RatingType;
1576
+ * }
1577
+ *
1578
+ * interface RepeatRecordLog {
1579
+ * card: CardUnChecked; //see method: createEmptyCard
1580
+ * log: RevLogUnchecked;
1581
+ * }
1582
+ *
1583
+ * function repeatAfterHandler(recordLog: RecordLog) {
1584
+ * const record: { [key in Grade]: RepeatRecordLog } = {} as {
1585
+ * [key in Grade]: RepeatRecordLog;
1586
+ * };
1587
+ * for (const grade of Grades) {
1588
+ * record[grade] = {
1589
+ * card: {
1590
+ * ...(recordLog[grade].card as Card & { cid: string }),
1591
+ * due: recordLog[grade].card.due.getTime(),
1592
+ * state: State[recordLog[grade].card.state] as StateType,
1593
+ * last_review: recordLog[grade].card.last_review
1594
+ * ? recordLog[grade].card.last_review!.getTime()
1595
+ * : null,
1596
+ * },
1597
+ * log: {
1598
+ * ...recordLog[grade].log,
1599
+ * cid: (recordLog[grade].card as Card & { cid: string }).cid,
1600
+ * due: recordLog[grade].log.due.getTime(),
1601
+ * review: recordLog[grade].log.review.getTime(),
1602
+ * state: State[recordLog[grade].log.state] as StateType,
1603
+ * rating: Rating[recordLog[grade].log.rating] as RatingType,
1604
+ * },
1605
+ * };
1606
+ * }
1607
+ * return record;
1608
+ * }
1609
+ * const card: Card = createEmptyCard(new Date(), cardAfterHandler); //see method: createEmptyCard
1610
+ * const f = fsrs();
1611
+ * const recordLog = f.repeat(card, new Date(), repeatAfterHandler);
1612
+ * ```
1613
+ */
1614
+ repeat(card, now, afterHandler) {
1615
+ const instance = this.getScheduler(card, now);
1616
+ const recordLog = instance.preview();
1617
+ if (afterHandler && typeof afterHandler === "function") {
1618
+ return afterHandler(recordLog);
1619
+ } else {
1620
+ return recordLog;
1621
+ }
1622
+ }
1623
+ /**
1624
+ * Display the collection of cards and logs for the card scheduled at the current time, after applying a specific grade rating.
1625
+ * @param card Card to be processed
1626
+ * @param now Current time or scheduled time
1627
+ * @param grade Rating of the review (Again, Hard, Good, Easy)
1628
+ * @param afterHandler Convert the result to another type. (Optional)
1629
+ * @example
1630
+ * ```typescript
1631
+ * const card: Card = createEmptyCard(new Date());
1632
+ * const f = fsrs();
1633
+ * const recordLogItem = f.next(card, new Date(), Rating.Again);
1634
+ * ```
1635
+ * @example
1636
+ * ```typescript
1637
+ * interface RevLogUnchecked
1638
+ * extends Omit<ReviewLog, "due" | "review" | "state" | "rating"> {
1639
+ * cid: string;
1640
+ * due: Date | number;
1641
+ * state: StateType;
1642
+ * review: Date | number;
1643
+ * rating: RatingType;
1644
+ * }
1645
+ *
1646
+ * interface NextRecordLog {
1647
+ * card: CardUnChecked; //see method: createEmptyCard
1648
+ * log: RevLogUnchecked;
1649
+ * }
1650
+ *
1651
+ function nextAfterHandler(recordLogItem: RecordLogItem) {
1652
+ const recordItem = {
1653
+ card: {
1654
+ ...(recordLogItem.card as Card & { cid: string }),
1655
+ due: recordLogItem.card.due.getTime(),
1656
+ state: State[recordLogItem.card.state] as StateType,
1657
+ last_review: recordLogItem.card.last_review
1658
+ ? recordLogItem.card.last_review!.getTime()
1659
+ : null,
1660
+ },
1661
+ log: {
1662
+ ...recordLogItem.log,
1663
+ cid: (recordLogItem.card as Card & { cid: string }).cid,
1664
+ due: recordLogItem.log.due.getTime(),
1665
+ review: recordLogItem.log.review.getTime(),
1666
+ state: State[recordLogItem.log.state] as StateType,
1667
+ rating: Rating[recordLogItem.log.rating] as RatingType,
1668
+ },
1669
+ };
1670
+ return recordItem
1671
+ }
1672
+ * const card: Card = createEmptyCard(new Date(), cardAfterHandler); //see method: createEmptyCard
1673
+ * const f = fsrs();
1674
+ * const recordLogItem = f.repeat(card, new Date(), Rating.Again, nextAfterHandler);
1675
+ * ```
1676
+ */
1677
+ next(card, now, grade, afterHandler) {
1678
+ const instance = this.getScheduler(card, now);
1679
+ const g = TypeConvert.rating(grade);
1680
+ if (g === Rating.Manual) {
1681
+ throw new Error("Cannot review a manual rating");
1682
+ }
1683
+ const recordLogItem = instance.review(g);
1684
+ if (afterHandler && typeof afterHandler === "function") {
1685
+ return afterHandler(recordLogItem);
1686
+ } else {
1687
+ return recordLogItem;
1688
+ }
1689
+ }
1690
+ /**
1691
+ * Get the retrievability of the card
1692
+ * @param card Card to be processed
1693
+ * @param now Current time or scheduled time
1694
+ * @param format default:true , Convert the result to another type. (Optional)
1695
+ * @returns The retrievability of the card,if format is true, the result is a string, otherwise it is a number
1696
+ */
1697
+ get_retrievability(card, now, format = true) {
1698
+ const processedCard = TypeConvert.card(card);
1699
+ now = now ? TypeConvert.time(now) : /* @__PURE__ */ new Date();
1700
+ const t = processedCard.state !== State.New ? Math.max(date_diff(now, processedCard.last_review, "days"), 0) : 0;
1701
+ const r = processedCard.state !== State.New ? this.forgetting_curve(t, +processedCard.stability.toFixed(8)) : 0;
1702
+ return format ? `${(r * 100).toFixed(2)}%` : r;
1703
+ }
1704
+ /**
1705
+ *
1706
+ * @param card Card to be processed
1707
+ * @param log last review log
1708
+ * @param afterHandler Convert the result to another type. (Optional)
1709
+ * @example
1710
+ * ```typescript
1711
+ * const now = new Date();
1712
+ * const f = fsrs();
1713
+ * const emptyCardFormAfterHandler = createEmptyCard(now);
1714
+ * const repeatFormAfterHandler = f.repeat(emptyCardFormAfterHandler, now);
1715
+ * const { card, log } = repeatFormAfterHandler[Rating.Hard];
1716
+ * const rollbackFromAfterHandler = f.rollback(card, log);
1717
+ * ```
1718
+ *
1719
+ * @example
1720
+ * ```typescript
1721
+ * const now = new Date();
1722
+ * const f = fsrs();
1723
+ * const emptyCardFormAfterHandler = createEmptyCard(now, cardAfterHandler); //see method: createEmptyCard
1724
+ * const repeatFormAfterHandler = f.repeat(emptyCardFormAfterHandler, now, repeatAfterHandler); //see method: fsrs.repeat()
1725
+ * const { card, log } = repeatFormAfterHandler[Rating.Hard];
1726
+ * const rollbackFromAfterHandler = f.rollback(card, log, cardAfterHandler);
1727
+ * ```
1728
+ */
1729
+ rollback(card, log, afterHandler) {
1730
+ const processedCard = TypeConvert.card(card);
1731
+ const processedLog = TypeConvert.review_log(log);
1732
+ if (processedLog.rating === Rating.Manual) {
1733
+ throw new Error("Cannot rollback a manual rating");
1734
+ }
1735
+ let last_due;
1736
+ let last_review;
1737
+ let last_lapses;
1738
+ switch (processedLog.state) {
1739
+ case State.New:
1740
+ last_due = processedLog.due;
1741
+ last_review = void 0;
1742
+ last_lapses = 0;
1743
+ break;
1744
+ case State.Learning:
1745
+ case State.Relearning:
1746
+ case State.Review:
1747
+ last_due = processedLog.review;
1748
+ last_review = processedLog.due;
1749
+ last_lapses = processedCard.lapses - (processedLog.rating === Rating.Again && processedLog.state === State.Review ? 1 : 0);
1750
+ break;
1751
+ }
1752
+ const prevCard = {
1753
+ ...processedCard,
1754
+ due: last_due,
1755
+ stability: processedLog.stability,
1756
+ difficulty: processedLog.difficulty,
1757
+ elapsed_days: processedLog.last_elapsed_days,
1758
+ scheduled_days: processedLog.scheduled_days,
1759
+ reps: Math.max(0, processedCard.reps - 1),
1760
+ lapses: Math.max(0, last_lapses),
1761
+ learning_steps: processedLog.learning_steps,
1762
+ state: processedLog.state,
1763
+ last_review
1764
+ };
1765
+ if (afterHandler && typeof afterHandler === "function") {
1766
+ return afterHandler(prevCard);
1767
+ } else {
1768
+ return prevCard;
1769
+ }
1770
+ }
1771
+ /**
1772
+ *
1773
+ * @param card Card to be processed
1774
+ * @param now Current time or scheduled time
1775
+ * @param reset_count Should the review count information(reps,lapses) be reset. (Optional)
1776
+ * @param afterHandler Convert the result to another type. (Optional)
1777
+ * @example
1778
+ * ```typescript
1779
+ * const now = new Date();
1780
+ * const f = fsrs();
1781
+ * const emptyCard = createEmptyCard(now);
1782
+ * const scheduling_cards = f.repeat(emptyCard, now);
1783
+ * const { card, log } = scheduling_cards[Rating.Hard];
1784
+ * const forgetCard = f.forget(card, new Date(), true);
1785
+ * ```
1786
+ *
1787
+ * @example
1788
+ * ```typescript
1789
+ * interface RepeatRecordLog {
1790
+ * card: CardUnChecked; //see method: createEmptyCard
1791
+ * log: RevLogUnchecked; //see method: fsrs.repeat()
1792
+ * }
1793
+ *
1794
+ * function forgetAfterHandler(recordLogItem: RecordLogItem): RepeatRecordLog {
1795
+ * return {
1796
+ * card: {
1797
+ * ...(recordLogItem.card as Card & { cid: string }),
1798
+ * due: recordLogItem.card.due.getTime(),
1799
+ * state: State[recordLogItem.card.state] as StateType,
1800
+ * last_review: recordLogItem.card.last_review
1801
+ * ? recordLogItem.card.last_review!.getTime()
1802
+ * : null,
1803
+ * },
1804
+ * log: {
1805
+ * ...recordLogItem.log,
1806
+ * cid: (recordLogItem.card as Card & { cid: string }).cid,
1807
+ * due: recordLogItem.log.due.getTime(),
1808
+ * review: recordLogItem.log.review.getTime(),
1809
+ * state: State[recordLogItem.log.state] as StateType,
1810
+ * rating: Rating[recordLogItem.log.rating] as RatingType,
1811
+ * },
1812
+ * };
1813
+ * }
1814
+ * const now = new Date();
1815
+ * const f = fsrs();
1816
+ * const emptyCardFormAfterHandler = createEmptyCard(now, cardAfterHandler); //see method: createEmptyCard
1817
+ * const repeatFormAfterHandler = f.repeat(emptyCardFormAfterHandler, now, repeatAfterHandler); //see method: fsrs.repeat()
1818
+ * const { card } = repeatFormAfterHandler[Rating.Hard];
1819
+ * const forgetFromAfterHandler = f.forget(card, date_scheduler(now, 1, true), false, forgetAfterHandler);
1820
+ * ```
1821
+ */
1822
+ forget(card, now, reset_count = false, afterHandler) {
1823
+ const processedCard = TypeConvert.card(card);
1824
+ now = TypeConvert.time(now);
1825
+ const scheduled_days = processedCard.state === State.New ? 0 : date_diff(now, processedCard.due, "days");
1826
+ const forget_log = {
1827
+ rating: Rating.Manual,
1828
+ state: processedCard.state,
1829
+ due: processedCard.due,
1830
+ stability: processedCard.stability,
1831
+ difficulty: processedCard.difficulty,
1832
+ elapsed_days: 0,
1833
+ last_elapsed_days: processedCard.elapsed_days,
1834
+ scheduled_days,
1835
+ learning_steps: processedCard.learning_steps,
1836
+ review: now
1837
+ };
1838
+ const forget_card = {
1839
+ ...processedCard,
1840
+ due: now,
1841
+ stability: 0,
1842
+ difficulty: 0,
1843
+ elapsed_days: 0,
1844
+ scheduled_days: 0,
1845
+ reps: reset_count ? 0 : processedCard.reps,
1846
+ lapses: reset_count ? 0 : processedCard.lapses,
1847
+ learning_steps: 0,
1848
+ state: State.New,
1849
+ last_review: processedCard.last_review
1850
+ };
1851
+ const recordLogItem = { card: forget_card, log: forget_log };
1852
+ if (afterHandler && typeof afterHandler === "function") {
1853
+ return afterHandler(recordLogItem);
1854
+ } else {
1855
+ return recordLogItem;
1856
+ }
1857
+ }
1858
+ /**
1859
+ * Reschedules the current card and returns the rescheduled collections and reschedule item.
1860
+ *
1861
+ * @template T - The type of the record log item.
1862
+ * @param {CardInput | Card} current_card - The current card to be rescheduled.
1863
+ * @param {Array<FSRSHistory>} reviews - The array of FSRSHistory objects representing the reviews.
1864
+ * @param {Partial<RescheduleOptions<T>>} options - The optional reschedule options.
1865
+ * @returns {IReschedule<T>} - The rescheduled collections and reschedule item.
1866
+ *
1867
+ * @example
1868
+ * ```typescript
1869
+ * const f = fsrs()
1870
+ * const grades: Grade[] = [Rating.Good, Rating.Good, Rating.Good, Rating.Good]
1871
+ * const reviews_at = [
1872
+ * new Date(2024, 8, 13),
1873
+ * new Date(2024, 8, 13),
1874
+ * new Date(2024, 8, 17),
1875
+ * new Date(2024, 8, 28),
1876
+ * ]
1877
+ *
1878
+ * const reviews: FSRSHistory[] = []
1879
+ * for (let i = 0; i < grades.length; i++) {
1880
+ * reviews.push({
1881
+ * rating: grades[i],
1882
+ * review: reviews_at[i],
1883
+ * })
1884
+ * }
1885
+ *
1886
+ * const results_short = scheduler.reschedule(
1887
+ * createEmptyCard(),
1888
+ * reviews,
1889
+ * {
1890
+ * skipManual: false,
1891
+ * }
1892
+ * )
1893
+ * console.log(results_short)
1894
+ * ```
1895
+ */
1896
+ reschedule(current_card, reviews = [], options = {}) {
1897
+ const {
1898
+ recordLogHandler,
1899
+ reviewsOrderBy,
1900
+ skipManual = true,
1901
+ now = /* @__PURE__ */ new Date(),
1902
+ update_memory_state: updateMemoryState = false
1903
+ } = options;
1904
+ if (reviewsOrderBy && typeof reviewsOrderBy === "function") {
1905
+ reviews.sort(reviewsOrderBy);
1906
+ }
1907
+ if (skipManual) {
1908
+ reviews = reviews.filter((review) => review.rating !== Rating.Manual);
1909
+ }
1910
+ const rescheduleSvc = new Reschedule(this);
1911
+ const collections = rescheduleSvc.reschedule(
1912
+ options.first_card || createEmptyCard(),
1913
+ reviews
1914
+ );
1915
+ const len = collections.length;
1916
+ const cur_card = TypeConvert.card(current_card);
1917
+ const manual_item = rescheduleSvc.calculateManualRecord(
1918
+ cur_card,
1919
+ now,
1920
+ len ? collections[len - 1] : void 0,
1921
+ updateMemoryState
1922
+ );
1923
+ if (recordLogHandler && typeof recordLogHandler === "function") {
1924
+ return {
1925
+ collections: collections.map(recordLogHandler),
1926
+ reschedule_item: manual_item ? recordLogHandler(manual_item) : null
1927
+ };
1928
+ }
1929
+ return {
1930
+ collections,
1931
+ reschedule_item: manual_item
1932
+ };
1933
+ }
1934
+ }
1935
+ const fsrs = (params) => {
1936
+ return new FSRS(params || {});
1937
+ };
1938
+
1939
+ 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
1940
  //# sourceMappingURL=index.mjs.map