format.pony

"""
# Format package

The Format package provides support for formatting strings. It can be
used to set things like width, padding and alignment, as well as
controlling the way numbers are displayed (decimal, octal,
hexadecimal).

# Example program

```pony
use "format"

actor Main
  fun disp(desc: String, v: I32, fmt: FormatInt = FormatDefault): String =>
    Format(desc where width = 10)
      + ":"
      + Format.int[I32](v where width = 10, align = AlignRight, fmt = fmt)

  new create(env: Env) =>
    try
      (let x, let y) = (env.args(1)?.i32()?, env.args(2)?.i32()?)
      env.out.print(disp("x", x))
      env.out.print(disp("y", y))
      env.out.print(disp("hex(x)", x, FormatHex))
      env.out.print(disp("hex(y)", y, FormatHex))
      env.out.print(disp("x * y", x * y))
    else
      let exe = try env.args(0)? else "fmt_example" end
      env.err.print("Usage: " + exe + " NUMBER1 NUMBER2")
    end
```
"""

use "collections"

primitive Format
  """
  Provides functions for generating formatted strings.

  * fmt. Format to use.
  * prefix. Prefix to use.
  * prec. Precision to use. The exact meaning of this depends on the type,
  but is generally the number of characters used for all, or part, of the
  string. A value of -1 indicates that the default for the type should be
  used.
  * width. The minimum number of characters that will be in the produced
  string. If necessary the string will be padded with the fill character to
  make it long enough.
  * align. Specify whether fill characters should be added at the beginning or
  end of the generated string, or both.
  * fill. The character to pad a string with if is is shorter than width.
  """
  fun apply(
    str: String,
    fmt: FormatDefault = FormatDefault,
    prefix: PrefixDefault = PrefixDefault,
    prec: USize = -1,
    width: USize = 0,
    align: Align = AlignLeft,
    fill: U32 = ' ')
    : String iso^
  =>
    let copy_len = str.size().min(prec.usize())
    let len = copy_len.max(width.usize())
    recover
      let s = String(len)

      match align
      | AlignLeft =>
        s.append(str)
        for i in Range(s.size(), s.space()) do
          s.push_utf32(fill)
        end
      | AlignRight =>
        for i in Range(0, len - copy_len) do
          s.push_utf32(fill)
        end
        s.append(str)
      | AlignCenter =>
        let half = (len - copy_len) / 2
        for i in Range(0, half) do
          s.push_utf32(fill)
        end
        s.append(str)
        for i in Range(s.size(), s.space()) do
          s.push_utf32(fill)
        end
      end

      s .> recalc()
    end

  fun int[A: (Int & Integer[A])](
    x: A,
    fmt: FormatInt = FormatDefault,
    prefix: PrefixNumber = PrefixDefault,
    prec: USize = -1,
    width: USize = 0,
    align: Align = AlignRight,
    fill: U32 = ' ')
    : String iso^
  =>
    let zero = x.from[USize](0)
    (let abs, let neg) = if x < zero then (-x, true) else (x, false) end

    iftype A <: U128 then
      _FormatInt.u128(x.u128(), false, fmt, prefix, prec, width, align, fill)
    elseif A <: I128 then
      _FormatInt.u128(abs.u128(), neg, fmt, prefix, prec, width, align, fill)
    elseif A <: (U64 | I64) then
      _FormatInt.u64(abs.u64(), neg, fmt, prefix, prec, width, align, fill)
    elseif A <: (U32 | I32) then
      _FormatInt.u32(abs.u32(), neg, fmt, prefix, prec, width, align, fill)
    elseif A <: (U16 | I16) then
      _FormatInt.u16(abs.u16(), neg, fmt, prefix, prec, width, align, fill)
    elseif A <: (U8 | I8) then
      _FormatInt.u8(abs.u8(), neg, fmt, prefix, prec, width, align, fill)
    elseif A <: (USize | ISize) then
      ifdef ilp32 then
        _FormatInt.u32(abs.u32(), neg, fmt, prefix, prec, width, align, fill)
      else
        _FormatInt.u64(abs.u64(), neg, fmt, prefix, prec, width, align, fill)
      end
    elseif A <: (ULong | ILong) then
      ifdef ilp32 or llp64 then
        _FormatInt.u32(abs.u32(), neg, fmt, prefix, prec, width, align, fill)
      else
        _FormatInt.u64(abs.u64(), neg, fmt, prefix, prec, width, align, fill)
      end
    else
      _FormatInt.u128(x.u128(), false, fmt, prefix, prec, width, align, fill)
    end

  fun float[A: (Float & FloatingPoint[A])](
    x: A,
    fmt: FormatFloat = FormatDefault,
    prefix: PrefixNumber = PrefixDefault,
    prec: USize = 6,
    width: USize = 0,
    align: Align = AlignRight,
    fill: U32 = ' ')
    : String iso^
  =>
    _FormatFloat.f64(x.f64(), fmt, prefix, prec, width, align, fill)