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