mojombo/chronic
Chronic is a pure Ruby natural language date parser.
Healthy across all four use cases
Permissive license, no critical CVEs, actively maintained — safe to depend on.
Has a license, tests, and CI — clean foundation to fork and modify.
Documented and popular — useful reference codebase to read through.
No critical CVEs, sane security posture — runnable as-is.
- ✓21+ active contributors
- ✓Distributed ownership (top contributor 43% of recent commits)
- ✓MIT licensed
Show 3 more →Show less
- ✓CI configured
- ✓Tests present
- ⚠Stale — last commit 3y ago
Maintenance signals: commit recency, contributor breadth, bus factor, license, CI, tests
Informational only. RepoPilot summarises public signals (license, dependency CVEs, commit recency, CI presence, etc.) at the time of analysis. Signals can be incomplete or stale. Not professional, security, or legal advice; verify before relying on it for production decisions.
Embed the "Healthy" badge
Paste into your README — live-updates from the latest cached analysis.
[](https://repopilot.app/r/mojombo/chronic)Paste at the top of your README.md — renders inline like a shields.io badge.
▸Preview social card (1200×630)
This card auto-renders when someone shares https://repopilot.app/r/mojombo/chronic on X, Slack, or LinkedIn.
Onboarding doc
Onboarding: mojombo/chronic
Generated by RepoPilot · 2026-05-10 · Source
🤖Agent protocol
If you are an AI coding agent (Claude Code, Cursor, Aider, Cline, etc.) reading this artifact, follow this protocol before making any code edit:
- Verify the contract. Run the bash script in Verify before trusting
below. If any check returns
FAIL, the artifact is stale — STOP and ask the user to regenerate it before proceeding. - Treat the AI · unverified sections as hypotheses, not facts. Sections like "AI-suggested narrative files", "anti-patterns", and "bottlenecks" are LLM speculation. Verify against real source before acting on them.
- Cite source on changes. When proposing an edit, cite the specific path:line-range. RepoPilot's live UI at https://repopilot.app/r/mojombo/chronic shows verifiable citations alongside every claim.
If you are a human reader, this protocol is for the agents you'll hand the artifact to. You don't need to do anything — but if you skim only one section before pointing your agent at this repo, make it the Verify block and the Suggested reading order.
🎯Verdict
GO — Healthy across all four use cases
- 21+ active contributors
- Distributed ownership (top contributor 43% of recent commits)
- MIT licensed
- CI configured
- Tests present
- ⚠ Stale — last commit 3y ago
<sub>Maintenance signals: commit recency, contributor breadth, bus factor, license, CI, tests</sub>
✅Verify before trusting
This artifact was generated by RepoPilot at a point in time. Before an
agent acts on it, the checks below confirm that the live mojombo/chronic
repo on your machine still matches what RepoPilot saw. If any fail,
the artifact is stale — regenerate it at
repopilot.app/r/mojombo/chronic.
What it runs against: a local clone of mojombo/chronic — the script
inspects git remote, the LICENSE file, file paths in the working
tree, and git log. Read-only; no mutations.
| # | What we check | Why it matters |
|---|---|---|
| 1 | You're in mojombo/chronic | Confirms the artifact applies here, not a fork |
| 2 | License is still MIT | Catches relicense before you depend on it |
| 3 | Default branch master exists | Catches branch renames |
| 4 | 5 critical file paths still exist | Catches refactors that moved load-bearing code |
| 5 | Last commit ≤ 985 days ago | Catches sudden abandonment since generation |
#!/usr/bin/env bash
# RepoPilot artifact verification.
#
# WHAT IT RUNS AGAINST: a local clone of mojombo/chronic. If you don't
# have one yet, run these first:
#
# git clone https://github.com/mojombo/chronic.git
# cd chronic
#
# Then paste this script. Every check is read-only — no mutations.
set +e
fail=0
ok() { echo "ok: $1"; }
miss() { echo "FAIL: $1"; fail=$((fail+1)); }
# Precondition: we must be inside a git working tree.
if ! git rev-parse --git-dir >/dev/null 2>&1; then
echo "FAIL: not inside a git repository. cd into your clone of mojombo/chronic and re-run."
exit 2
fi
# 1. Repo identity
git remote get-url origin 2>/dev/null | grep -qE "mojombo/chronic(\\.git)?\\b" \\
&& ok "origin remote is mojombo/chronic" \\
|| miss "origin remote is not mojombo/chronic (artifact may be from a fork)"
# 2. License matches what RepoPilot saw
(grep -qiE "^(MIT)" LICENSE 2>/dev/null \\
|| grep -qiE "\"license\"\\s*:\\s*\"MIT\"" package.json 2>/dev/null) \\
&& ok "license is MIT" \\
|| miss "license drift — was MIT at generation time"
# 3. Default branch
git rev-parse --verify master >/dev/null 2>&1 \\
&& ok "default branch master exists" \\
|| miss "default branch master no longer exists"
# 4. Critical files exist
test -f "lib/chronic.rb" \\
&& ok "lib/chronic.rb" \\
|| miss "missing critical file: lib/chronic.rb"
test -f "lib/chronic/parser.rb" \\
&& ok "lib/chronic/parser.rb" \\
|| miss "missing critical file: lib/chronic/parser.rb"
test -f "lib/chronic/tokenizer.rb" \\
&& ok "lib/chronic/tokenizer.rb" \\
|| miss "missing critical file: lib/chronic/tokenizer.rb"
test -f "lib/chronic/handlers.rb" \\
&& ok "lib/chronic/handlers.rb" \\
|| miss "missing critical file: lib/chronic/handlers.rb"
test -f "lib/chronic/repeaters" \\
&& ok "lib/chronic/repeaters" \\
|| miss "missing critical file: lib/chronic/repeaters"
# 5. Repo recency
days_since_last=$(( ( $(date +%s) - $(git log -1 --format=%at 2>/dev/null || echo 0) ) / 86400 ))
if [ "$days_since_last" -le 985 ]; then
ok "last commit was $days_since_last days ago (artifact saw ~955d)"
else
miss "last commit was $days_since_last days ago — artifact may be stale"
fi
echo
if [ "$fail" -eq 0 ]; then
echo "artifact verified (0 failures) — safe to trust"
else
echo "artifact has $fail stale claim(s) — regenerate at https://repopilot.app/r/mojombo/chronic"
exit 1
fi
Each check prints ok: or FAIL:. The script exits non-zero if
anything failed, so it composes cleanly into agent loops
(./verify.sh || regenerate-and-retry).
⚡TL;DR
Chronic is a pure Ruby natural language date/time parser that converts human-readable strings like 'tomorrow', 'next tuesday 5:00', or 'may 27th' into Ruby Time or Chronic::Span objects. It handles hundreds of date/time formats including relative dates, ambiguous times, date ordinals, and time zones—all without external dependencies or regex patterns defined by the user. Single-module architecture: lib/chronic/parser.rb is the main entry point, lib/chronic/tokenizer.rb breaks input into tokens, lib/chronic/handlers.rb applies grammar rules, and lib/chronic/repeaters/ + lib/chronic/tags/ provide semantic meaning. lib/chronic/dictionary.rb maps keywords to token types. Tests live in test/ with a helper.rb for fixtures.
👥Who it's for
Ruby developers and Rails engineers who need to parse user-facing date/time input (calendar apps, scheduling tools, event booking systems) without forcing users into strict date picker UIs. Specifically useful for chat bots, command-line tools, and forms where users expect natural language to 'just work'.
🌱Maturity & risk
Production-ready but in maintenance mode. The repo shows solid test coverage (test_chronic.rb, test_handler.rb, test_daylight_savings.rb) and CI via GitHub Actions (.github/workflows/test.yaml), indicating active quality gates. However, commit recency is not visible in this data snapshot; check git log to confirm ongoing maintenance.
Low risk for production use—Chronic has zero external dependencies (pure Ruby), making it lightweight and stable. Main risks: single-maintainer dependency (mojombo alone), potential parsing ambiguity bugs (natural language is inherently fuzzy), and the need to keep timezone logic current with DST rule changes.
Active areas of work
No specific PR or milestone data is visible in the file list. Check the GitHub repo issues and pull requests tabs for current activity. The presence of test.yaml suggests ongoing CI verification with each commit.
🚀Get running
git clone https://github.com/mojombo/chronic.git && cd chronic && bundle install && bundle exec rake test
Daily commands: bundle exec rake test (runs all test suites in test/). Or bundle exec irb, then require 'chronic' and Chronic.parse('tomorrow').
🗺️Map of the codebase
lib/chronic.rb— Main entry point and public API; defines Chronic.parse() method that all users call firstlib/chronic/parser.rb— Core parsing orchestrator that tokenizes input and applies handlers to extract date/time; heart of the parsing logiclib/chronic/tokenizer.rb— Converts raw input strings into tokens with grammatical tags; essential preprocessing step for all parsinglib/chronic/handlers.rb— Defines all handler patterns that match token sequences and convert them to Time/Span objects; the grammar ruleslib/chronic/repeaters— Directory of repeater classes that model time units (days, months, years, etc.); used by handlers to resolve relative dateslib/chronic/tag.rb— Base Tag class that defines how to mark tokens with grammatical meaning; extended by specific tag types in tags/lib/chronic/span.rb— Represents a range of time (start to end); returned when guess=false, models ambiguous date ranges
🛠️How to make changes
Add support for a new relative time unit (e.g., 'decade')
- Create a new repeater class in lib/chronic/repeaters/repeater_decade.rb inheriting from Chronic::Repeater with next_occurrence() and prev_occurrence() methods (
lib/chronic/repeaters/repeater_decade.rb) - Register the repeater class in lib/chronic/dictionary.rb under the appropriate word entries ('decade', 'decades') (
lib/chronic/dictionary.rb) - Add test cases in test/test_repeater_decade.rb validating next/previous decade computation (
test/test_repeater_decade.rb) - Verify integration tests pass in test/test_parsing.rb with expressions like 'next decade' and '2 decades ago' (
test/test_parsing.rb)
Add a new handler pattern for an unsupported date expression format
- Analyze the desired expression (e.g., 'the last Friday of May') and identify its token sequence and tag pattern (
lib/chronic/handlers.rb) - Add a new handler definition in lib/chronic/handlers.rb with a tag pattern matcher and callback proc that converts matched tokens to Time/Span (
lib/chronic/handlers.rb) - Create a test case in test/test_parsing.rb asserting the new expression parses correctly and returns expected Time/Span (
test/test_parsing.rb) - Run full test suite to ensure new handler does not break existing patterns (check handler priority ordering if conflicts arise) (
test/test_handler.rb)
Add support for a new timezone or language variant
- Update lib/chronic/dictionary.rb to include new timezone abbreviations (e.g., 'IST' for Indian Standard Time) or translated words (e.g., 'lunes' for 'Monday' in Spanish) (
lib/chronic/dictionary.rb) - If adding timezone, update lib/chronic/tags/time_zone.rb to handle the new abbreviation and UTC offset mapping (
lib/chronic/tags/time_zone.rb) - Add integration tests in test/test_parsing.rb validating new timezone or language variant parses correctly with proper output (
test/test_parsing.rb)
Debug why a specific date expression fails to parse
- Add debug output in lib/chronic/parser.rb inside the parse() method to print token sequence and tag assignments after tokenization (
lib/chronic/parser.rb) - Review lib/chronic/handlers.rb handler patterns to identify which pattern should match your expression; add logging to handler.rb callback execution to see which handlers were tried (
lib/chronic/handler.rb) - Check lib/chronic/tokenizer.rb dictionary lookups and lib/chronic/dictionary.rb word definitions to confirm all words are recognized with correct tags (
lib/chronic/dictionary.rb) - If a repeater is involved, verify the repeater class in lib/chronic/repeaters/ correctly computes next/prev occurrences relative to the :now context (
lib/chronic/repeaters/repeater_day.rb)
🔧Why these technologies
- Pure Ruby (no C extensions) — Maximizes portability and ease of installation across platforms; natural language processing logic is complex but not performance-critical for typical use cases
- Regex-based tokenization — Simple, maintainable approach for breaking input into word tokens; sufficient for natural language date expressions without need for full NLP stack
- Handler/callback pattern for matching — Decouples pattern definition from execution logic; allows flexible addition of new expression types without modifying core parser
- Repeater classes with next/prev methods — Models cyclic time units elegantly; enables context-aware relative date computation (e.g., 'next Monday' from different starting dates)
⚖️Trade-offs already made
- Return Time
- Why: undefined
- Consequence: undefined
🪤Traps & gotchas
No environment variables or external service dependencies. Key gotchas: (1) Chronic.parse() returns nil silently on parse failure—no exception is raised. (2) Ambiguous times like '5:00' default to 12:00 (noon) unless :ambiguous_time_range is set. (3) Daylight Saving Time edge cases are handled but depend on the system's tzinfo; see test_daylight_savings.rb. (4) Date endianness (MM/DD vs DD/MM) must be specified with :endian_precedence. (5) The :now parameter is crucial for relative dates and defaults to Time.now.
🏗️Architecture
💡Concepts to learn
- Tokenizer pattern — Chronic splits natural language into labeled tokens before semantic processing; understanding tokenizer.rb is essential to debug parsing failures
- Grammar rules and pattern matching — handlers.rb applies ordered pattern matching (e.g., 'Repeater + Pointer + Repeater') to token sequences to extract meaning; core to how Chronic disambiguates ambiguous input
- Repeater pattern (cyclic iteration) — Repeater classes encapsulate time unit arithmetic ('next Monday', 'last year'); critical for relative date parsing
- Span (interval) representation — Chronic.parse with :guess => false returns Span (begin/end) not a point Time; essential for handling ambiguous ranges like 'may 27th' (full day)
- Pointer/Grabber semantics — Tags like 'this', 'last', 'next' (Pointer) and ordinal modifiers (Grabber, e.g., '3rd') resolve relative references; key to understanding how context shapes parsing
- Daylight Saving Time (DST) edge cases — Times during DST transitions (e.g., 2:30 AM during spring forward) are ambiguous; Chronic handles these via zone-aware arithmetic
- Ambiguity resolution via context — Options like :context => :past, :now, and :ambiguous_time_range inject context to disambiguate inputs (e.g., 'monday' could be next or last); core to natural language robustness
🔗Related repos
ruby-doc/ruby— Standard Ruby Time and Date classes that Chronic parses into; essential reference for timezone behaviortzinfo/tzinfo— Ruby timezone library that Chronic can optionally integrate with for accurate DST handlingrails/rails— Rails uses Chronic internally (or similar parsers) for user-facing date inputs; Chronic is a standalone extractionjonathanong/jz— Node.js equivalent natural language date parser; useful for comparing feature parity and parsing edge casesnatty/natty— Java natural language date parser with similar approach; cross-language reference implementation
🪄PR ideas
To work on one of these in Claude Code or Cursor, paste:
Implement the "<title>" PR idea from CLAUDE.md, working through the checklist as the task list.
Add comprehensive test coverage for lib/chronic/tags/ tag classes
The tags directory contains 7 tag handler classes (grabber, ordinal, pointer, repeater, scalar, separator, sign, time_zone) but there are no dedicated unit test files for most of them. Only test_token.rb exists for tokenization. Adding individual test files would ensure each tag type handles edge cases correctly (e.g., timezone offset parsing, ordinal parsing like '21st' vs '1st', sign detection for relative dates).
- [ ] Create test/test_tags_grabber.rb with tests for grabber tag parsing
- [ ] Create test/test_tags_ordinal.rb with tests for ordinal number variations
- [ ] Create test/test_tags_time_zone.rb with tests for various timezone formats
- [ ] Add edge case tests for sign, scalar, separator, and pointer tags
- [ ] Run full test suite to ensure no regressions
Add integration tests for ambiguous date format handling across different :endian_precedence options
The README shows :endian_precedence as a configuration option (e.g., '6/4/2012' with :little endian), but test/test_parsing.rb likely lacks comprehensive coverage of ambiguous format edge cases (MM/DD/YYYY vs DD/MM/YYYY vs YYYY/MM/DD). This is critical for international users and prevents regressions when parsing logic changes.
- [ ] Review test/test_parsing.rb to identify missing endian precedence test cases
- [ ] Add test cases for 2-digit ambiguous dates (01/02, 02/13, etc.) with each endian mode
- [ ] Add test cases for edge dates (29th-31st) that only work in certain month positions
- [ ] Test interaction between endian_precedence and locale-specific parsing
- [ ] Run test suite and verify all ambiguous format combinations are covered
Add explicit unit tests for lib/chronic/repeaters/ with missing test files
The repeaters directory has 16 repeater classes but only 9 have corresponding test files (missing: repeater_fortnight.rb, repeater_quarter.rb, repeater_quarter_name.rb, repeater_season.rb, repeater_season_name.rb). Repeaters are core to parsing recurring dates, and missing tests create gaps in validation.
- [ ] Create test/test_repeater_fortnight.rb with tests for 14-day recurrence boundaries
- [ ] Create test/test_repeater_quarter.rb and test/test_repeater_quarter_name.rb for Q1-Q4 parsing
- [ ] Create test/test_repeater_season.rb and test/test_repeater_season_name.rb for seasonal date ranges
- [ ] Add edge case tests (e.g., leap year quarters, season boundary shifts)
- [ ] Verify repeater behavior aligns with natural language expectations ('next quarter', 'this season')
🌿Good first issues
- Add repeater support for 'decade' or 'century' (e.g., 'in the 2020s'). File: create lib/chronic/repeaters/repeater_decade.rb following repeater_year.rb pattern, add dictionary entry in lib/chronic/dictionary.rb, and write tests in test/test_chronic.rb.
- Improve error messaging: currently Chronic.parse() returns nil on failure. Add optional :verbose => true flag to lib/chronic/parser.rb that logs which tokens failed to match, aiding debugging of unsupported formats.
- Add time zone inference for airport codes and military time zones (e.g., 'Zulu time', 'CST'). Extend lib/chronic/tags/time_zone.rb with additional zone aliases and write coverage tests in test/test_chronic.rb.
⭐Top contributors
Click to expand
Top contributors
- @davispuh — 43 commits
- @leejarvis — 19 commits
- @mkdynamic — 6 commits
- @TalkativeTree — 5 commits
- @petergoldstein — 4 commits
📝Recent commits
Click to expand
Recent commits
416d504— Merge pull request #408 from petergoldstein/feature/migrate_to_github_actions (davispuh)b39fa5e— Remove truffleruby version (petergoldstein)55d51e7— Loosen gem constraints (petergoldstein)7c06407— essage=Add Ruby 3.2 (petergoldstein)15921e5— Migrate CI to GitHub Actions (petergoldstein)2b1eae7— Merge pull request #307 from kakra/patch-1 (davispuh)0bf4c09— Provide test case for German date parsing (kakra)0da6254— Improve German date format parser (kakra)df691d9— Merge pull request #371 from kant/patch-1 (davispuh)3eb45f7— Minor fixes (proposal) (kant)
🔒Security observations
Chronic is a pure Ruby date parser with a relatively small attack surface. The library itself has no direct security risks related to injection, hardcoded credentials, or exposed infrastructure. However, the static analysis is limited by the absence of actual Gemfile content and partial code visibility. The main concerns are around dependency management and potential ReDoS vulnerabilities in regex parsing, which are common in natural language processing libraries. The library's straightforward purpose (date parsing without external I/O) reduces the likelihood of critical vulnerabilities. Recommendations focus on dependency auditing and input validation documentation for downstream users.
- Low · Missing dependency version constraints —
Gemfile, chronic.gemspec. The Gemfile content was not provided for analysis. Without reviewing the Gemfile and gemspec, it's impossible to verify if dependencies are pinned to specific secure versions or if they allow installation of vulnerable versions. Fix: Ensure all dependencies in Gemfile and gemspec are pinned to specific versions or use a lockfile (Gemfile.lock). Regularly audit dependencies using 'bundle audit' for known vulnerabilities. - Low · Potential ReDoS vulnerability in date parsing regex —
lib/chronic/tokenizer.rb, lib/chronic/parser.rb, lib/chronic/dictionary.rb. Natural language date parsing libraries often use complex regular expressions for tokenization and pattern matching. Without reviewing the actual regex patterns in tokenizer.rb and parser.rb, there's a risk of Regular Expression Denial of Service (ReDoS) attacks if user input is parsed without safeguards. Fix: Review all regex patterns for catastrophic backtracking potential. Consider using regex profiling tools. Implement input length limits and timeout mechanisms for parsing operations. Add validation for maximum input string length. - Low · No input validation documented —
lib/chronic.rb (main parse entry point). The codebase accepts natural language input strings for parsing. While Chronic is not a web service, if integrated into web applications, there's potential for abuse if input size or complexity isn't restricted. Fix: Document recommended input validation practices for library consumers. Consider adding optional parameters to limit parsing complexity (e.g., maximum input length, recursion depth limits).
LLM-derived; treat as a starting point, not a security audit.
👉Where to read next
- Open issues — current backlog
- Recent PRs — what's actively shipping
- Source on GitHub
Generated by RepoPilot. Verdict based on maintenance signals — see the live page for receipts. Re-run on a new commit to refresh.