Functional Weave
Code in Rust

todo.parse-repeat@1.0.0

impl/typescript.ts

2,368 bytes · the TypeScript implementation · view raw

Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.

import { addDays } from "./dates_add_days.ts";  ← from dates.add-days ^1.0.0 · built alongside by fune
import { dayOfWeek } from "./dates_day_of_week.ts";  ← from dates.day-of-week ^1.0.0 · built alongside by fune
import { type Recurrence } from "./todo_item.ts";  ← from todo.item ^1.0.0 · built alongside by fune
import { parseDatePhrase } from "./todo_parse_date_phrase.ts";  ← from todo.parse-date-phrase ^1.0.0 · built alongside by fune
import { parseQuickAdd } from "./todo_parse_quick_add.ts";  ← from todo.parse-quick-add ^1.0.0 · built alongside by fune

function lowerAscii(text: string): string {
  return text.replace(/[A-Z]/g, (c) => c.toLowerCase());
}

/**
 * A Repeat box: an "every ..." phrase exactly as todo.parse-quick-add reads
 * it, then optionally a date phrase for the start. Anything else, words left
 * over included, is null. The series starts on the typed date, else on
 * `due`, else where quick-add would start it; for "every <weekday>" and
 * "every weekday" the start moves forward to the first day the pattern
 * allows. The anchor is that first date.
 */
export function parseRepeat(text: string, due: string | null, today: string): Recurrence | null {
  // addDays(x, 0) is the date check: it throws dates.add-days's message.
  addDays(today, 0);
  if (due !== null) addDays(due, 0);
  const words = text.split(/[ \t\n\r\v\f]+/).filter((w) => w !== "");
  if (words.length < 2 || lowerAscii(words[0]) !== "every") return null;

  // The repeat is 2 words ("every week") or 3 ("every 2 weeks"); no 2-word
  // form starts a 3-word one, so the first that parses as a bare repeat wins.
  let repeat: Recurrence | null = null;
  let length = 0;
  for (const n of [2, 3]) {
    if (n > words.length) break;
    const draft = parseQuickAdd(words.slice(0, n).join(" "), today);
    if (draft.recurrence !== null && draft.title === "") {
      repeat = draft.recurrence;
      length = n;
      break;
    }
  }
  if (repeat === null) return null;

  let start = due;
  if (words.length > length) {
    start = parseDatePhrase(words.slice(length).join(" "), today);
    if (start === null) return null;
  }
  if (start === null) return repeat;

  if (repeat.frequency === "weekdays") {
    const day = dayOfWeek(start);
    if (day >= 6) start = addDays(start, 8 - day);
  } else if (repeat.frequency === "weekly" && length === 2 && parseDatePhrase(words[1], today) !== null) {
    // "every monday": quick-add anchored it on the next Monday, so that
    // anchor's weekday is the one the series keeps.
    start = addDays(start, (dayOfWeek(repeat.anchor) - dayOfWeek(start) + 7) % 7);
  }
  return { ...repeat, anchor: start };
}