Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Chapter 9: Marking Time

Biscuit was due for a booster. Dr. Portbridge was sure of it, but the appointment book fought her every step. One entry read "15/03/2025", another "March 15", a third "2025-03-15". To her they were the same afternoon; to the computer they were three unrelated strings. She couldn’t ask “which vaccinations are overdue?” because a string doesn’t know it’s a date. It was time to give dates real meaning.


The problem with string dates

Back in Chapter 3 we stored a date as a string and promised a better way later. This is later.

A string like "15/03/2025" is opaque. Nothing stops a typo ("2025-13-40"), nothing agrees on the format, and, worst of all, the date carries no meaning the machine can compute with. Dolfin now has four temporal types that fix this:

TypeHoldsExample value
dateA calendar daydate(March 15th 2025)
timeA wall-clock timetime(2:30 PM)
date_timeA day and a timedate_time(June 1st 2026, 14:30)
durationA length of timeduration(1y 6mo)

The value inside the parentheses is a smart literal: you write the date the way a human would, and Dolfin parses it into the exact form an RDF datastore expects (xsd:date, xsd:time, xsd:dateTime, xsd:duration).

Writing dates

The most natural way is to spell it out. Month names are case-insensitive, and the ordinal ending (st, nd, rd, th) is optional:


date(March 15th 2025)     # → 2025-03-15
date(15 March 2025)       # → 2025-03-15
date(Mar 15 2025)         # → 2025-03-15

You can also write it numerically. But 01/06/2025 is ambiguous (is it June 1st or January 6th?), so Dolfin makes you say which order the fields are in with an as mask:


date(15/03/2025 as d/m/y) # → 2025-03-15
date(2025-03-15 as y-m-d) # → 2025-03-15

The separator (/, -, or .) must match on both sides.

Set the locale once

Typing as d/m/y on every date gets old fast. The @locale directive, placed at the top of the file, sets the default field order for the whole file:


@locale d/m/y

fact rabies_shot a Vaccination
  given_on date(15/03/2025)   # no mask needed the file already knows

Times and timestamps

A time is 24-hour by default; add AM/PM for the 12-hour clock. An appointment slot pairs a day with a time in a date_time:


time(14:30)                          # → 14:30:00
time(2:30 PM)                        # → 14:30:00
date_time(June 1st 2026, 2:30 PM)    # → 2026-06-01T14:30:00

Clinics keep local time, so you can pin a timezone, either inline, or file-wide with @timezone:


@timezone Europe/Brussels

fact checkup a Appointment
  scheduled_for date_time(June 1st 2026, 9:00 AM)  # → …T09:00:00+01:00

An inline offset (time(9:00 AM +02:00)) always overrides the file default.

Durations

A duration measures a span, how long a vaccine stays valid, how far ahead to send a reminder. Write one or more <number><unit> terms in any order:

ymowdhmins
yearsmonthsweeksdayshoursminutesseconds

duration(3y)          # → P3Y      a rabies shot good for three years
duration(2w)          # → P2W      send the reminder two weeks ahead
duration(1h 30min)    # → PT1H30M  a long surgery slot

(The month unit is mo, so it never collides with minutes.)

Upgrading the clinic

Now the appointment book earns its keep. The Appointment gets a real date_time, and a new Vaccination concept records when a shot was given and how long it lasts:


concept Appointment:
  has animal: one Animal
  has scheduled_for: one date_time   # was: has date: string
  has reason: string

concept Vaccination:
  has animal: one Animal
  has vaccine_name: one string
  has given_on: one date
  has valid_for: one duration

With that, Biscuit’s booster is now a date the system understands, paired with a validity span:


fact rabies_shot a Vaccination
  animal :biscuit
  vaccine_name "Rabies"
  given_on date(March 15th 2025)
  valid_for duration(3y)

Because given_on is a real date and valid_for a real duration, the machine can now do what Dr. Portbridge couldn’t do by squinting at napkins: compare them, and work out exactly when the protection runs out. That arithmetic is the seed of the alerts we build next chapter.

The story so far


package <http://happypaws.com/clinic>:
  dolfin_version "1"
  version "0.1.0"
  author "Dr. Helen Portbridge"
  description "The Happy Paws veterinary clinic data model"

concept Animal:
  has name: string
  has species: string
  has age: int
  has owner: Owner
  
concept Owner:
  has first_name: string
  has last_name: string
  has phone: string

concept Vaccination:
  has animal: one Animal
  has vaccine_name: one string
  has given_on: one date
  has valid_for: one duration

fact biscuit a Animal
  name "Biscuit"
  species "Dog"
  age 5

fact rabies_shot a Vaccination
  animal :biscuit
  vaccine_name "Rabies"
  given_on date(March 15th 2025)
  valid_for duration(3y)

Try it

Add an Appointment fact for Biscuit’s booster with a real date_time, and give the Vaccination a reminder_lead of two weeks:


@locale d/m/y

concept Animal:
  has name: string
  has species: string
  has age: int
  has owner: Owner
  
concept Owner:
  has first_name: string
  has last_name: string
  has phone: string

concept Vaccination:
  has animal: one Animal
  has vaccine_name: one string
  has given_on: one date
  has valid_for: one duration
  # add: has reminder_lead: optional duration

# Add your booster appointment and vaccination facts here

Dr. Portbridge closed the appointment book (the real one, the one in the computer) and smiled. Every date now knew it was a date. But knowing the dates wasn’t the same as being warned in time. What she really wanted was for the system to speak up on its own: “Biscuit’s rabies protection expires next month.” For that, the clinic would have to start thinking.