defmodule Cldr.Print do
@moduledoc """
Implements `printf/3`, `sprintf/3` and `lprintf/3` in a manner
largely compatible with the standard `C` language implementations.
"""
alias Cldr.Print.Parser
import Cldr.Print.Splice
@doc """
Formats and prints its arguments under control of a format.
The format is a character string which contains two types of objects:
plain characters, which are simply copied to standard output and format
specifications, each of which causes printing of the next successive argument.
## Arguments
* `format` is a format string. Information on the definition of a
format string is below.
* `args` is a list of arguments that are formatted according to
the directives in the format string. The number of `args` in the list
must be at least equal to the number of format specifiers in the format
string.
* `options` is a keyword list defining how the number is to be formatted. The
valid options are:
## Options
* `backend` is any `Cldr` backend. That is, any module that
contains `use Cldr`. The default is the included `Cldr.Print.Backend`
which is configured with only the locale `en`.
* `:rounding_mode`: determines how a number is rounded to meet the precision
of the format requested. The available rounding modes are `:down`,
:half_up, :half_even, :ceiling, :floor, :half_down, :up. The default is
`:half_even`.
* `:number_system`: determines which of the number systems for a locale
should be used to define the separators and digits for the formatted
number. If `number_system` is an `atom` then `number_system` is
interpreted as a number system. See
`Cldr.Number.System.number_systems_for/2`. If the `:number_system` is
`binary` then it is interpreted as a number system name. See
`Cldr.Number.System.number_system_names_for/2`. The default is `:default`.
* `:locale`: determines the locale in which the number is formatted. See
`Cldr.known_locale_names/0`. The default is`Cldr.get_locale/0` which is the
locale currently in affect for this `Process` and which is set by
`Cldr.put_locale/1`.
* `:device` which is used to define the output device for `printf/3`. The default is
`:stdout`.
## Returns
* `:ok` on success
* `{:error, {exception, reason}}` if an error is detected
## Format definition
Each format specification is introduced by the percent character (`%`).
The remainder of the format specification includes, in the following order:
* Optional format flags
* Optional field width
* Optional precision
* Required format type
The can be represented as:
```
%[flags][width][.precision]format_type
```
## Format flags
Zero or more of the following flags:
| Flag | Description |
| ----- | -------------------------------------------------------------------------------|
| # | A `#` character specifying that the value should be printed in an alternate form. For `b`, `c`, `d`, `s` and `u` formats, this option has no effect. For the `o` formats the precision of the number is increased to force the first character of the output string to a zero. For the `x` (`X`) format, a non-zero result has the string `0x` (`0X`) prepended to it. For `a`, `A`, `e`, `E`, `f`, `F`, `g` and `G` formats, the result will always contain a decimal point, even if no digits follow the point (normally, a decimal point only appears in the results of those formats if a digit follows the decimal point). For `g` and `G` formats, trailing zeros are not removed from the result as they would otherwise be. |
| - | A minus sign `-' which specifies left adjustment of the output in the indicated field. |
| + | A `+` character specifying that there should always be a sign placed before the number when using signed formats. |
| space | A space character specifying that a blank should be left before a positive number for a signed format. A `+` overrides a space if both are used. |
| 0 | A zero `0` character indicating that zero-padding should be used rather than blank-padding. A `-` overrides a `0` if both are used. |
| ' | Formats a number with digit grouping applied. The group size and grouping character are determined based upon the current processes locale or the `:locale` option to `printf/3` if provided. |
| I | Formats a number using the native number system digits of the current processes locale or the `:locale` option to `printf/3` if provided. The option `:number_system` if provided takes precedence over this flag. |
## Field Width
An optional digit string specifying a field width; if the output string has fewer bytes than the field
width it will be blank-padded on the left (or right, if the left-adjustment indicator has been given)
to make up the field width (note that a leading zero is a flag, but an embedded zero is part of a
field width).
## Precision
An optional period, `.`, followed by an optional digit string giving a precision which specifies the
number of digits to appear after the decimal point, for `e` and `f` formats, or the maximum number of
graphemes to be printed from a string. If the digit string is missing, the precision is treated as zero.
## Format Type
A character which indicates the type of format to use (one of `diouxXfFeEgGaAs`). The uppercase
formats differ from their lowercase counterparts only in that the output of the former is entirely in
uppercase.
| Format | Description |
| ------ | -------------------------------------------------------------------------------|
| diouXx | The argument is printed as a signed decimal (d or i), unsigned octal, unsigned decimal, or unsigned hexadecimal (X or x), respectively. |
| fF | The argument is printed in the style `[-]ddd.ddd` where the number of d's after the decimal point is equal to the precision specification for the argument. If the precision is missing, 6 digits are given; if the precision is explicitly 0, no digits and no decimal point are printed. The values infinity and NaN are printed as `inf' and `nan', respectively. |
| eE | The argument is printed in the style e `[-d.ddd+-dd]` where there is one digit before the decimal point and the number after is equal to the precision specification for the argument; when the precision is missing, 6 digits are produced. The values infinity and NaN are printed as `inf` and `nan`, respectively. |
| gG | The argument is printed in style f or e (or in style E for a G format code), with the precision specifying the number of significant digits. The style used depends on the value converted: style e will be used only if the exponent resulting from the conversion is less than -4 or greater than the precision. Trailing zeroes are removed from the result; a decimal point appears only if it is followed by a digit. |
| aA | The argument is printed in style `[-h.hhh+-pd]` where there is one digit before the hexadecimal point and the number after is equal to the precision specification for the argument; when the precision is missing, enough digits are produced to convey the argument's exact double-precision floating-point representation. The values infinity and NaN are printed as `inf` and `nan`, respectively. |
| s | Graphemes from the string argument are printed until the end is reached or until the number of graphemes indicated by the precision specification is reached; however if the precision is 0 or missing, the string is printed entirely. |
| % | Print a `%`; no argument is used. |
## Notes
* The grouping separator, decimal point and exponent characters are defined in the current
processes locale or as specified in the `:locale` option to `printf/3`.
* In no case does a non-existent or small field width cause truncation of a field; padding
takes place only if the specified field width exceeds the actual width.
* `printf/3` calls `IO.write/2` and therefore there are no control characters emitted
unless provided in the format string. This is consistent with the `C` implementation
but different from `IO.puts/2`.
"""
def printf(format, args, options \\ []) do
{device, options} = Keyword.pop(options, :device, :stdio)
with {:ok, io_list} <- lprintf(format, args, options) do
IO.write(device, io_list)
end
end
@doc """
Returns a `{:ok, string}` after applying a format to a list of arguments.
The arguments and options are the same as those for `printf/3`
"""
def sprintf(format, args, options \\ []) do
with {:ok, io_list} <- lprintf(format, args, options) do
{:ok, IO.iodata_to_binary(io_list)}
end
end
@doc """
Returns a `string` or raises after applying a format to
a list of arguments.
The arguments and options are the same as those for `printf/3`
"""
def sprintf!(format, args, options \\ []) do
case sprintf(format, args, options) do
{:ok, string} -> string
{:error, {exception, reason}} -> raise exception, reason
end
end
@doc """
Returns an `{:ok, io_list}` after applying a format to a list of arguments.
The arguments and options are the same as those for `printf/3`
"""
def lprintf(format, args, options \\ [])
def lprintf(format, args, options) when is_list(args) do
with {:ok, tokens} <- Parser.parse(format),
{:ok, io_list} <- splice_arguments(tokens, args, options, &format/2) do
{:ok, Enum.reverse(io_list)}
end
end
def lprintf(format, arg, options) do
lprintf(format, [arg], options)
end
@doc """
Returns an `io_list` or raises after applying a format to
a list of arguments.
The arguments and options are the same as those for `printf/3`
"""
def lprintf!(format, args, options \\ []) do
case lprintf(format, args, options) do
{:ok, string} -> string
{:error, {exception, reason}} -> raise exception, reason
end
end
@doc false
defmacro mprintf(format, args, options \\ []) do
args = if is_list(args), do: args, else: [args]
with {:ok, tokens} <- Parser.parse(format),
{:ok, io_list} <- splice_arguments(tokens, args, options, &identity/2) do
quote do
IO.write Keyword.get(unquote(options), :device, :stdout),
IO.iodata_to_binary(format_list(unquote(Enum.reverse(io_list))))
end
end
end
@doc false
defmacro msprintf(format, args, options \\ []) do
args = if is_list(args), do: args, else: [args]
with {:ok, tokens} <- Parser.parse(format),
{:ok, io_list} <- splice_arguments(tokens, args, options, &identity/2) do
quote do
{:ok, format_list(unquote(Enum.reverse(io_list))) |> IO.iodata_to_binary}
end
end
end
@doc false
defmacro msprintf!(format, args, options \\ []) do
args = if is_list(args), do: args, else: [args]
with {:ok, tokens} <- Parser.parse(format),
{:ok, io_list} <- splice_arguments(tokens, args, options, &identity/2) do
quote do
format_list(unquote(Enum.reverse(io_list))) |> IO.iodata_to_binary
end
end
end
@doc false
defmacro mlprintf(format, args, options \\ []) do
args = if is_list(args), do: args, else: [args]
with {:ok, tokens} <- Parser.parse(format),
{:ok, io_list} <- splice_arguments(tokens, args, options, &identity/2) do
quote do
{:ok, format_list(unquote(Enum.reverse(io_list)))}
end
end
end
@doc false
defmacro mlprintf!(format, args, options \\ []) do
args = if is_list(args), do: args, else: [args]
with {:ok, tokens} <- Parser.parse(format),
{:ok, io_list} <- splice_arguments(tokens, args, options, &identity/2) do
quote do
format_list(unquote(Enum.reverse(io_list)))
end
end
end
end