Package

humanize

Human-readable numbers and times: byte sizes, durations, relative times, ordinals, plurals, lists, compact numbers, number words, Roman numerals, fractions

0.1.0 MIT

Install

sspur add humanize@^0.1.0

or in sspur.toml:

humanize = { version = "^0.1.0" }

README

humanize

Human-readable formatting for SSPUR: byte sizes in SI and IEC units, durations (compact, in words and approximate), relative times ("3 minutes ago"), ordinals, English plurals and singulars, lists ("a, b and c"), compact numbers ("1.2k"), thousands separators, numbers in words, Roman numerals, percentages, fractions, scientific notation and SI prefixes. Byte sizes, durations and compact numbers parse back too.

Everything is pure and English. The rounding rules are stated on each function (rounding can carry into the next unit: 999950 bytes is "1 MB", not "1000 kB"). For locale-specific number and date formats, use the builtin Locale (locale("de-DE") and friends).

Example

use humanize.{format_bytes, format_bytes_iec, parse_bytes, format_duration, format_duration_long, relative_time, parse_duration, ordinal, quantity, join_truncated, compact, number_words, to_roman, fraction}

fn main() -> Unit ! log
= do
  log("{format_bytes(1536000)} / {format_bytes_iec(1536000)} / {parse_bytes("1.5 GB")}")
  log(format_duration(millis(3723250)))
  log(format_duration_long(millis(3723000)))
  log(relative_time(mins(-3)))
  log("{parse_duration("1h30m").map(_.mins)} minutes")
  log("{ordinal(22)} {quantity(0, "file")}, {quantity(3, "person")}")
  log(join_truncated(["ana", "ben", "cai", "dee", "eve"], 2))
  log("{compact(1234)} {compact(3400000)} {compact(999950)}")
  log(number_words(1042))
  log("{to_roman(2026)} {fraction(2.75, 8)}")
1.5 MB / 1.5 MiB / some(1500000000)
1h 2m 3s 250ms
1 hour, 2 minutes and 3 seconds
3 minutes ago
some(90) minutes
22nd no files, 3 people
ana, ben and 3 others
1.2k 3.4M 1M
one thousand forty-two
MMXXVI 2 3/4

API

Byte sizes

  • format_bytes(n): SI units (kB, MB, ... EB), one decimal, trailing zeros dropped: "1.5 kB".
  • format_bytes_iec(n): IEC units (KiB, MiB, ... EiB): "1.5 KiB".
  • format_bytes_with(n, binary, decimals): either, with up to decimals decimals.
  • parse_bytes(s): "1.5 GB", "10MiB", "512", "3 bytes" to bytes; K/KB are 1000, KiB 1024.

Durations and relative times

  • format_duration(d), format_ms(ms): "1d 2h 3m 4s", "1s 250ms", "0ms", "-1h 30m".
  • format_duration_long(d): "1 hour, 2 minutes and 3 seconds".
  • approximate_duration(d): Rails' distance_of_time_in_words: "less than a minute", "about 3 hours", "12 days", "over 2 years".
  • relative_time(d): for d = t.since(now): "just now", "3 minutes ago", "in about 2 hours".
  • parse_duration(s): "1h30m", "90s", "1.5h", "2 days, 3 hours and 4 minutes", "-1h".

Ordinals

  • ordinal(n): "1st", "2nd", "3rd", "11th", "21st", "111th", "-1st".
  • ordinal_suffix(n): just the suffix.

Plurals

  • pluralize(word), singularize(word): irregular nouns, uncountable nouns, the regular rules, and the word's case kept ("Box" to "Boxes").
  • plural_for(n, word): the word or its plural for a count.
  • count_noun(n, word): "1 file", "0 files", "1,234 files".
  • quantity(n, word): "no files", "1 file", "3 people".

Lists

  • join_list(xs): "a, b and c". join_list_oxford(xs): "a, b, and c". join_or(xs): "a, b or c".
  • join_words(xs, conj, oxford): any conjunction.
  • join_truncated(xs, limit): "a, b and 3 others".

Numbers

  • with_commas(n), group_digits(n, sep), with_commas_f64(x, decimals): "1,234,567", "1 234 567", "1,234.57".
  • compact(n), compact_long(n), compact_with(n, decimals, long): "1.2k", "3.4M", "1.2B", "1T"; "1.2 thousand". Rounded half away from zero.
  • parse_compact(s): "1.2k", "1.5 billion", "-1,500" back to an Int.

Numbers in words

  • number_words(n): "one hundred twenty-three", "minus five"; the whole Int range.
  • ordinal_words(n): "first", "twenty-first", "one hundredth".
  • apnumber(n): "zero" to "nine", digits otherwise (AP style).

Roman numerals

  • to_roman(n): 1 to 3999, "MCMXCIV".
  • from_roman(s): strict, only the standard form ("IIII" and "IC" are none).

Percentages, fractions and magnitudes

  • percent(x, decimals), percent_of(part, whole, decimals): "12.5%", "33.3%".
  • fraction(x, max_den): "1/2", "1 1/2", "-2 3/4", the closest fraction with a bounded denominator.
  • fraction_parts(x, max_den): the same as (numerator, denominator): (355, 113) for pi with 1000.
  • scientific(x, decimals): "1.23 x 10⁴".
  • metric(x, unit, decimals): SI prefixes from y to Y: "1.5 kV", "2 µF".

Out of scope

  • Languages other than English, and locale-specific separators beyond group_digits: the builtin Locale formats numbers (format_int, format_f64, format_dec) and dates in six locales.
  • Calendar-aware phrases ("yesterday", "last Tuesday") and date formatting: see the dates package. This package describes lengths of time.
  • A complete English morphology: pluralize and singularize cover the regular rules and a list of about a hundred irregular and uncountable nouns, matched as whole words (so "salesperson" becomes "salespersons"), like npm's pluralize but without user-added rules.
  • British "and" in number words ("one hundred and five"), and words for numbers beyond Int (use BigInt.str).
  • Durations in months or years ("2 months 3 days"), whose length depends on the calendar: see dates.diff and ISO 8601 durations there.

Compared with Python's humanize (naturalsize, naturaldelta, naturaltime, ordinal, intcomma, intword, apnumber, fractional, scientific, metric) and npm's pretty-bytes, pretty-ms, pluralize and numeral, the formatting functions are covered, plus parsers back for sizes, durations and compact numbers.

Install

sspur add humanize

MIT licensed.

API

format_bytes fn

fn format_bytes(n: Int) -> Str

n bytes in SI units (powers of 1000: B, kB, MB, GB, TB, PB, EB) with one decimal and trailing zeros dropped: "999 B", "1 kB", "1.5 kB", "-2.3 MB". Rounding can carry into the next unit (999950 is "1 MB").

format_bytes_iec fn

fn format_bytes_iec(n: Int) -> Str

n bytes in IEC units (powers of 1024: B, KiB, MiB, GiB, TiB, PiB, EiB) with one decimal and trailing zeros dropped: "1 KiB", "1.5 MiB".

format_bytes_with fn

fn format_bytes_with(n: Int, binary: Bool, decimals: Int where _ >= 0 and _ <= 10) -> Str

n bytes in IEC units when binary, else SI units, with at most decimals decimals (0 to 10, trailing zeros dropped). Sizes below one kilobyte are whole bytes ("512 B").

parse_bytes fn

fn parse_bytes(s: Str) -> Opt[Int]

Parses a byte size such as "1.5 GB", "10MiB", "512", "2 kb" or "3 bytes" to a number of bytes, rounded to the nearest byte (ties to even). Units are case-insensitive: B (or byte, bytes), the SI units K/KB (1000), M/MB, G/GB, T/TB, P/PB, E/EB, and the IEC units KiB (1024), MiB, GiB, TiB, PiB, EiB. none for an unknown unit, malformed text or a size beyond Int.

format_duration fn

fn format_duration(d: Duration) -> Str

Duration d precisely and compactly, largest unit first, zero parts left out: "1d 2h 3m 4s", "1s 250ms", "250ms", "0ms" for zero, "-1h 30m" for a negative duration.

format_ms fn

fn format_ms(ms: Int) -> Str

ms milliseconds as format_duration writes them.

format_duration_long fn

fn format_duration_long(d: Duration) -> Str

Duration d in words, largest unit first, zero parts left out: "1 hour, 2 minutes and 3 seconds", "250 milliseconds", "0 seconds" for zero, "minus 2 days" for a negative duration.

approximate_duration fn

fn approximate_duration(d: Duration) -> Str

Duration d approximately, as Rails' distance_of_time_in_words words it (the sign is ignored): "less than a minute" (under 30 seconds), "1 minute", "5 minutes", "about 1 hour", "about 3 hours" (to 23.5 hours), "1 day", "12 days", "about 1 month", "4 months", "about 1 year", "over 2 years", "almost 3 years". Minutes are rounded to the nearest one first.

relative_time fn

fn relative_time(d: Duration) -> Str

A time d away from now in words, where d is t.since(now): positive is in the future and negative in the past. Under 10 seconds either way is "just now"; otherwise it is approximate_duration with "ago" or "in": "3 minutes ago", "in about 2 hours".

parse_duration fn

fn parse_duration(s: Str) -> Opt[Duration]

Parses a duration: one or more parts, each a number (a fraction is allowed) and a unit, with optional spaces, commas and "and" between them: "1h30m", "90s", "1.5h", "250ms", "2 days, 3 hours and 4 minutes", "-1h". Units: ms (msec, millisecond), s (sec, second), m (min, minute), h (hr, hour), d (day), w (wk, week), singular or plural, case-insensitive. The total is rounded to the nearest millisecond (ties to even). none for a bare number, an unknown unit or a total beyond Int.

ordinal_suffix fn

fn ordinal_suffix(n: Int) -> Str

The English ordinal suffix of n: "st", "nd", "rd" or "th" (11, 12 and 13 take "th", as do 111, 112 and 113). The sign is ignored.

ordinal fn

fn ordinal(n: Int) -> Str

n with its ordinal suffix: "1st", "2nd", "3rd", "11th", "21st", "111th", "-1st" (a negative number keeps its sign and takes the suffix of its magnitude).

pluralize fn

fn pluralize(word: Str) -> Str

The plural of English noun word: irregular nouns ("person" to "people", "child" to "children", "leaf" to "leaves", "analysis" to "analyses", "cactus" to "cacti"), uncountable ones unchanged ("sheep", "information"), then the rules for "-y" after a consonant ("-ies"), "-s -x -z -ch -sh" ("-es") and otherwise "-s". The case of word is kept: "Box" gives "Boxes" and "BOX" gives "BOXES". An empty word stays empty.

singularize fn

fn singularize(word: Str) -> Str

The singular of English noun word, undoing pluralize: irregular plurals ("people", "mice", "wolves", "crises"), uncountable nouns unchanged, then "-ies" to "-y", "-sses" to "-ss", "-xes -zes -ches -shes" without "-es", and a final "-s" dropped unless the word ends in "ss", "us" or "is". A word that is already singular is returned unchanged where the rules can tell. The case of word is kept.

plural_for fn

fn plural_for(n: Int, word: Str) -> Str

word if n is 1 (or -1), else its plural.

count_noun fn

fn count_noun(n: Int, word: Str) -> Str

n and the noun, with thousands separators: "1 file", "0 files", "1,234 files", "-1 file".

quantity fn

fn quantity(n: Int, word: Str) -> Str

Like count_noun but zero is "no": "no files", "1 file", "3 files".

join_list fn

fn join_list(xs: List[Str]) -> Str

The items joined as an English list with "and" and no serial comma: "" for none, "a", "a and b", "a, b and c".

join_list_oxford fn

fn join_list_oxford(xs: List[Str]) -> Str

The items joined with "and" and the serial (Oxford) comma: "a, b, and c" ("a and b" for two).

join_or fn

fn join_or(xs: List[Str]) -> Str

The items joined with "or": "a, b or c".

join_words fn

fn join_words(xs: List[Str], conj: Str, oxford: Bool) -> Str

The items joined with commas and conjunction conj before the last one, with a comma before it too when oxford and there are at least three items.

join_truncated fn

fn join_truncated(xs: List[Str], limit: Int where _ >= 0) -> Str

The first limit items and a count of the rest: "a, b and 3 others" ("and 1 other"); all of them as join_list when there are at most limit. With limit 0 only the count is left ("5 others").

with_commas fn

fn with_commas(n: Int) -> Str

n with commas between groups of three digits: "1,234,567", "-1,000".

group_digits fn

fn group_digits(n: Int, sep: Str) -> Str

n with sep between groups of three digits: group_digits(1234567, " ") is "1 234 567".

with_commas_f64 fn

fn with_commas_f64(x: F64, decimals: Int where _ >= 0 and _ <= 20) -> Str

x with commas between groups of three digits and exactly decimals decimals: "1,234.57". NaN and infinities are written "nan", "inf" and "-inf".

compact fn

fn compact(n: Int) -> Str

n in short compact form with one decimal at most: "999", "1.2k", "3.4M", "1.2B", "1T", "-1.5k". The value is rounded half away from zero, and rounding can carry into the next unit (999950 is "1M", 999949 is "999.9k"). Above 999.95 trillion it stays in trillions with separators ("9,223,372T").

compact_long fn

fn compact_long(n: Int) -> Str

n in long compact form: "1.2 thousand", "3.4 million", "1 billion", "2 trillion".

compact_with fn

fn compact_with(n: Int, decimals: Int where _ >= 0 and _ <= 6, long: Bool) -> Str

n in compact form with at most decimals decimals (trailing zeros dropped), short ("1.23k") or long ("1.23 thousand").

parse_compact fn

fn parse_compact(s: Str) -> Opt[Int]

Parses a compact number back to an Int: "1.2k" (1200), "3.4M", "1.5 billion", "2T", "-1,500", "750". Suffixes are case-insensitive: k or thousand, m or million, b, bn or billion, t, tn or trillion. The value is rounded to the nearest integer (ties to even). none for malformed text, an unknown suffix or a value beyond Int.

number_words fn

fn number_words(n: Int) -> Str

n in English words, in the US style without "and": "zero", "seven", "forty-two", "one hundred twenty-three", "minus five", "one million two thousand three". Every Int works, up to the quintillions of the smallest one.

ordinal_words fn

fn ordinal_words(n: Int) -> Str

n as an English ordinal in words: "first", "second", "twelfth", "twenty-first", "one hundredth", "one million second", "minus third".

apnumber fn

fn apnumber(n: Int) -> Str

n in Associated Press style: words for 0 to 9 ("zero" to "nine"), digits otherwise ("10", "-3").

to_roman fn

fn to_roman(n: Int where _ >= 1 and _ <= 3999) -> Str

n as a Roman numeral in the standard subtractive form: 4 is "IV", 1994 is "MCMXCIV", 3999 is "MMMCMXCIX".

from_roman fn

fn from_roman(s: Str) -> Opt[Int]

Parses a Roman numeral in the standard form to_roman writes, upper case only: "XIV" is 14. Non-standard forms ("IIII", "IC", "VX", "MMMM") and anything else give none.

percent fn

fn percent(x: F64, decimals: Int where _ >= 0 and _ <= 20) -> Str

The ratio x as a percentage with exactly decimals decimals: percent(0.125, 1) is "12.5%".

percent_of fn

fn percent_of(part: Int, whole: Int where _ != 0, decimals: Int where _ >= 0 and _ <= 20) -> Str

part of whole as a percentage with exactly decimals decimals: percent_of(1, 3, 1) is "33.3%".

fraction_parts fn

fn fraction_parts(x: F64, max_den: Int where _ >= 1) -> (Int, Int)

The fraction closest to x whose denominator is at most max_den, as (numerator, denominator) in lowest terms with a positive denominator: fraction_parts(0.75, 100) is (3, 4), fraction_parts(3.14159, 10) is (22, 7).

fraction fn

fn fraction(x: F64, max_den: Int where _ >= 1) -> Str

x as a mixed fraction with denominator at most max_den: "1/2", "1 1/2", "-2 3/4", "3" for a whole number, "0" for values that round to zero.

scientific fn

fn scientific(x: F64, decimals: Int where _ >= 0 and _ <= 20) -> Str

x in scientific notation with decimals decimals in the mantissa and a superscript exponent: scientific(12345.0, 2) is "1.23 x 10⁴", scientific(0.00042, 1) is "4.2 x 10⁻⁴". NaN and infinities are written "nan", "inf" and "-inf".

metric fn

fn metric(x: F64, unit: Str, decimals: Int where _ >= 0 and _ <= 20) -> Str

x with an SI prefix for unit and decimals decimals (trailing zeros dropped): metric(1500.0, "V", 2) is "1.5 kV", metric(0.000002, "F", 1) is "2 µF". Prefixes run from y (10^-24) to Y (10^24); rounding can carry into the next prefix (999.96 with one decimal is "1 k"). Zero is "0 " plus the unit; NaN and infinities are written "nan", "inf" and "-inf" before the unit.

Versions

VersionPublishedCommitSourceEffects
0.1.0 2026-10-162cbec2b4c1snapshot