#
# $Id: format.icn,v 1.2 2006-07-10 13:43:32 rparlett Exp $
#
# This file is in the public domain.
#
# Author: Robert Parlett (parlett@dial.pipex.com)
#

package util

#
# Convert a string to an integer.
# @param base  The base to use for the conversion (default is 16).
#
procedure format_string_to_int(subject, base)
   local n, digs, s, c
   n := 0
   /base := 16
   digs := "0123456789abcdef"[1:base + 1] | fail

   s := ::string(subject) | fail
   every c := !::map(s) do
      n := base * n + ::find(c, digs) - 1 | fail
   return n
end

#
# Convert an integer to a string.
#
# @param base   The desired base of the result.
# @param p      The minimum width of the result, padding with zeroes
# @             if necessary.
#
procedure format_int_to_string(subject, base, p)
   local s, n, digs
   s := ""
   /base := 16
   /p := 1
   n := ::integer(subject) | fail

   digs := "0123456789ABCDEF"[1:base + 1] | fail
   while n > 0 do {
      s := digs[n % base + 1] || s
      n /:= base
   }

   if p > *s then
      s := ::repl("0", p - *s) || s

   return s
end

#
# Convert a numeric to a string.
#
# @param p  The number of decimal places to produce (default 4).
# @param f  A cset of flags.  If c contains {'e'} then the output is
# @         in scientific notation.  If c contains {','} then commas
# @         are introduced into the non-fractional part of the number;
# @         if c contains {'+'} then a leading + is added to positive
# @         numbers.
# @
#
procedure format_numeric_to_string(subject, p, f)
   local n1, s, t, lim, d, zs, dig, i, n, sig_digs

   /f := ''
   n := ::numeric(subject) | fail
   sig_digs := 16 - 2
   /p := 4
   n1 := ::abs(n)
   if not ::any(f, "e") & ::type(n1) == "integer" then
      s := ::string(n1) || ::repl("0", p)
   else {
      t := format_norm(n1)
      lim := if ::any(f, "e") then p else p + t[2]
      if lim >= -1 then {
         s := ""
         d := t[1]
         if lim > sig_digs then {
            zs := ::repl("0", lim - sig_digs)
            lim := sig_digs
         }
         every 0 to lim do {
            s ||:= dig := ::integer(d)
            d := (d - dig) * 10.0
         }
         if ::integer(d) >= 5 then {
            (every i := *s to 1 by -1 do
             if s[i] := 10 > s[i] + 1 then break
             else s[i] := 0
             ) | {          # need to add 1 to left of s
                s := "1" || s
                if ::any(f, "e") then {
                   s[-1] := ""
                   t[2] +:= 1
                }
             }
         }
         s ||:= \zs
         s := ::repl("0", 0 < p + 1 - *s) || s
      }
      else s := ::repl("0", p + 1)
   }

   if ::any(f, ",") then
      every s[*s - p - 3 to 1 by -3] ||:= ","

   if p > 0 then {
      s[-p - 1] ||:= "."
      if ::any(f, "s"| "z") then {
         "0" ~== s[i := *s to *s - p + 1 by -1]
         s[i + 1 : 0] := if ::any(f, "s") then
            ::repl(" ", *s - i)
         else ""
      }
   }
   if n < 0 & ::upto('123456789', s) then
      s := "-" || s
   else
      if ::any(f, "+") then
         s := "+" || s

   return if ::any(f, "e") then
      s || "E" || (if t[2] < 0 then "-" else "+") ||
      ::right(::abs(t[2]), 3, "0")
   else s
end

procedure format_norm(n)
   local m, e, ve
   static pwr
   initial pwr := [1e1, 1e2, 1e4, 1e8, 1e16, 1e32]

   if n = 0.0 then
      return [0.0, 0]

   m := if n < 1.0 then 1.0 / n else n
   e := 0

   if not(pwr[1 + (ve := 1 to *pwr)] > m) then {
      while m /:= (m >= pwr[ve]) do
         e +:= 2 ^ (ve - 1)
      ve -:= 1
   }

   # invariant : 1 <= m < pwr[1 + ve] & m * 10 ^ e = m0
   while m >= 10.0 do {
      if m /:= (m >= pwr[ve]) then
         e +:= 2 ^ (ve - 1)
      ve -:= 1
   }

   if n < 1.0 then {
      e := -e
      if m := 10.0 / (1.0 ~= m) then
         e -:= 1
   }
   return [m, e]
end


#
# Remove escape sequences from the subject.
#
procedure format_unescape(subject)
   local s, res, ch

   s := ::string(subject) | fail

   res := ""
   s ? {
      repeat {
         res ||:= ::tab(::upto('\\') | 0)
         if ::pos(0) then
            break
         ::move(1)
         if ::any('01234567') then
            res ||:= ::char(format_string_to_int(::move(3), 8))
         else if ="x" then
            res ||:= ::char(format_string_to_int(::move(2), 16))
         else if ="^" then
            res ||:= ::char(::map(::move(1)) + 1 - ::ord("a"))
         else {
            ch := ::move(1) | "\""
            res ||:= case ch of {
               "z" : ""
               "n" : "\n"
               "l" : "\l"
               "b" : "\b"
               "d" : "\d"
               "e" : "\e"
               "r" : "\r"
               "t" : "\t"
               "v" : "\v"
               "f" : "\f"
               "w" : " "
               "s" : "\s"
               default  :  ch
            }
         }
      }
   }
   return res
end

#
# Add escape sequences to the subject.
#
procedure format_escape(subject)
   local s, res, ch
   static printable
   initial
      printable := ::cset(&ascii[33:128]) -- '\\'

   s := ::string(subject) | fail

   res := ""
   s ? {
      repeat {
         res ||:= ::tab(::many(printable))
         if ::pos(0) then
            break
         ch := ::move(1)
         res ||:= case ch of {
            "\n" : "\\n"
            "\l" : "\\l"
            "\b" : "\\b"
            "\d" : "\\d"
            "\e" : "\\e"
            "\r" : "\\r"
            "\t" : "\\t"
            "\v" : "\\v"
            "\f" : "\\f"
            "\\" : "\\\\"
            default  :  "\\x" || format_int_to_string(::ord(ch), 16, 2)
         }
      }
   }
   return res
end

#
# Convert the subject integer into words, eg 231 to "Two Hundred and Thirty-One"
#
procedure format_int_to_words(subject)
   local n, s, i, m
   static small, tens, pwr10, pwr10num

   initial {
      small := ["One", "Two", "Three", "Four", "Five", "Six",
                "Seven", "Eight", "Nine", "Ten", "Eleven",
                "Twelve", "Thirteen", "Fourteen", "Fifteen",
                "Sixteen", "Seventeen", "Eighteen", "Nineteen"]

      tens := ["Ten", "Twenty", "Thirty", "Forty", "Fifty", "Sixty",
               "Seventy", "Eighty", "Ninety"]

      pwr10 := ["Million", "Thousand", "Hundred"]

      pwr10num := [1000000, 1000, 100]
   }

   n := ::integer(subject) | fail

   s := ""

   every i := 1 to *pwr10num do
      if (m := n / pwr10num[i]) > 0 then {
         if *s > 0 then
            s ||:= " "

         s ||:= format_int_to_words(m) || " " || pwr10[i]

         n %:= pwr10num[i]
      }

   if n = 0 then {
      if *s = 0 then
         s := "Zero"
   } else {
      if *s > 0 then
         s ||:= " and "

      if n < 20 then
         s ||:= small[n]
      else {
         s ||:= tens[n / 10]
         if n % 10 > 0 then
            s ||:= "-" || small[n % 10]
      }
   }

   return s
end
