Data Types

Updated:

Modeling real life scenarios into primitive data types that programming languages support can be difficult and software developers usually have varying on views on how certain data should be represented. There are also cultural differences in some data types like dates and currencies. In this article, I have collected some strategies to handle the most common data types.

Primitives

The most primitive data type is integer and that alone can be used to represent more advanced data types.

Culturally Dependent Data

Culturally dependent data is difficult to handle since different countries have different practices, and the culture tend to evolve. You should almost always use a third-party library to handle these if the standard library doesn't support them.

Date

In the modern world, the Gregorian Calendar has been the most used calendar for centuries. However, it is not the most programmer-friendly as the weekdays don't match the numbers, months have different number of days, and leap years are used to fix day lengths.

Storage

The simplest way to store dates is string using ISO 8601 standard format. The databases e.g. PostgreSQL has a dedicated type DATE.

YYYY-MM-DD

Utility Functions

In modern JavaScript you no longer need a third-party library to handle the basic date operations. However, you may see libraries like moment.js and luxon.js in older projects.

function toDateString(date: Date): string {
  return date.toISOString().split("T")[0]
}

The dates should always be localized in the frontend based on the content language

function toLongFinnishDate(date: Date): string {
  return date.toLocaleDateString("fi-FI", { day: "numeric", month: "long", year: "numeric" })
}

// "7. elokuuta 2022"

To manipulate Date object, always create a fresh copy to avoid unnecessary mutations e.g.

function mutateDate(date: Date): Date {
  const updatedDate = new Date(date);
  updatedDate.setFullYear(2030)
  updatedDate.setMonth(1)
  updatedDate.setDate(1) // Days
  updatedDate.setHours(0)
  updatedDate.setSeconds(0)

 // If days overflows or underflow, the year and month will be automatically adjusted.

  return updatedDate;
}

Date-Time

The most common clock systems are 12-hour clock and 24-hour clock. The difficulty comes from timezones, in the most cases storing time in Coordinated Universal Time (UTC) is the best choice. However, it may not be good for future local events like alarm clocks. The daylight saving time (DST) is also subject to policy changes.

Storage

There are a few options to store time, the most common might be a string in ISO 8601 format. The databases usually offer an optimized data type for date-time values e.g. TIMESTAMP in PostgreSQL.

YYYY-MM-DDThh:mm:ss.sssZ

The timestamp may have an offset which is used as a timezone values

YYYY-MM-DDThh:mm:ss.sss-hh:mm
YYYY-MM-DDThh:mm:ss.sss+hh:mm

Sometimes, when you need to store information of the timezone using the full IANA Time Zone name to ensure accurate conversion.

Europe/Helsinki

Storing the offset itself is not accurate because timezone can vary by regions in same country.

Unix Time

For absolute events that require accuracy like monetary transactions using a Unix Timestamp (also Epoch) might be the best choice. The time can be in seconds or milliseconds, it depends on the vendors and platforms in use e.g. Stripe API uses seconds and JavaScript milliseconds.

function toDate(unixTime: number): Date {
  return new Date(unixTime * 1000)
}

function toUnixTime(date: Date): number {
  const unixMilliseconds = date.getTime()
  return Math.floor(unixMilliseconds / 1000)
}

Utility Functions

Displaying a UTC timestamp in certain timezone is rather straightforward:

function toFinnishDateTime(date: Date): string {
  const locale = "fi-FI"
  const timeZone = "Europe/Helsinki"
  const localDate = date.toLocaleDateString(locale, { timeZone })
  const localTime = date.toLocaleTimeString(locale, { timeZone })

  return `${localDate} kello ${localTime}`
}

// "7.8.2022 kello 17.45.33"

You can add time to date by adding or subtracting seconds from unix time.

function addTimeToDate(date: Date, seconds: number): Date {
  const unixTime = date.getTime()
  return new Date(unixTime + seconds * 1000)
}

function toSeconds(days: number, hours: number, minutes: number): number {
  const daysInSeconds = days * 86400
  const hoursInSeconds = hours * 3600
  const minutesInSeconds = minutes * 60
  return daysInSeconds + hoursInSeconds + minutesInSeconds
}

Duration

Duration is best stored in seconds or milliseconds because incrementing and subtracting is easier, many programming languages also has utilities to subtract dates using seconds.

To obtain a duration between to timestamps

function difference(startTime: Date, endTime: Date): number {
  return endTime.getTime() - startTime.getTime()
}

function toDurationString(seconds: number): string {
  const days = Math.floor(seconds / 86400)
  const daysInSeconds = days * 86400
  const hours = Math.floor((seconds - daysInSeconds) / 3600)
  const hoursInSeconds = hours * 3600
  const minutes = Math.floor((seconds - daysInSeconds - hoursInSeconds) / 60)
  const minutesInSeconds = minutes * 60
  const remainingSeconds = seconds - daysInSeconds - hoursInSeconds - minutesInSeconds

  return `${days}d ${hours}h ${minutes}m ${remainingSeconds}s`
}

// toDurationString(100203)
// "1d 3h 50m 3s"

Money

Many programming courses always tell us to NOT store money as floats because they are not accurate enough. Instead, types that have fixed precision should be used such as integer or decimal. For example, Stripe uses integers.

Taking inspiration from django-money the data could be represented as

price | price_currency
------+----------------
10.09 | EUR
50.10 | USD

There do exist monetary types in databases but this SO answer has some arguments against using them. Some state that not all currencies have decimal parts. So using integers would save us from data type errors. However when we need to have more accurate precision such as stocks prices or store computed values without rounding this becomes an issue. Therefore, most flexibility is gained using decimal.

These represent 10.09€ and $50.10. The interface could be as follows:

import decimal
from typing import Union


class Money:
    """
    Represents money with currencies.

    Attrs:
        amount (int): The amount of money to represent as integer.
        currency (str): The currency of money.
    """

    def __init__(self, amount: Union[decimal.Decimal, int], currency: str):
        """
        Constructor will further convert the data type to decimal.

        Args:
            amount: to represent
            currency: using the official short notation https://www.iban.com/currency-codes
        """

    def to_decimal(self, precision: int = 2) -> decimal.Decimal:
        """
        Converts the integer amount to decimal.

        Args:
            precision: for decimal part

        Returns:
            The amount as decimal without currency info.
        """

The API -interface need to have two fields. I think it is better to use numbers if the clients are frontend applications, because they get converted to number type anyway. However, when the client is another backend application, then it might be better to use strings.

{
  "price": 20.50,
  "price_currency": "EUR"
}

The frontend needs to convert the value and do localization:

/**
* Returns the money as localized currency string.
*
* @param {number} amount of money
* @param {string} currency to use
* @returns {string} money in localized format
*/
const localizeMoney = (amount, currency) => amount.toLocaleString(getLocale(), {style: "currency", currency: currency})