Files
learn-languages/src/app/(features)/memorize/interval-preview.ts
goddonebianu 13e8f51ada feat(memorize): add interval preview calculation utility
- Add calculatePreviewIntervals for Again/Hard/Good/Easy buttons
- Support NEW, LEARNING, RELEARNING, and REVIEW card types
- Use SM2_CONFIG constants for accurate interval calculation
- All intervals returned in minutes for consistent formatting

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)
Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-03-13 15:07:32 +08:00

85 lines
2.5 KiB
TypeScript

import { CardType } from "../../../../generated/prisma/enums";
import { SM2_CONFIG } from "@/modules/card/card-service-dto";
export interface CardPreview {
type: CardType;
ivl: number;
factor: number;
left: number;
}
export interface PreviewIntervals {
again: number;
hard: number;
good: number;
easy: number;
}
function calculateReviewIntervals(ivl: number, factor: number): PreviewIntervals {
const MINUTES_PER_DAY = 1440;
return {
again: Math.max(1, Math.floor(ivl * SM2_CONFIG.NEW_INTERVAL)) * MINUTES_PER_DAY,
hard: Math.floor(ivl * SM2_CONFIG.HARD_INTERVAL * SM2_CONFIG.INTERVAL_MODIFIER) * MINUTES_PER_DAY,
good: Math.floor(ivl * (factor / 1000) * SM2_CONFIG.INTERVAL_MODIFIER) * MINUTES_PER_DAY,
easy: Math.floor(ivl * (factor / 1000) * SM2_CONFIG.EASY_BONUS * SM2_CONFIG.INTERVAL_MODIFIER) * MINUTES_PER_DAY,
};
}
function calculateNewCardIntervals(): PreviewIntervals {
const steps = SM2_CONFIG.LEARNING_STEPS;
return {
again: steps[0],
hard: steps.length >= 2 ? (steps[0] + steps[1]) / 2 : steps[0],
good: steps.length >= 2 ? steps[1] : SM2_CONFIG.GRADUATING_INTERVAL_GOOD * 1440,
easy: SM2_CONFIG.EASY_INTERVAL * 1440,
};
}
function calculateLearningIntervals(left: number): PreviewIntervals {
const steps = SM2_CONFIG.LEARNING_STEPS;
const stepIndex = Math.floor(left % 1000);
const again = steps[0];
let hard: number;
if (stepIndex === 0 && steps.length >= 2) {
hard = (steps[0] + steps[1]) / 2;
} else if (stepIndex < steps.length - 1) {
hard = steps[stepIndex];
} else {
hard = SM2_CONFIG.GRADUATING_INTERVAL_GOOD * 1440;
}
let good: number;
if (stepIndex < steps.length - 1) {
good = steps[stepIndex + 1];
} else {
good = SM2_CONFIG.GRADUATING_INTERVAL_GOOD * 1440;
}
const easy = SM2_CONFIG.GRADUATING_INTERVAL_EASY * 1440;
return { again, hard, good, easy };
}
export function calculatePreviewIntervals(card: CardPreview): PreviewIntervals {
switch (card.type) {
case CardType.NEW:
return calculateNewCardIntervals();
case CardType.LEARNING:
case CardType.RELEARNING:
return calculateLearningIntervals(card.left);
case CardType.REVIEW:
default:
return calculateReviewIntervals(card.ivl, card.factor);
}
}
export function formatPreviewInterval(minutes: number): string {
if (minutes < 1) return "<1";
if (minutes < 60) return `${Math.round(minutes)}`;
if (minutes < 1440) return `${Math.round(minutes / 60)}h`;
return `${Math.round(minutes / 1440)}d`;
}