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."

# 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

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])