Getting Started with EK9 · Chapter 7
Text, Numbers, Time and Money
So far you have held text in a String, whole numbers in an Integer and decimals in a Float.
Real programs also deal in prices, dates, durations and measurements. In many languages
those have no type of their own: a price is "just a number", a date "just a string", and the awkward
details — pennies, leap years, currencies, units — are left to you or to a library you have to find.
EK9 ships a set of built-in types so that the things programs talk about have a proper, safe home. You do not import anything; they are there, and they know how to behave. This chapter introduces them.
The ones you already know
String holds text. You build it with double quotes, and you place values into it with
backtick interpolation:
name <- "Ada" greeting <- `Hello, ${name}`
A String can also join, trim, change case and search; later chapters use those as they need them.
Integer holds whole numbers; Float holds numbers with a decimal point. They do the
arithmetic you expect with +, -, * and /. One warning leads into the next section: do not
use Float for money. A Float cannot hold 0.10 exactly, so pennies drift. EK9 gives money its
own type.
Money
Here is a small shop receipt. It totals a few items and prints the bill:
#!ek9 defines module book.receipt defines program Receipt() stdout <- Stdout() enGB <- Locale("en_GB") applePrice <- 0.35#GBP breadPrice <- 1.10#GBP milkPrice <- 0.85#GBP apples <- 6 loaves <- 2 bottles <- 1 appleLine <- applePrice * apples breadLine <- breadPrice * loaves milkLine <- milkPrice * bottles total <- appleLine + breadLine + milkLine stdout.println(`Apples x${apples} ${enGB.format(appleLine)}`) stdout.println(`Bread x${loaves} ${enGB.format(breadLine)}`) stdout.println(`Milk x${bottles} ${enGB.format(milkLine)}`) stdout.println(`Total ${enGB.format(total)}`)
It prints:
Apples x6 £2.10
Bread x2 £2.20
Milk x1 £0.85
Total £5.15
A money literal is a number, a #, and a three-letter currency code: 0.35#GBP, 1.10#GBP.
The currency is part of the value, not a note kept on the side. The codes are the international
standard ones: GBP, USD, EUR, and so on.
The arithmetic is exact. applePrice * apples multiplies a price by a whole number of apples and
gives money back; appleLine + breadLine + milkLine adds money to money. There is no rounding drift,
because Money keeps exact decimal values, not the approximations a Float would.
Formatting is a job for a Locale, which represents a place. Locale("en_GB") knows that
British money is shown with a £ and two decimal places, so enGB.format(total) gives £5.15. A
different locale presents the same value for its own country. You never assemble a currency string by
hand.
Now the safety point. What is ten pounds plus five dollars? It is not a number: the question has no answer without an exchange rate. So EK9 does not invent one:
pounds <- 10#GBP dollars <- 5#USD mixed <- pounds + dollars // different currencies
mixed comes back unset: not zero, not fifteen of something, but the "no meaningful value" state
from Values and the Three States. Check it with ? and you get false. Silently adding
incompatible currencies cannot produce a plausible wrong number. When you do need to combine
currencies, you convert first, with a real rate, and say so.
Dates, times and durations
Time is the other place where "just a string" and "just a number" cause trouble: leap years, month lengths, time zones. EK9 has proper types for it, with literals you write directly:
- a
Dateis a calendar day:2026-01-15. - a
Timeis a time of day:14:25or09:30:00. - a
DateTimeis both, with a zone:2026-01-15T10:30:00Z. - a
Durationis a span of time, written in the international (ISO 8601) style:P1M15Dis "one month and fifteen days",PT1H30Mis "one hour and thirty minutes".
They work together through operators. Adding a duration to a date gives a date; subtracting one date from another gives the duration between them:
#!ek9 defines module book.dates defines program Deadline() stdout <- Stdout() enGB <- Locale("en_GB") start <- 2026-01-15 notice <- P1M15D due <- start + notice stdout.println(`Start: ${enGB.mediumFormat(start)}`) stdout.println(`Due: ${enGB.mediumFormat(due)}`) span <- due - start stdout.println(`Notice period is ${span}`) stdout.println(`Due falls on day-of-week ${due.dayOfWeek()}`)
which prints:
Start: 15 Jan 2026
Due: 2 Mar 2026
Notice period is P1M15D
Due falls on day-of-week 1
start + notice moved the date on by a month and fifteen days, stepping correctly over the length
of January without you writing any rule about month lengths. due - start gave back the span between
the two dates. A Date can also describe itself: due.dayOfWeek() gives 1 (a Monday), and there
are matching .day(), .month() and .year() methods. As with money, a Locale does the
formatting: mediumFormat gives 15 Jan 2026, and shortFormat, longFormat and fullFormat
give other lengths.
Measures, colours and more
The same idea extends to types many languages do not provide at all.
A Dimension is a measurement with a unit: 20cm, 10px, 4.5em, 8mile. It does arithmetic,
with the same safety as money. Here is what happens when the units do not match:
#!ek9 defines module book.measures defines program Measures() stdout <- Stdout() width <- 20cm doubled <- width * 2 wider <- width + 5cm mismatch <- width + 3px stdout.println(`Width: ${width}`) stdout.println(`Doubled: ${doubled}`) stdout.println(`Wider: ${wider}`) stdout.println(`20cm + 3px is set? ${mismatch?}`)
Width: 20.0cm
Doubled: 40.0cm
Wider: 25.0cm
20cm + 3px is set? false
20cm doubled is 40.0cm; 20cm + 5cm is 25.0cm. But 20cm + 3px mixes centimetres and pixels,
which has no meaning, so, as with the mixed currencies, the answer is unset. The pattern repeats
across the language: when an operation does not make sense, EK9 gives you a value you can check,
not a wrong answer that looks right.
Two more, so you know they exist. A Colour is written as a hex value, #FF5500 (or, with an
alpha channel first, #80FF5500, where the leading 80 is the transparency), and can be
lightened, blended and taken apart. A Resolution captures pixel density, 96dpi or 300dpc,
for print and screen work.
The common thread
These types share a few habits you can rely on:
- Every value answers
$, "give me this as text", so any of them fits into a backtick string. - Every value answers
?, "are you set?", so you can always ask whether it holds something meaningful. - A
Localeturns values into text suited to a place — the right currency symbol, the right date order — without you formatting by hand. - When an operation has no meaning — pounds plus dollars, centimetres plus pixels — the result is unset: a gap you can test for, not a plausible wrong answer.
A type that knows what it is can refuse to give you a wrong answer. That idea runs through the whole book.
Naming fixed values
Some values never change: the number of retries you allow, a greeting, the price of a ticket, the
time the doors open. Instead of scattering a bare 3 or "Hello" through your code, you can name a
fixed value once, with defines constant:
#!ek9 defines module book.constants defines constant maxRetries <- 3 greeting <- "Hello" ticketPrice <- 12#GBP openingTime <- 09:00 defines program Show() stdout <- Stdout() stdout.println(`retries allowed: ${maxRetries}`) stdout.println(`${greeting} — doors open at ${openingTime}, entry ${ticketPrice}`)
retries allowed: 3
Hello — doors open at 09:00:00, entry 12.00#GBP
Each line names a value with <- and a literal: a number, a string, a Money amount, a time.
That is the first rule: a constant is always a literal of one of the built-in types in this chapter.
You can name a 12#GBP or a 09:00, but not a value of a type you define yourself. Constants are
for small, fixed facts; EK9 has a different tool for a single shared thing of your own type, which
comes later.
The second rule is the point of the name: a constant is immutable, its value can never change. That matters because values get passed around. A function you hand a value to can sometimes change it; Classes explains when and why. A function can never change a constant, and the way EK9 guarantees it is simple: it hands the function a copy of the constant, never the constant itself.
You can see it happen. Here useOne subtracts one from whatever it is given (lives -= 1 is short
for "take one from lives, in place"):
#!ek9 defines module book.constantcopy defines constant startingLives <- 3 defines function useOne() -> lives as Integer lives -= 1 defines program Show() stdout <- Stdout() myLives <- 3 useOne(myLives) stdout.println(`my variable is now: ${myLives}`) useOne(startingLives) stdout.println(`the constant is still: ${startingLives}`)
my variable is now: 2
the constant is still: 3
Both started as 3. The variable myLives came back changed: useOne did subtract one from it.
The constant startingLives came back unchanged, because the function only saw a copy. The
immutability does not depend on anyone's care; it is built into how the value is passed. (That the
variable changed at all previews a larger idea: most values in EK9 are shared, not copied, when
passed around, which is why useOne could change myLives. Constants are the deliberate exception.
Classes has the full story.)1
Try it yourself
- Change the receipt to use
USDprices and format it withLocale("en_US"). See the symbol and layout change. - In the deadline program, make
noticea duration ofPT48H(forty-eight hours) added to aDateTimeinstead of aDate. What does the result look like? - Add two
Dimensionvalues with the same unit and print the result. Then add two with different units and print the?check, to confirm the mismatch is unset. - Add a
defines constantblock with aDuration(sayPT30M) and aColour, and print them. Then pass one to a function that tries to change it, and confirm the constant is unchanged.
Recap
EK9 gives real-world values real types: Money (exact, with the currency in the value, written
0.35#GBP), a family of date and time types whose operators read as you would say them
(date + duration, date - date), and Dimension, Colour and Resolution. A
Locale formats them for a place; every one answers $ and ?; and each returns unset rather
than a wrong answer when an operation has no meaning. Constants (defines constant) are named,
immutable literals, kept safe by being copied whenever they are passed.
The next chapter, Speaking Every Language, uses Locale further: it shows how one program can
speak to its users in their own language.
-
"Is this value shared or copied?" is subtle enough to catch mainstream languages. For more than a decade, Go's
for-loop variable was a single variable reused across every iteration rather than a fresh one each time, so closures and goroutines created inside a loop all captured the same variable and saw only its final value. Go 1.22 (February 2024) changed the language to give each iteration its own copy. The lesson EK9 draws is to make the question explicit: know when you hold a shared reference and when you hold a copy. ↩
In the EK9 reference: EK9 Basics, EK9 Built in Types, EK9 Standard Types