{"id":50964318,"url":"https://github.com/bitbemol/swift-fsrs","last_synced_at":"2026-06-18T18:01:14.927Z","repository":{"id":352180896,"uuid":"1214177627","full_name":"bitbemol/swift-fsrs","owner":"bitbemol","description":"Swift port of the FSRS v6 spaced-repetition algorithm. Mirrors the ts-fsrs reference. Unofficial.","archived":false,"fork":false,"pushed_at":"2026-04-18T08:17:52.000Z","size":88,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-04-18T10:21:27.178Z","etag":null,"topics":["algorithm","flashcards","fsrs","spaced-repetition","swift","swift-package"],"latest_commit_sha":null,"homepage":"","language":"Swift","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/bitbemol.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-04-18T08:09:47.000Z","updated_at":"2026-04-18T08:17:56.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/bitbemol/swift-fsrs","commit_stats":null,"previous_names":["bitbemol/swift-fsrs"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/bitbemol/swift-fsrs","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bitbemol%2Fswift-fsrs","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bitbemol%2Fswift-fsrs/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bitbemol%2Fswift-fsrs/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bitbemol%2Fswift-fsrs/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/bitbemol","download_url":"https://codeload.github.com/bitbemol/swift-fsrs/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bitbemol%2Fswift-fsrs/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34501482,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-18T02:00:06.871Z","response_time":128,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["algorithm","flashcards","fsrs","spaced-repetition","swift","swift-package"],"created_at":"2026-06-18T18:01:11.825Z","updated_at":"2026-06-18T18:01:14.911Z","avatar_url":"https://github.com/bitbemol.png","language":"Swift","funding_links":[],"categories":[],"sub_categories":[],"readme":"# swift-fsrs\n\nA native Swift implementation of [FSRS v6](https://github.com/open-spaced-repetition/fsrs4anki/wiki/The-Algorithm) — the modern spaced-repetition algorithm used by Anki, RemNote, and many other flashcard systems. Pure Swift, zero dependencies, byte-parity verified against the canonical TypeScript reference.\n\n\u003e **Status:** Algorithm and scheduler are byte-parity verified against [ts-fsrs](https://github.com/open-spaced-repetition/ts-fsrs) v6 across 216 tests. Not yet 1.0 — APIs may shift before tagging stable. **This is an unofficial port** and is not affiliated with the [open-spaced-repetition](https://github.com/open-spaced-repetition) organization.\n\n## Why this exists\n\nIf you're building a flashcard app for iOS/macOS/visionOS and want FSRS scheduling, your previous options were:\n\n1. **Embed JavaScriptCore + ts-fsrs** — heavy, indirect, no Swift type safety.\n2. **FFI into a Rust binary** ([fsrs-rs](https://github.com/open-spaced-repetition/fsrs-rs)) — build complexity, no native `Sendable` story, awkward across platforms.\n3. **Roll your own** — error-prone; the algorithm has subtle edge cases that take time to discover the hard way.\n\n`swift-fsrs` gives you a fourth option: a faithful native port written in modern Swift, with the actual ts-fsrs reference values asserted in tests so you can be confident the math matches.\n\n## Inspiration and Attribution\n\nThis package is a port — almost all credit belongs upstream:\n\n- **[ts-fsrs](https://github.com/open-spaced-repetition/ts-fsrs)** by [@ishiko732](https://github.com/ishiko732) and contributors is the canonical TypeScript reference. Every formula, scheduler decision, rounding site, and edge case in this Swift port traces back to ts-fsrs. The 216 parity tests are anchored on hardcoded reference values lifted directly from `ts-fsrs/packages/fsrs/__tests__/`. If something looks weirdly precise here, it's because ts-fsrs got there first.\n- **[fsrs-rs](https://github.com/open-spaced-repetition/fsrs-rs)** by [@asukaminato0721](https://github.com/asukaminato0721), [@L-M-Sherlock](https://github.com/L-M-Sherlock) and contributors is the canonical Rust port and the FSRS optimizer/trainer. swift-fsrs deliberately does **not** include the optimizer (see \"Scope\" below); if you want to train weights from review history, fsrs-rs is the reference implementation.\n- **The FSRS algorithm itself** was designed by [Jarrett Ye](https://github.com/L-M-Sherlock) and the FSRS research community. Read the [algorithm wiki](https://github.com/open-spaced-repetition/fsrs4anki/wiki/The-Algorithm) for the math behind the formulas this package implements.\n\nIf this Swift port is useful to you, consider [starring ts-fsrs](https://github.com/open-spaced-repetition/ts-fsrs) and supporting the upstream FSRS project — they did the hard work.\n\n## Install\n\nSwift Package Manager:\n\n```swift\n.package(url: \"https://github.com/\u003cyour-username\u003e/swift-fsrs.git\", from: \"0.1.0\")\n```\n\nThen add `\"FSRS\"` to your target dependencies. Requires Swift 6.2 / Xcode 26 or later.\n\n## Quick start\n\n```swift\nimport FSRS\n\nlet fsrs = FSRS()                     // default v6 weights\nvar card = FSRS.createCard()          // a brand-new card, due immediately\n\n// User reviews the card; you decide the rating from response time, errors, etc.\nlet result = fsrs.schedule(card: card, now: Date(), rating: .good)\n\ncard = result.card                    // updated state — persist this\nlet log = result.log                  // a record of the review — persist this too\n\nprint(card.due)                       // when to show the card next\nprint(card.state)                     // .new / .learning / .review / .relearning\nprint(fsrs.retrievability(of: card))  // estimated recall probability right now\n```\n\nThat's the entire scheduling cycle. FSRS computes the `due` date; you query for `due \u003c= Date()` cards in the next session.\n\n## Concepts (60-second tour)\n\n- **`Card`** — a value type holding the per-card scheduling state: `due`, `stability`, `difficulty`, `state`, `step`, `reps`, `lapses`, `scheduledDays`, `lastReview`. It does **not** hold your card content (front/back) — wire that up in your own model with a card ID.\n- **`Rating`** — `.again` / `.hard` / `.good` / `.easy`. You decide how to derive these from user input (response time, error count, self-grading button, etc.).\n- **`FSRS`** — the scheduler. `init(parameters:)` accepts default v6 weights or a custom `Parameters` (e.g., weights from an FSRS optimizer trained on your users' data).\n- **`SchedulingResult`** — what `schedule(card:now:)` returns: previews of all four ratings (`again`, `hard`, `good`, `easy`), each a `RecordLogItem { card, log }`. Use this to show \"1m / 6m / 10m / 15m\" buttons before the user picks.\n- **`ReviewLog`** — an immutable snapshot of one review event. Persist these for history, undo, and (eventually) feeding into an optimizer pipeline.\n\n## API surface\n\n```swift\npublic struct FSRS: Sendable {\n    init(parameters: Parameters = Parameters())\n    static func createCard(now: Date = Date()) -\u003e Card\n\n    func schedule(card: Card, now: Date = Date()) -\u003e SchedulingResult\n    func schedule(card: Card, now: Date = Date(), rating: Rating) -\u003e RecordLogItem\n\n    func retrievability(of card: Card, now: Date = Date()) -\u003e Double\n\n    func rollback(card: Card, log: ReviewLog) -\u003e Card\n    func forget(card: Card, now: Date = Date(), resetCount: Bool = false) -\u003e RecordLogItem\n}\n```\n\n- `schedule(card:now:)` returns all four rating outcomes for previewing in your UI.\n- `schedule(card:now:rating:)` is the convenience overload when you already know the rating.\n- `rollback` undoes a review — for an \"undo\" button. Pass the same `ReviewLog` you got from `schedule`.\n- `forget` resets a card to `.new` — for a \"really forgot, start over\" button.\n\n## Two scheduler modes\n\n```swift\n// Basic (default) — new cards step through learning intervals (1m, 10m by default)\n//                   before graduating to FSRS-computed intervals.\nlet basic = FSRS()\n\n// Long-term — every review immediately produces an FSRS interval, no learning steps.\nlet longTerm = FSRS(parameters: Parameters(enableShortTerm: false))\n```\n\nUse **basic** for an Anki-style flow with quick relearning loops. Use **long-term** when you want FSRS to control intervals from the very first review (often preferred for less tactile inputs like web apps).\n\n## Custom parameters (optimizer-trained weights)\n\nThe 21 default weights are pre-trained on a large aggregate dataset and work well for most users. If you run the FSRS optimizer ([fsrs-rs](https://github.com/open-spaced-repetition/fsrs-rs) or the [Python optimizer](https://github.com/open-spaced-repetition/fsrs-optimizer)) on your own review data, plug the result in:\n\n```swift\nlet myWeights = Weights(array: [/* 21 values from the optimizer */])\nlet fsrs = FSRS(parameters: Parameters(weights: myWeights))\n```\n\nYou can also tune `requestRetention`, `maximumInterval`, `learningSteps`, `relearningSteps`, `enableShortTerm`, and `enableFuzz` on `Parameters`.\n\n## Scope (what's included, what isn't)\n\n### Included\n- The full FSRS v6 algorithm (`forgettingCurve`, `nextDifficulty`, `nextRecallStability`, `nextForgetStability`, `nextShortTermStability`, `nextState`, `nextInterval`).\n- `BasicScheduler` and `LongTermScheduler`, both byte-parity verified.\n- Interval fuzzing with a deterministic seeded PRNG (Alea, byte-for-byte ported from JS).\n- `rollback` and `forget` for undo / reset UX.\n- `Codable` on `Card`, `ReviewLog`, and `RecordLogItem` for trivial persistence — with a backward-compat decoder for `Card.scheduledDays` so existing JSON keeps working when you upgrade.\n\n### Deliberately not included\n- **`reschedule(card, reviews, options)`** — replays a card's full history under new weights. Useful only after retraining the optimizer; most apps don't need it. PRs welcome with use-case discussion.\n- **`Rating.manual` (5th rating value)** — ts-fsrs uses it to tag forget/reschedule logs. Adding it to Swift's exhaustive `Rating` enum is invasive and provides no user-visible behavior. Swift's `forget` log uses `.again` as a stand-in (documented in source).\n- **Strategy hooks (`useStrategy(SCHEDULER/SEED/LEARNING_STEPS, ...)`)** — ts-fsrs's plug-in mechanism for custom schedulers, RNG seeds, learning-step strategies. Skipped to keep the surface simple.\n- **The optimizer / training pipeline** — that's [fsrs-rs](https://github.com/open-spaced-repetition/fsrs-rs)'s job. Train weights there (Python or Rust), then load them into swift-fsrs at runtime.\n- **`migrateParameters`** — auto-upgrade FSRS-4 (17 weights) or FSRS-5 (19 weights) inputs to v6 (21 weights). On the roadmap; for now, only FSRS-6 weights are accepted.\n- **Loose date-input types** (`Date | number | string` like ts-fsrs) — Swift's type system makes this needlessly noisy. Use `Date` everywhere.\n\nIf any of these matter for your use case, [open an issue](#contributing) — most are tractable, they're just not in v0.x.\n\n## Verification (why you can trust the math)\n\n- **216 tests across 57 suites**, all passing.\n- The four `*ParityTests.swift` files cross-check Swift output against ts-fsrs's own test suite at `packages/fsrs/__tests__/`. Reference values are taken **verbatim** from there — they're computed and asserted by the ts-fsrs maintainers, so they're our ground truth.\n- Every parity assertion uses exact `==` (no tolerance), except 4 assertions that mirror ts-fsrs's own `toBeCloseTo(... 4)` precision on its memory-state finals.\n- Sequence A canary: after a new card receives Good at t=0 and Good again at t=2 days, `card.stability == 10.96433194` and `card.difficulty == 2.11121424` — both exact, locked in tests.\n- The [Alea PRNG](Sources/FSRS/Alea.swift) (used by interval fuzzing) is a byte-for-byte port of the JS implementation, verified by 5 reference vectors that match ts-fsrs bit-exact.\n- Coverage matrix: every `(state × rating)` cell for both schedulers is asserted byte-exact. Long sequences of 50 mixed-rating reviews drift zero from the reference.\n\nRun the suite locally:\n\n```bash\nswift test\n```\n\n## Concurrency\n\nSwift 6.2 strict concurrency is on. Everything public is `Sendable`:\n\n- `FSRS`, `Parameters`, `Weights`, `Card`, `ReviewLog`, `RecordLogItem`, `SchedulingResult` — all value types.\n- No actors, no locks, no mutable shared state. Multiple `FSRS` instances can run on different threads concurrently with no synchronization needed.\n\n## Roadmap\n\nIn rough priority order:\n\n- [ ] `migrateParameters` (FSRS-4/5 → v6 weight migration)\n- [ ] DocC catalog + hosted documentation\n- [ ] CI badge (GitHub Actions on macOS / Linux)\n- [ ] An `Examples/` directory with a minimal SwiftUI flashcard sample\n- [ ] `reschedule` (only if there's demand)\n\n## Contributing\n\nIssues, bug reports, and PRs are welcome. Two ground rules before contributing:\n\n1. **ts-fsrs is canonical.** If a Swift change would diverge from ts-fsrs's output, it needs a strong justification. The 216 parity tests are the regression net — don't weaken assertions to make a change pass.\n2. **Run `swift test` before opening a PR.** Test count must stay ≥ 216, all passing, with clean build (zero warnings).\n\nFor internal architecture rules and the \"do not touch\" list, see [`CLAUDE.md`](CLAUDE.md).\n\n## Acknowledgments\n\n- [Jarrett Ye](https://github.com/L-M-Sherlock) and the FSRS research community for designing the algorithm.\n- [@ishiko732](https://github.com/ishiko732) and the [ts-fsrs](https://github.com/open-spaced-repetition/ts-fsrs) contributors — this port would not be possible without their reference implementation and exhaustive test suite.\n- The [open-spaced-repetition](https://github.com/open-spaced-repetition) organization for stewarding the FSRS ecosystem in the open.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbitbemol%2Fswift-fsrs","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fbitbemol%2Fswift-fsrs","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbitbemol%2Fswift-fsrs/lists"}