Skip to content

mylanconnolly/chronix

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

29 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Chronix

Chronix is a natural language date parser. It is heavily inspired by Chronic.

Installation

Until a proper Hex package is available, you can add Chronix as a dependency in your mix.exs:

def deps do
  [
    {:chronix, github: "mylanconnolly/chronix"}
  ]
end

Usage

The main entry points are Chronix.parse/2, Chronix.parse!/2, and Chronix.expression?/1. All three share the same definition of "a valid Chronix expression."

  • parse/2 returns {:ok, %DateTime{}} on success or {:error, reason} on failure. It never raises.
  • parse!/2 returns the DateTime directly and raises ArgumentError on failure.
  • expression?/1 returns true if and only if parse/2 would succeed on the same input.
# Current dates (two equivalent formats)
iex> Chronix.parse("today")
{:ok, ~U[2025-01-27 11:59:03Z]}

iex> Chronix.parse("now")
{:ok, ~U[2025-01-27 11:59:03Z]}

# Future dates (two equivalent formats)
iex> Chronix.parse("in 2 minutes")
{:ok, ~U[2025-01-27 12:01:03Z]}  # 2 minutes from now

iex> Chronix.parse("2 minutes from now")
{:ok, ~U[2025-01-27 12:01:03Z]}  # same as above

iex> Chronix.parse("in 3 days")
{:ok, ~U[2025-01-30 11:59:03Z]}  # 3 days from now

# Past dates
iex> Chronix.parse("2 hours ago")
{:ok, ~U[2025-01-27 09:59:03Z]}  # 2 hours before now

# Weekday-based parsing
iex> Chronix.parse("next monday")
{:ok, ~U[2025-02-03 11:59:03Z]}  # Next Monday

iex> Chronix.parse("last friday")
{:ok, ~U[2025-01-24 11:59:03Z]}  # Previous Friday

# Using a reference date (applies to ALL relative expressions, including "today" / "now")
iex> reference = ~U[2025-01-27 00:00:00Z]
iex> Chronix.parse("in 1 day", reference_date: reference)
{:ok, ~U[2025-01-28 00:00:00Z]}
iex> Chronix.parse("today", reference_date: reference)
{:ok, ~U[2025-01-27 00:00:00Z]}

# Raising variant
iex> Chronix.parse!("in 1 day", reference_date: reference)
~U[2025-01-28 00:00:00Z]

# Validity check
iex> Chronix.expression?("in 3 days")
true
iex> Chronix.expression?("tomorrow")
false

Beginning and End of Durations

Chronix can parse expressions that refer to the beginning or end of a duration:

# Beginning of durations
iex> Chronix.parse("beginning of 2 days from now")
{:ok, ~U[2025-01-29 00:00:00.000000Z]}  # Start of the day, 2 days from now

iex> Chronix.parse("beginning of 1 week from now")
{:ok, ~U[2025-02-03 00:00:00.000000Z]}  # Monday 00:00:00, start of next week

iex> Chronix.parse("beginning of 2 months from now")
{:ok, ~U[2025-03-01 00:00:00.000000Z]}  # First day of the month, 2 months ahead

# End of durations
iex> Chronix.parse("end of 2 days from now")
{:ok, ~U[2025-01-29 23:59:59.999999Z]}  # Last microsecond of the day

iex> Chronix.parse("end of 1 week from now")
{:ok, ~U[2025-02-09 23:59:59.999999Z]}  # Sunday 23:59:59, end of next week

iex> Chronix.parse("end of 1 month from now")
{:ok, ~U[2025-02-28 23:59:59.999999Z]}  # Last microsecond of the last day of next month

# With reference date
iex> reference = ~U[2025-01-01 12:30:45Z]
iex> Chronix.parse("beginning of 1 year from now", reference_date: reference)
{:ok, ~U[2026-01-01 00:00:00.000000Z]}  # Start of next year
iex> Chronix.parse("end of 1 year from now", reference_date: reference)
{:ok, ~U[2026-12-31 23:59:59.999999Z]}  # End of next year

Supported formats

  • Single-token: "now", "today", "tomorrow", "yesterday"
  • Compound day aliases: "the day after tomorrow", "the day before yesterday" (the word "the" is optional)
  • Future: "in X <unit>s" or "X <unit>s from now"
  • Past: "X <unit>s ago"
  • Bare: "X <unit>s" (treated as future from the reference date)
  • Weekday: "next monday", "last friday", "this monday", "on monday". "this" and "on" resolve to the soonest upcoming occurrence (today itself if it matches).
  • Period: "next week" | "next month" | "next year" (and "last ...")
  • Pleonasms: "this week", "this month", "this year" (resolve to the reference date); "this morning" (09:00), "this afternoon" (15:00), "this evening" (19:00), "tonight" (20:00), "last night" (yesterday 20:00); "tomorrow morning", "yesterday evening", and all other {today,tomorrow,yesterday} × {morning,afternoon,evening,night} combinations.
  • Boundaries: "beginning of ...", "end of ..." applied to any of the above
  • Explicit dates: mm/dd/yyyy, dd/mm/yyyy, mm-dd-yyyy, dd-mm-yyyy, yyyy-mm-dd, yyyy/mm/dd (midnight UTC). Month and day components may be unpadded ("1/5/2024", "2024-1-5"); year must be four digits. Ambiguous three-component forms default to US-style (month first); pass endian: :eu to flip that.
  • Word dates: "January 1, 2025", "Jan 1 2025", "December 31st, 2024", "1 Jan 2025", "1st Jan, 2025", "the 15th of March 2024". Full month names and 3-letter abbreviations; ordinal day suffixes (1st, 2nd, 3rd, 4th) accepted but optional. Year is optional — omitted year defaults to the reference date's year ("March 15" → March 15 of this year).
  • ISO-8601 timestamps: "2024-12-25T15:30:00Z", "2024-12-25T15:30:00+02:00", "2024-12-25T15:30:00.123456Z". Non-UTC offsets are converted to UTC. A bare space ("2024-12-25 15:30:00Z") also works. A trailing offset is required — naive timestamps like "2024-12-25T15:30:00" are rejected (use "2024-12-25 at 15:30" instead).
  • Time-of-day: "noon", "midnight", "3pm", "3 p.m.", "3:15pm", "3:15:30pm", "15:30", "15:30:45". On its own, resolves to the reference date at that time.
  • Combined date + time: any date expression followed by " at " and a time — "tomorrow at 3pm", "next monday at noon", "2024-12-25 at 3pm", "in 3 days at 8am". Bare "at 3pm" is shorthand for today at that time.

Supported units: second, minute, hour, day, week, fortnight (= 14 days), month, quarter (= 3 months), year, decade (= 10 years), century (= 100 years). Each accepts the plural form as well.

Numbers may include commas for readability ("in 1,000 seconds") and can be fractional for fixed-duration units ("in 1.5 hours", "0.5 days ago"). Fractional months and years are rejected (no unambiguous conversion); "beginning of" / "end of" require integer durations. The words "a" and "an" are accepted as synonyms for 1 ("in a week", "an hour ago"). Numeric words work too: zero through twelve, teens, and tens (with compounds like "twenty one" or "thirty-five") — "in five days", "twenty years ago", "ninety nine seconds from now".

Parsing is case-insensitive and whitespace-tolerant. Contradictory phrases like "in 2 seconds ago" are rejected with {:error, _} rather than silently normalized.

Reference date

All relative expressions — including "today" and "now" — are resolved against the :reference_date option. If omitted, Chronix uses DateTime.utc_now/0. Pinning the reference date is the right way to make tests deterministic:

Chronix.parse("next monday", reference_date: ~U[2025-01-27 00:00:00Z])

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages