PtcRunner.Temporal (PtcRunner v0.11.0)

Copy Markdown View Source

Normalize Elixir temporal structs (DateTime, NaiveDateTime, Date, Time) to ISO 8601 strings before they cross any boundary into LLM-visible territory.

Why this exists

Elixir's Inspect protocol renders temporal structs with sigil syntax:

iex> inspect(~U[2026-05-03 09:14:00Z])
"~U[2026-05-03 09:14:00Z]"

That's idiomatic Elixir, but every downstream consumer that expects a parseable date string (the LLM, JSON Schema validators, (java.util.Date. ...) in PTC-Lisp) breaks when it sees the sigil. ISO 8601 is the universal lingua franca for temporal data.

This module is the central seam: any code that exposes Elixir data to an LLM or to PTC-Lisp should normalize through iso8601/1 (for known scalar position) or walk/1 (for arbitrary nested data like tool results).

Functions

  • iso8601/1 — convert a single value. Pass-through for non-temporal terms.
  • walk/1 — recursively normalize temporal structs inside maps and lists, leaving everything else alone.

Examples

iex> PtcRunner.Temporal.iso8601(~U[2026-05-03 09:14:00Z])
"2026-05-03T09:14:00Z"

iex> PtcRunner.Temporal.iso8601(~D[2026-05-03])
"2026-05-03"

iex> PtcRunner.Temporal.iso8601(~N[2026-05-03 09:14:00])
"2026-05-03T09:14:00"

iex> PtcRunner.Temporal.iso8601(~T[09:14:00])
"09:14:00"

iex> PtcRunner.Temporal.iso8601("hello")
"hello"

iex> PtcRunner.Temporal.iso8601(nil)
nil

iex> PtcRunner.Temporal.walk(%{at: ~D[2026-05-03], items: [~T[09:14:00]]})
%{at: "2026-05-03", items: ["09:14:00"]}

Summary

Functions

Convert a temporal struct to its ISO 8601 string. Pass-through for everything else (including non-temporal structs like user-defined ones).

Recursively walk a value, normalizing any temporal structs found inside maps and lists. Non-temporal structs are left untouched at struct boundaries (we don't dive into them) since their internal shape is the user's contract.

Functions

iso8601(dt)

@spec iso8601(term()) :: term()

Convert a temporal struct to its ISO 8601 string. Pass-through for everything else (including non-temporal structs like user-defined ones).

walk(dt)

@spec walk(term()) :: term()

Recursively walk a value, normalizing any temporal structs found inside maps and lists. Non-temporal structs are left untouched at struct boundaries (we don't dive into them) since their internal shape is the user's contract.

Use this for tool results and other arbitrary data that gets JSON-encoded or otherwise serialized for the LLM.