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
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").
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".
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").
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.
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.
fn format_ms(ms: Int) -> Str
ms milliseconds as format_duration writes them.
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.
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.
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".
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.
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.
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).
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.
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.
fn plural_for(n: Int, word: Str) -> Str
word if n is 1 (or -1), else its plural.
fn count_noun(n: Int, word: Str) -> Str
n and the noun, with thousands separators: "1 file", "0 files", "1,234 files", "-1 file".
fn quantity(n: Int, word: Str) -> Str
Like count_noun but zero is "no": "no files", "1 file", "3 files".
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".
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).
fn join_or(xs: List[Str]) -> Str
The items joined with "or": "a, b or c".
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.
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").
fn with_commas(n: Int) -> Str
n with commas between groups of three digits: "1,234,567", "-1,000".
fn group_digits(n: Int, sep: Str) -> Str
n with sep between groups of three digits: group_digits(1234567, " ") is "1 234 567".
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".
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").
fn compact_long(n: Int) -> Str
n in long compact form: "1.2 thousand", "3.4 million", "1 billion", "2 trillion".
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").
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.
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.
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".
fn apnumber(n: Int) -> Str
n in Associated Press style: words for 0 to 9 ("zero" to "nine"), digits otherwise ("10", "-3").
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".
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.
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%".
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%".
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).
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.
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".
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.