Skip to content

Instantly share code, notes, and snippets.

@mosioc
Created June 5, 2026 11:49
Show Gist options
  • Select an option

  • Save mosioc/a535e545aa803893d231fb66be4fedb2 to your computer and use it in GitHub Desktop.

Select an option

Save mosioc/a535e545aa803893d231fb66be4fedb2 to your computer and use it in GitHub Desktop.
5-Box Leitner Spaced Repetition System - G5
// map of box index to the days interval array
const G5_INTERVALS = [1, 2, 5, 9, 14];
interface Flashcard {
id: string;
word: string;
definition: string;
box: number; // 1 to 5
nextReviewDate: Date;
}
// execute user response evaluation
function processReview(card: Flashcard, isCorrect: boolean): Flashcard {
const updatedCard = { ...card };
if (isCorrect) {
// move forward up to box 5
updatedCard.box = Math.min(card.box + 1, 5);
} else {
// reset to box 1 on failure
updatedCard.box = 1;
}
// calculate next review timestamp
const targetDays = G5_INTERVALS[updatedCard.box - 1];
const today = new Date();
today.setDate(today.getDate() + targetDays);
updatedCard.nextReviewDate = today;
return updatedCard;
}
@mosioc

mosioc commented Jun 5, 2026

Copy link
Copy Markdown
Author
// type definitions for runtime safety
export type BoxNumber = 1 | 2 | 3 | 4 | 5;

export interface Flashcard {
  id: string;
  word: string;
  definition: string;
  box: BoxNumber;
  nextReviewDate: string; // stored as clean iso format: 'yyyy-mm-dd'
}

// deterministic g5 day intervals mapping
// index 0 corresponding to box 1, etc.
const G5_INTERVALS: Record<BoxNumber, number> = {
  1: 1,
  2: 2,
  3: 5,
  4: 9,
  5: 14,
};

/**
 * helper to cleanly calculate calendar dates without time zone drift
 */
function addDaysToCurrentDate(days: number): string {
  const targetDate = new Date();
  targetDate.setDate(targetDate.getDate() + days);
  return targetDate.toISOString().split('T')[0];
}

/**
 * evaluates a flashcard review and returns a brand new immutable card instance
 */
export function reviewCard(card: Flashcard, isCorrect: boolean): Flashcard {
  // calculate next box placement based on performance
  let nextBox: BoxNumber;

  if (isCorrect) {
    // advance linearly up to a max of box 5
    nextBox = Math.min(card.box + 1, 5) as BoxNumber;
  } else {
    // asymmetric penalty: strict reset back to box 1
    nextBox = 1;
  }

  // derive review cadence using our g5 map
  const intervalDays = G5_INTERVALS[nextBox];
  const nextReviewDate = addDaysToCurrentDate(intervalDays);

  // return fresh state object
  return {
    ...card,
    box: nextBox,
    nextReviewDate,
  };
}

/**
 * pipeline filters to organize active training queues
 */
export const cardFilters = {
  // check if a card is ready to be tested right now
  isDue: (card: Flashcard): boolean => {
    const todayStr = new Date().toISOString().split('T')[0];
    return card.nextReviewDate <= todayStr;
  },

  // query specifically what is sitting inside a chosen container
  getBoxContents: (cards: Flashcard[], box: BoxNumber): Flashcard[] => {
    return cards.filter((card) => card.box === box);
  },
};

@mosioc

mosioc commented Jun 5, 2026

Copy link
Copy Markdown
Author
// sample starting card in box 3
const initialCard: Flashcard = {
  id: "vocab-001",
  word: "déjà vu",
  definition: "the illusion of having previously experienced something",
  box: 3,
  nextReviewDate: "2026-06-05",
};

// scenario a: user gets it right! card gets promoted to box 4
const promotedCard = reviewCard(initialCard, true);
console.log(promotedCard.box);            // output: 4
console.log(promotedCard.nextReviewDate); // output: (today's date + 9 days)

// scenario b: user slips up! card drops all the way back to box 1
const failedCard = reviewCard(initialCard, false);
console.log(failedCard.box);            // output: 1
console.log(failedCard.nextReviewDate); // output: (today's date + 1 day)

@mosioc

mosioc commented Jun 5, 2026

Copy link
Copy Markdown
Author
// sample starting card in box 3
const initialCard: Flashcard = {
  id: "vocab-001",
  word: "déjà vu",
  definition: "the illusion of having previously experienced something",
  box: 3,
  nextReviewDate: "2026-06-05",
};

// scenario a: user gets it right! card gets promoted to box 4
const promotedCard = reviewCard(initialCard, true);
console.log(promotedCard.box);            // output: 4
console.log(promotedCard.nextReviewDate); // output: (today's date + 9 days)

// scenario b: user slips up! card drops all the way back to box 1
const failedCard = reviewCard(initialCard, false);
console.log(failedCard.box);            // output: 1
console.log(failedCard.nextReviewDate); // output: (today's date + 1 day)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment