Dragon

Dates, Times, and Math

This chapter covers the part of Dragon's standard library that deals with clocks, calendars, and numbers: datetime, time, calendar, math, random, statistics, and fractions.

These modules are written in Dragon (with thin extern "C" bridges to libc and libm where the work genuinely belongs to the C runtime). Because they are ordinary Dragon source, the surfaces are typed and concrete - and they do not always match Python name-for-name. Each section below documents what the code actually exposes. Read the "Differs from Python" note in each section before you port code over from CPython.

A module is used by importing it and qualifying the call (import math then math.sqrt(2.0)), or by pulling names in directly (from math import sqrt). Both forms are shown throughout.


datetime - dates, times, and durations

The datetime module provides four classes - timedelta, date, time_of_day, and datetime - plus a set of module-level constructor functions. Everything is UTC-only; there is no tzinfo and no local-timezone conversion in this version.

The classes are plain Dragon classes, so construction is positional and fully typed:

ConstructorSignature
timedelta(days, seconds, microseconds)all int; normalized to canonical form
date(year, month, day)all int
time_of_day(hour, minute, second, microsecond)all int
datetime(year, month, day, hour, minute, second, microsecond)all int

Current-time values come from free functions, not classmethods:

FunctionReturns
datetime_now() / datetime_utcnow()current UTC datetime
datetime_fromtimestamp(ts: float)datetime from a Unix timestamp
date_today()today's UTC date
date_fromtimestamp(ts: float)date from a Unix timestamp

The classes carry the methods you would expect: isoformat() on date, time_of_day, and datetime; timestamp() and date_part()/time_part() on datetime; toordinal() on date; and total_seconds() on timedelta. Arithmetic is wired through operator dunders, so + and - work directly: datetime + timedelta yields a datetime, and datetime - datetime yields a timedelta.

import datetime

const dt: datetime.datetime = datetime.datetime(2026, 6, 1, 14, 30, 5, 0)
print(dt.isoformat())            # 2026-06-01T14:30:05
print(dt.date_part().isoformat()) # 2026-06-01
print(dt.timestamp())            # 1780324205.0

const td: datetime.timedelta = datetime.timedelta(7, 3600, 0)
print(td.total_seconds())        # 608400.0

const later: datetime.datetime = dt + td
print(later.isoformat())         # 2026-06-08T15:30:05

const gap: datetime.timedelta = later - dt
print(gap.total_seconds())       # 608400.0

The constructor functions read the wall clock through time.now_float():

from datetime import datetime, date, datetime_now, date_today, datetime_fromtimestamp

const now: datetime = datetime_now()
print(now.year > 2000)           # True

const today: date = date_today()
print(today.isoformat())         # e.g. 2026-06-01

const epoch: datetime = datetime_fromtimestamp(0.0)
print(epoch.isoformat())         # 1970-01-01T00:00:00

Differs from Python. There are no classmethods: use datetime_now() instead of datetime.now(), and date_today() instead of date.today(). The time-of-day class is named time_of_day, not time (the latter is a sibling module). There is no strftime/strptime - formatting is isoformat() only. The whole module is UTC-only: no tzinfo, no astimezone, and datetime_utcnow() is just an alias for datetime_now().


time - clocks and sleeping

The time module wraps the POSIX clocks. The reusable surface is a set of free functions; the bare time()/sleep() names are extern "C" ABI declarations, so call the Dragon wrappers below rather than those.

FunctionReturnsMeaning
now()intseconds since the Unix epoch
now_float()floatepoch seconds with sub-second precision (CLOCK_REALTIME)
monotonic()floatmonotonic clock, immune to wall-clock changes
perf_counter()floathigh-resolution counter (alias for monotonic)
process_time()floatCPU time used by this process
sleep_secs(seconds: int)intsleep whole seconds
sleep_ms(milliseconds: int)intsleep milliseconds
sleep_float(seconds: float)intsleep with sub-second precision

Use monotonic() (or perf_counter()) for measuring elapsed time, and now()/now_float() when you need a real-world timestamp.

import time

const start: float = time.monotonic()
time.sleep_float(0.01)
const elapsed: float = time.monotonic() - start
print(elapsed >= 0.0)            # True

print(time.now() > 1700000000)   # True

Differs from Python. The single Python time.sleep(secs) is split into three typed wrappers - sleep_secs, sleep_ms, and sleep_float - because Dragon does not overload on argument type. time.time() is now() / now_float(). The sleep wrappers return an int status code, not None.


calendar - calendar math and formatting

calendar is pure date arithmetic with no C dependencies. Weekdays are numbered 0 = Monday … 6 = Sunday; months are 1 = January … 12 = December. The module exports named constants (MONDAYSUNDAY, JANUARYDECEMBER) for both.

FunctionReturns
isleap(year: int)bool
leapdays(y1: int, y2: int)int - leap years in [y1, y2)
weekday(year, month, day)int - 0=Monday … 6=Sunday
monthrange(year, month)list[int] - [first_weekday, num_days]
monthcalendar(year, month)list[list[int]] - weeks of day numbers (0 = padding)
day_name(d) / day_abbr(d)str
month_name(m) / month_abbr(m)str
formatmonth(year, month)str - multi-line month grid
formatyear(year)str - all twelve months
import calendar

print(calendar.isleap(2024))         # True
print(calendar.weekday(2026, 6, 1))  # 0  (a Monday)
print(calendar.month_name(6))        # June

const mr: list[int] = calendar.monthrange(2026, 6)
print(mr[0])                         # 0   first day is Monday
print(mr[1])                         # 30  days in June

print(calendar.formatmonth(2026, 6))
# Output:
#      June 2026
# Mo Tu We Th Fr Sa Su
#  1  2  3  4  5  6  7
#  8  9 10 11 12 13 14
# 15 16 17 18 19 20 21
# 22 23 24 25 26 27 28
# 29 30

Differs from Python. monthrange returns a two-element list[int] rather than a tuple, and the day/month name lookups are functions (day_name(0)) rather than the array-like calendar.day_name[0] objects in Python. There is no Calendar/TextCalendar/HTMLCalendar class hierarchy - formatmonth and formatyear are the formatting surface.


math - numeric functions

math is a libm bridge plus a layer of idiomatic Dragon wrappers. All the trigonometric, exponential, and rounding functions take and return float. The integer helpers (gcd, factorial, …) take and return int.

Constants: pi, e, tau, inf, nan.

The libm-backed functions include sqrt, fabs, fmod, remainder, pow, exp, exp2, expm1, log, log2, log10, log1p, cbrt, hypot; the trig family sin, cos, tan, asin, acos, atan, atan2; the hyperbolic family sinh, cosh, tanh, asinh, acosh, atanh; rounding ceil, floor, trunc, round, copysign; and the classifiers isnan, isinf, isfinite.

The Dragon-side wrappers add the conveniences Python keeps in math:

FunctionReturnsNotes
degrees(x) / radians(x)floatangle conversion
dist(p, q)floatEuclidean distance over list[float]
clamp(x, low, high) / lerp(a, b, t)float
gcd(a, b) / lcm(a, b)int
factorial(n)int-1 for negative input
comb(n, k) / perm(n, k)int
isqrt(n)intinteger square root
log_int(x, base)intinteger logarithm
sum_float(items) / prod(items)floatover list[float]
import math

print(math.sqrt(2.0))        # 1.4142135623730951
print(math.floor(3.7))       # 3.0
print(math.ceil(3.2))        # 4.0
print(math.hypot(3.0, 4.0))  # 5.0
print(math.gcd(12, 18))      # 6
print(math.factorial(5))     # 120
print(math.isqrt(50))        # 7
print(math.comb(5, 2))       # 10
print(math.degrees(math.pi)) # 180.0

Differs from Python. floor, ceil, trunc, and round come straight from libm and therefore return a float, not an int - wrap them in int(...) if you need an integer. round rounds half away from zero (libm round), not Python's banker's rounding. factorial(-1) returns -1 rather than raising. The classifiers (isnan, etc.) return an intc (0/1), not a bool.


random - pseudo-random numbers

random wraps libc's rand()/srand(). It is fast but not cryptographically secure - reach for secrets when you need unpredictable values.

FunctionReturnsMeaning
seed(s: int)-seed the generator deterministically
seed_time()-seed from the current time
random()floata value in [0.0, 1.0)
randint(a, b)intN with a <= N <= b (inclusive)
randrange(start, stop)intN with start <= N < stop
uniform(a, b)floatN with a <= N <= b
choice(items: list[str])stra random element
choice_int(items: list[int])inta random element
shuffle(items: list[int])list[int]a new shuffled list
sample(items: list[int], k)list[int]k elements without replacement
import random

random.seed(42)                       # reproducible runs

const r: float = random.random()
print(r >= 0.0 and r < 1.0)           # True

const roll: int = random.randint(1, 6)
print(roll >= 1 and roll <= 6)        # True

const colors: list[str] = ["red", "green", "blue"]
print(random.choice(colors) in colors)  # True

const order: list[int] = random.shuffle([1, 2, 3, 4, 5])
print(len(order))                     # 5

The exact draws above are not shown because they depend on the seed and the platform's rand(). Call random.seed(n) first if you need a run to be reproducible.

Differs from Python. choice is monomorphic per element type: choice(list[str]) and choice_int(list[int]) are separate functions because Dragon does not have a runtime-polymorphic sequence type. shuffle returns a new list rather than shuffling in place (and is int-only), and sample is int-only. The generator is libc's, so the stream for a given seed will not match CPython's Mersenne Twister.


statistics - summary statistics

statistics operates on list[float] (with int-list variants where it matters). Functions raise ValueError on empty input, and variance/stdev require at least two points.

FunctionReturnsNotes
mean(data) / fmean(data)floatarithmetic mean
geometric_mean(data)floatpositive values only
harmonic_mean(data)floatpositive values only
median(data)floataverage of the middle two when even
median_low(data) / median_high(data)float
mode_float(data) / mode_int(data)float / intfirst value seen wins a tie
variance(data) / stdev(data)floatsample (divides by n-1)
pvariance(data) / pstdev(data)floatpopulation (divides by n)
quantiles(data, n)list[float]n-1 cut points (exclusive method)
import statistics

const data: list[float] = [2.0, 4.0, 4.0, 4.0, 5.0, 5.0, 7.0, 9.0]
print(statistics.mean(data))      # 5.0
print(statistics.median(data))    # 4.5
print(statistics.variance(data))  # 4.571428571428571
print(statistics.stdev(data))     # 2.138089935299395
print(statistics.pstdev(data))    # 2.0
print(statistics.mode_float(data))# 4.0

print(statistics.quantiles([1.0, 2.0, 3.0, 4.0, 5.0, 6.0], 4))
# [1.5, 3.0, 4.5]

Differs from Python. mode is split into mode_float and mode_int. The data argument is a concrete list[float] (or list[int]), not an arbitrary iterable, and there is no NormalDist class or correlation/covariance suite. geometric_mean and harmonic_mean reject non-positive values with a ValueError.


fractions - exact rational arithmetic

fractions provides a Fraction class storing reduced rationals with the sign on the numerator. It is pure Dragon and uses named methods rather than operators.

Fraction(num: int, den: int) constructs and immediately reduces; a zero denominator raises ValueError. The module-level from_float(f: float) approximates a float via continued fractions (denominator capped at 10^6).

MethodReturnsMeaning
add / sub / mul / divFractionarithmetic (each takes one Fraction)
eq / lt / le / gt / geboolcomparison
to_float()floatdecimal value
to_str()str"num/den", or just "num" when the denominator is 1
abs() / neg() / reciprocal()Fraction
limit_denominator(max_den)Fractionclosest fraction with a bounded denominator
from fractions import Fraction, from_float

const half: Fraction = Fraction(1, 2)
const third: Fraction = Fraction(1, 3)

const total: Fraction = half.add(third)
print(total.to_str())      # 5/6
print(total.to_float())    # 0.8333333333333334

print(Fraction(6, 8).to_str())   # 3/4   (auto-reduced)
print(half.lt(third))            # False

const quarter: Fraction = from_float(0.25)
print(quarter.to_str())          # 1/4

Differs from Python. There are no operator overloads - write a.add(b) and a.lt(b), not a + b and a < b. Construction is Fraction(num, den) with two integer arguments; there is no string parser (Fraction("3/4")) and no mixed-type construction. to_str() / to_float() replace Python's str() / float() conversions, and the numerator/denominator are the public fields num and den.


At a glance

ModuleImportKey surfaceWatch out for
datetimeimport datetimedatetime/date/timedelta/time_of_day, datetime_now(), date_today()UTC-only; no strftime; current time via free functions
timeimport timenow(), now_float(), monotonic(), sleep_float()sleep is split into sleep_secs/_ms/_float
calendarimport calendarisleap, weekday, monthrange, formatmonthweekday 0=Monday; monthrange returns a list
mathimport mathsqrt, floor, gcd, factorial, comb, pifloor/ceil/round return float
randomimport randomseed, random, randint, choice, shufflenot secure; choice/shuffle typed per element
statisticsimport statisticsmean, median, stdev, variance, mode_floattakes list[float]; mode split by type
fractionsfrom fractions import FractionFraction(n, d), add/mul/to_strnamed methods, no operator overloads

With dates, clocks, and numbers covered, the next chapter turns to the tools for composing and transforming data: Functional Tools.