Age

Is someone over 80? A calendar question, answered with every day counted.

age.py · output · proof · check · try it in the playground


The question

Pat was born on 21 August 1944. Some benefits, discounts or rules start “after your 80th birthday”. It sounds easy, but calendars have leap years, months of different lengths and birthdays on 29 February, and we want an answer that shows its arithmetic.

On 1 October 2026, is Pat older than 80?


What we tell eyekind

Two facts: a birth date, and the date we ask about. Writing the date down (instead of reading the computer’s clock) means the answer and its proof are the same tomorrow.

fact(birth_date('pat_h', date(1944, 8, 21)))
fact(as_of(date(2026, 10, 1)))

# Without an explicit date, the reference date as_of/1 is used.
implied_by(age_above(Person, Duration), [as_of(Date), age_above(Person, Duration, Date)])

query(age_above(Person, years(80)))

A fact(...) is something we state as true; date(1944, 8, 21) is a date written as year, month, day. Capitalized names such as Person and Date are variables: placeholders for any value.

implied_by(conclusion, premise) is a backward rule, used on demand when a question needs it: a person’s age is above a duration if, on the as_of date, it is above that duration. A premise in square brackets needs all of its parts.

The last line is the question: find every Person whose age is above 80 years.


How the program counts

The rule for “older than N years on a date”, quoted from the program:

implied_by(
  age_above(Person, years(Years), Date),
  [
    is_int(Years),
    ge(Years, 0),
    birth_date(Person, Birth),
    date_day(Birth, Born),
    date_day(Date, Today),
    ge(Today, Born),
    anniversary(Birth, Years, Anniversary),
    date_day(Anniversary, Threshold),
    gt(Today, Threshold),
  ],
)

In words: the number of years must be a whole number, 0 or more; turn the birth date and the date into day numbers; the date must not be before the birth; find the anniversary that many years after the birth; and the date must come strictly after it. is_int, ge (“greater than or equal”) and gt (“greater than”) are built-in tests that eyekind evaluates directly. Strictly: on the 80th birthday itself Pat is exactly 80, not above it.

date_day gives each date its number in the Gregorian calendar, counting 1 January of year 1 as day 1. It adds up:

anniversary adds the years to the birth year, and clips the day to the month’s length (month_days), so a 29 February birthday falls on 28 February in a common year. All arithmetic is exact whole-number arithmetic, written as terms such as add, sub, mul, floordiv (division rounded down), mod (remainder) and min, and worked out by is_(Result, calculation).


What eyekind concludes

// Answers: each instance of a query() that holds.
age_above('pat_h', years(80))

Yes: Pat is older than 80 on 1 October 2026.

date day number
21 August 1944, Pat’s birth 709899
21 August 2024, Pat’s 80th birthday 739119
1 October 2026, the reference date 739890

739890 is greater than 739119, so the 80th birthday has passed.


The proof, line by line

eyekind --proof writes down how it reached the answer. This is the complete proof:

// Answers: each instance of a query() that holds.
age_above('pat_h', years(80))

// Clauses: the program clauses the steps cite, by clause number.
clause(1, fact(birth_date('pat_h', date(1944, 8, 21))))
clause(2, fact(as_of(date(2026, 10, 1))))
clause(3, implied_by(age_above(Person, Duration), [as_of(Date), age_above(Person, Duration, Date)]))
clause(4, implied_by(age_above(Person, years(Years), Date), [is_int(Years), ge(Years, 0), birth_date(Person, Birth), date_day(Birth, Born), date_day(Date, Today), ge(Today, Born), anniversary(Birth, Years, Anniversary), date_day(Anniversary, Threshold), gt(Today, Threshold)]))
clause(7, implied_by(anniversary(date(Y, M, D), Years, date(Year, M, Day)), [is(Year, add(Y, Years)), month_days(Year, M, Maximum), is(Day, min(D, Maximum))]))
clause(8, implied_by(date_day(date(Y, M, D), Ordinal), [is_int(Y), is_int(M), is_int(D), ge(Y, 1), month_days(Y, M, Maximum), ge(D, 1), le(D, Maximum), month_offset(M, Offset), leap_extra(Y, M, Extra), is(Previous, sub(Y, 1)), is(Ordinal, add(add(add(add(sub(add(mul(365, Previous), floordiv(Previous, 4)), floordiv(Previous, 100)), floordiv(Previous, 400)), Offset), Extra), D))]))
clause(11, implied_by(month_days(_0, M, Days), ordinary_month(M, Days)))
clause(18, fact(ordinary_month(8, 31)))
clause(20, fact(ordinary_month(10, 31)))
clause(24, implied_by(leap_year(Y), [eq(0, mod(Y, 4)), ne(0, mod(Y, 100))]))
clause(25, implied_by(common_year(Y), ne(0, mod(Y, 4))))
clause(28, implied_by(leap_extra(Y, M, 1), [gt(M, 2), leap_year(Y)]))
clause(29, implied_by(leap_extra(Y, M, 0), [gt(M, 2), common_year(Y)]))
clause(37, fact(month_offset(8, 212)))
clause(39, fact(month_offset(10, 273)))

// Steps: each goal, what justifies it (a clause, a builtin, a control, or an
// absence or collection taken on trust), the bindings and the goals it uses.
step(age_above('pat_h', years(80)), clause(3), {Person: 'pat_h', Duration: years(80), Date: date(2026, 10, 1)}, [as_of(date(2026, 10, 1)), age_above('pat_h', years(80), date(2026, 10, 1))])
step(as_of(date(2026, 10, 1)), clause(2), {}, [])
step(age_above('pat_h', years(80), date(2026, 10, 1)), clause(4), {Person: 'pat_h', Years: 80, Date: date(2026, 10, 1), Birth: date(1944, 8, 21), Born: 709899, Today: 739890, Anniversary: date(2024, 8, 21), Threshold: 739119}, [is_int(80), ge(80, 0), birth_date('pat_h', date(1944, 8, 21)), date_day(date(1944, 8, 21), 709899), date_day(date(2026, 10, 1), 739890), ge(739890, 709899), anniversary(date(1944, 8, 21), 80, date(2024, 8, 21)), date_day(date(2024, 8, 21), 739119), gt(739890, 739119)])
step(is_int(80), 'builtin', {}, [])
step(ge(80, 0), 'builtin', {}, [])
step(birth_date('pat_h', date(1944, 8, 21)), clause(1), {}, [])
step(date_day(date(1944, 8, 21), 709899), clause(8), {Y: 1944, M: 8, D: 21, Ordinal: 709899, Maximum: 31, Offset: 212, Extra: 1, Previous: 1943}, [is_int(1944), is_int(8), is_int(21), ge(1944, 1), month_days(1944, 8, 31), ge(21, 1), le(21, 31), month_offset(8, 212), leap_extra(1944, 8, 1), is(1943, sub(1944, 1)), is(709899, add(add(add(add(sub(add(mul(365, 1943), floordiv(1943, 4)), floordiv(1943, 100)), floordiv(1943, 400)), 212), 1), 21))])
step(is_int(1944), 'builtin', {}, [])
step(is_int(8), 'builtin', {}, [])
step(is_int(21), 'builtin', {}, [])
step(ge(1944, 1), 'builtin', {}, [])
step(month_days(1944, 8, 31), clause(11), {_0: 1944, M: 8, Days: 31}, [ordinary_month(8, 31)])
step(ordinary_month(8, 31), clause(18), {}, [])
step(ge(21, 1), 'builtin', {}, [])
step(le(21, 31), 'builtin', {}, [])
step(month_offset(8, 212), clause(37), {}, [])
step(leap_extra(1944, 8, 1), clause(28), {Y: 1944, M: 8}, [gt(8, 2), leap_year(1944)])
step(gt(8, 2), 'builtin', {}, [])
step(leap_year(1944), clause(24), {Y: 1944}, [eq(0, mod(1944, 4)), ne(0, mod(1944, 100))])
step(eq(0, mod(1944, 4)), 'builtin', {}, [])
step(ne(0, mod(1944, 100)), 'builtin', {}, [])
step(is(1943, sub(1944, 1)), 'builtin', {}, [])
step(is(709899, add(add(add(add(sub(add(mul(365, 1943), floordiv(1943, 4)), floordiv(1943, 100)), floordiv(1943, 400)), 212), 1), 21)), 'builtin', {}, [])
step(date_day(date(2026, 10, 1), 739890), clause(8), {Y: 2026, M: 10, D: 1, Ordinal: 739890, Maximum: 31, Offset: 273, Extra: 0, Previous: 2025}, [is_int(2026), is_int(10), is_int(1), ge(2026, 1), month_days(2026, 10, 31), ge(1, 1), le(1, 31), month_offset(10, 273), leap_extra(2026, 10, 0), is(2025, sub(2026, 1)), is(739890, add(add(add(add(sub(add(mul(365, 2025), floordiv(2025, 4)), floordiv(2025, 100)), floordiv(2025, 400)), 273), 0), 1))])
step(is_int(2026), 'builtin', {}, [])
step(is_int(10), 'builtin', {}, [])
step(is_int(1), 'builtin', {}, [])
step(ge(2026, 1), 'builtin', {}, [])
step(month_days(2026, 10, 31), clause(11), {_0: 2026, M: 10, Days: 31}, [ordinary_month(10, 31)])
step(ordinary_month(10, 31), clause(20), {}, [])
step(ge(1, 1), 'builtin', {}, [])
step(le(1, 31), 'builtin', {}, [])
step(month_offset(10, 273), clause(39), {}, [])
step(leap_extra(2026, 10, 0), clause(29), {Y: 2026, M: 10}, [gt(10, 2), common_year(2026)])
step(gt(10, 2), 'builtin', {}, [])
step(common_year(2026), clause(25), {Y: 2026}, [ne(0, mod(2026, 4))])
step(ne(0, mod(2026, 4)), 'builtin', {}, [])
step(is(2025, sub(2026, 1)), 'builtin', {}, [])
step(is(739890, add(add(add(add(sub(add(mul(365, 2025), floordiv(2025, 4)), floordiv(2025, 100)), floordiv(2025, 400)), 273), 0), 1)), 'builtin', {}, [])
step(ge(739890, 709899), 'builtin', {}, [])
step(anniversary(date(1944, 8, 21), 80, date(2024, 8, 21)), clause(7), {Y: 1944, M: 8, D: 21, Years: 80, Year: 2024, Day: 21, Maximum: 31}, [is(2024, add(1944, 80)), month_days(2024, 8, 31), is(21, min(21, 31))])
step(is(2024, add(1944, 80)), 'builtin', {}, [])
step(month_days(2024, 8, 31), clause(11), {_0: 2024, M: 8, Days: 31}, [ordinary_month(8, 31)])
step(is(21, min(21, 31)), 'builtin', {}, [])
step(date_day(date(2024, 8, 21), 739119), clause(8), {Y: 2024, M: 8, D: 21, Ordinal: 739119, Maximum: 31, Offset: 212, Extra: 1, Previous: 2023}, [is_int(2024), is_int(8), is_int(21), ge(2024, 1), month_days(2024, 8, 31), ge(21, 1), le(21, 31), month_offset(8, 212), leap_extra(2024, 8, 1), is(2023, sub(2024, 1)), is(739119, add(add(add(add(sub(add(mul(365, 2023), floordiv(2023, 4)), floordiv(2023, 100)), floordiv(2023, 400)), 212), 1), 21))])
step(is_int(2024), 'builtin', {}, [])
step(ge(2024, 1), 'builtin', {}, [])
step(leap_extra(2024, 8, 1), clause(28), {Y: 2024, M: 8}, [gt(8, 2), leap_year(2024)])
step(leap_year(2024), clause(24), {Y: 2024}, [eq(0, mod(2024, 4)), ne(0, mod(2024, 100))])
step(eq(0, mod(2024, 4)), 'builtin', {}, [])
step(ne(0, mod(2024, 100)), 'builtin', {}, [])
step(is(2023, sub(2024, 1)), 'builtin', {}, [])
step(is(739119, add(add(add(add(sub(add(mul(365, 2023), floordiv(2023, 4)), floordiv(2023, 100)), floordiv(2023, 400)), 212), 1), 21)), 'builtin', {}, [])
step(gt(739890, 739119), 'builtin', {}, [])

It has three parts:

There are two kinds of justification. clause(N) means “by line N of the program”. 'builtin' means a test or calculation, such as ge(80, 0) or is(1943, sub(1944, 1)); the checker does not take it on trust, it redoes it. Read the 54 steps as sentences:


Checked: what the report says

A proof is only worth something if someone else can check it. eyekind -c generates the proof and checks it against the program; --check-proof checks a saved proof. The checker never reasons by itself: it only confirms each step against the program. This is the complete report:

// Checked answers: these follow from the input program.
age_above('pat_h', years(80))

// C1-C8: resolution, acyclicity, justification, coverage,
// re-decision, boundary consistency, relevance and witness integrity.
condition('C1', 'resolution', 'ok', 21)
condition('C2', 'well_founded', 'ok', 54)
condition('C3', 'justification', 'ok', 54)
condition('C4', 'coverage', 'ok', 62)
condition('C5', 're_decision', 'ok', 33)
condition('C6', 'boundary_consistency', 'ok', 0)
condition('C7', 'relevance', 'ok', 55)
condition('C8', 'witness_integrity', 'ok', 0)

// Proof statistics: recorded steps and how they were checked.
steps(54)
verified(21)
recomputed(33)
composed(0)
trusted(0)
claims(1)

// This verdict applies to the claims above; it does not establish completeness.
verdict('checked')

Each condition line names a check, says 'ok', and counts what it looked at:

The statistics: 54 steps, 21 verified against the program, 33 recomputed, none taken on trust, 1 claim. The verdict is checked: Pat’s age is above 80 on 1 October 2026, down to the last day. As the last comment says, this confirms the claim; it does not say the program has no other conclusions.


Try it

eyekind examples/age.py
eyekind --goal "age_above('pat_h', years(80), date(2024, 8, 22))" examples/age.py
eyekind --goal "age_days('pat_h', date(2026, 10, 1), Days)" examples/age.py

The second asks about 22 August 2024, the day after the 80th birthday: it holds. The last one counts days and answers age_days('pat_h', date(2026, 10, 1), 29991): 739890 − 709899.

Or open it in the playground. Change as_of to date(2024, 8, 21), the 80th birthday itself: there is no answer, because on that day Pat is exactly 80, not above 80. Change it to date(2024, 8, 22), one day later, and the answer comes back.


Takeaway

Date arithmetic is where small mistakes hide. Here each day number, each leap-year decision and each comparison is written down and rechecked, so a “yes” can be audited down to the last day.