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:

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:

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

  1. Change the receipt to use USD prices and format it with Locale("en_US"). See the symbol and layout change.
  2. In the deadline program, make notice a duration of PT48H (forty-eight hours) added to a DateTime instead of a Date. What does the result look like?
  3. Add two Dimension values with the same unit and print the result. Then add two with different units and print the ? check, to confirm the mismatch is unset.
  4. Add a defines constant block with a Duration (say PT30M) and a Colour, 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.

  1. "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. ↩