├── .gitignore ├── README.md ├── config └── config.exs ├── lib ├── compiler.ex ├── debug.ex ├── display.ex ├── format.ex ├── fractional.ex ├── integral.ex └── interpreter.ex ├── mix.exs └── test ├── format_test.exs └── test_helper.exs /.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps 9 | 10 | # Where 3rd-party dependencies like ExDoc output generated docs. 11 | /doc 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Format 2 | 3 | --- 4 | 5 | **WARNING**: This library is of alpha quality. There's a lot of missing features and bugs should be expected. 6 | 7 | --- 8 | 9 | Alternative string formatter for Elixir inspired by Python and Rust. 10 | 11 | ```elixir 12 | iex> use Format 13 | iex> Format.fmt(~F"{} {}", [1, "foo"]) 14 | [[[[]] | "1"], " ", "foo"]] 15 | iex> Format.string(~F"{} {}", [1, "foo"]) 16 | "1 foo" 17 | iex> Format.puts(~F"{} {}", [1, "foo"]) 18 | 1 foo 19 | :ok 20 | 21 | iex> Format.string(~F"{foo} {bar}", bar: 1, foo: 3) 22 | "3 1" 23 | iex> Format.string(~F"{foo:.3f} {foo:.5f}", foo: 3.2) 24 | "3.200 3.20000" 25 | iex> Format.string(~F"{0:^10d}|{0:<10x}|{0:>10b}", [100]) 26 | " 100 | 64|1100100 " 27 | ``` 28 | 29 | ## Installation 30 | 31 | If [available in Hex](https://hex.pm/docs/publish), the package can be installed 32 | by adding `format` to your list of dependencies in `mix.exs`: 33 | 34 | ```elixir 35 | def deps do 36 | [{:format, "~> 0.1.0"}] 37 | end 38 | ``` 39 | 40 | Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc) 41 | and published on [HexDocs](https://hexdocs.pm). Once published, the docs can 42 | be found at [https://hexdocs.pm/format](https://hexdocs.pm/format). 43 | 44 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies with the aid of the Mix.Config module. 3 | use Mix.Config 4 | 5 | # This configuration is loaded before any dependency and is restricted 6 | # to this project. If another project depends on this project, this 7 | # file won't be loaded nor affect the parent project. For this reason, 8 | # if you want to provide default values for your application for 9 | # 3rd-party users, it should be done in your "mix.exs" file. 10 | 11 | # You can configure for your application as: 12 | # 13 | # config :format, key: :value 14 | # 15 | # And access this configuration in your application as: 16 | # 17 | # Application.get_env(:format, :key) 18 | # 19 | # Or configure a 3rd-party app: 20 | # 21 | # config :logger, level: :info 22 | # 23 | 24 | # It is also possible to import configuration files, relative to this 25 | # directory. For example, you can emulate configuration per environment 26 | # by uncommenting the line below and defining dev.exs, test.exs and such. 27 | # Configuration from the imported file will override the ones defined 28 | # here (which is why it is important to import them last). 29 | # 30 | # import_config "#{Mix.env}.exs" 31 | -------------------------------------------------------------------------------- /lib/compiler.ex: -------------------------------------------------------------------------------- 1 | defmodule Format.Compiler do 2 | def compile(format_string) do 3 | {fragments, mode} = compile(format_string, [], nil) 4 | {fragments, format_string, mode} 5 | end 6 | 7 | defp final_mode(nil), do: :positional 8 | defp final_mode({:positional, _next}), do: :positional 9 | defp final_mode(:named), do: :named 10 | 11 | defp compile("", fragments, mode) do 12 | {Enum.reverse(fragments), final_mode(mode)} 13 | end 14 | defp compile("{{" <> rest, fragments, mode) do 15 | {fragment, rest} = read_text(rest, "{") 16 | compile(rest, [fragment | fragments], mode) 17 | end 18 | defp compile("{" <> rest, fragments, mode) do 19 | {argument, format, rest} = read_argument(rest, "") 20 | {argument, mode} = compile_argument(argument, mode) 21 | format = compile_format(format, mode) 22 | compile(rest, [{argument, format} | fragments], mode) 23 | end 24 | defp compile("}}" <> rest, fragments, mode) do 25 | {fragment, rest} = read_text(rest, "}") 26 | compile(rest, [fragment | fragments], mode) 27 | end 28 | defp compile("}" <> _rest, _fragments, _mode) do 29 | raise "unexpected end of argument marker" 30 | end 31 | defp compile(<>, fragments, mode) do 32 | {fragment, rest} = read_text(rest, char) 33 | compile(rest, [fragment | fragments], mode) 34 | end 35 | 36 | ## Readers 37 | 38 | defp read_argument("", _acc) do 39 | raise "argument not finished" 40 | end 41 | defp read_argument("}}" <> rest, acc) do 42 | read_argument(rest, acc <> "}") 43 | end 44 | defp read_argument("}" <> rest, acc) do 45 | {acc, "", rest} 46 | end 47 | defp read_argument("{{" <> rest, acc) do 48 | read_argument(rest, acc <> "{") 49 | end 50 | defp read_argument("{" <> _rest, _acc) do 51 | raise "nested arguments are not supported" 52 | end 53 | defp read_argument(":" <> rest, acc) do 54 | {format, rest} = read_format(rest, "") 55 | {acc, format, rest} 56 | end 57 | defp read_argument(<>, acc) do 58 | read_argument(rest, acc <> char) 59 | end 60 | 61 | defp read_format("", _acc) do 62 | raise "argument not finished" 63 | end 64 | defp read_format("}}" <> rest, acc) do 65 | read_format(rest, acc <> "}") 66 | end 67 | defp read_format("}" <> rest, acc) do 68 | {acc, rest} 69 | end 70 | defp read_format("{{" <> rest, acc) do 71 | read_format(rest, acc <> "{") 72 | end 73 | defp read_format("{" <> _rest, _acc) do 74 | raise "nested arguments are not supported" 75 | end 76 | defp read_format(<>, acc) do 77 | read_format(rest, acc <> char) 78 | end 79 | 80 | defp read_text("{{" <> rest, acc) do 81 | read_text(rest, acc <> "{") 82 | end 83 | defp read_text("}}" <> rest, acc) do 84 | read_text(rest, acc <> "}") 85 | end 86 | defp read_text("{" <> _ = rest, acc) do 87 | {acc, rest} 88 | end 89 | defp read_text("", acc) do 90 | {acc, ""} 91 | end 92 | defp read_text(<>, acc) do 93 | read_text(rest, acc <> char) 94 | end 95 | 96 | ## Compilers 97 | 98 | @digits '0123456789' 99 | 100 | defp compile_argument("", {:positional, next}) do 101 | {next, {:positional, next + 1}} 102 | end 103 | defp compile_argument("", nil) do 104 | compile_argument("", {:positional, 0}) 105 | end 106 | defp compile_argument(<> = arg, nil) when digit in @digits do 107 | compile_argument(arg, {:positional, 0}) 108 | end 109 | defp compile_argument(<> = arg, {:positional, next}) when digit in @digits do 110 | case Integer.parse(arg) do 111 | {int, ""} -> 112 | {int, {:positional, next}} 113 | _ -> 114 | raise "invalid integer argument: #{inspect arg}" 115 | end 116 | end 117 | defp compile_argument(<>, :named) when digit in @digits do 118 | raise "named arguments cannot be mixed with positional ones" 119 | end 120 | defp compile_argument(name, mode) when name != "" and mode in [nil, :named] do 121 | {String.to_atom(name), :named} 122 | end 123 | defp compile_argument(_arg, _mode) do 124 | raise "named arguments cannot be mixed with positional ones" 125 | end 126 | 127 | defp compile_format(format, mode) do 128 | {fill, align, rest} = compile_align(format) 129 | {sign, rest} = compile_sign(rest) 130 | {alternate, rest} = compile_alternate(rest) 131 | {sign, fill, rest} = compile_zero(rest, sign, fill) 132 | {width, rest} = compile_width(rest) 133 | {grouping, rest} = compile_grouping(rest) 134 | {precision, rest} = compile_precision(rest, mode) 135 | {type, rest} = compile_type(rest) 136 | assert_done(rest) 137 | %Format.Specification{fill: fill, align: align, sign: sign, 138 | alternate: alternate, width: width, grouping: grouping, 139 | precision: precision, type: type} 140 | end 141 | 142 | defp compile_grouping("_" <> rest), 143 | do: {"_", rest} 144 | defp compile_grouping("," <> rest), 145 | do: {",", rest} 146 | defp compile_grouping(rest), 147 | do: {nil, rest} 148 | 149 | @integral [decimal: "d", octal: "o", hex: "x", upper_hex: "X", char: "c", 150 | binary: "b"] 151 | @fractional [float: "f", exponent: "e", upper_exponent: "E", general: "g", 152 | upper_general: "G"] 153 | @types [debug: "?", string: "s"] 154 | 155 | for {name, char} <- @integral do 156 | defp compile_type(unquote(char) <> rest), 157 | do: {{:integral, unquote(name)}, rest} 158 | end 159 | for {name, char} <- @fractional do 160 | defp compile_type(unquote(char) <> rest), 161 | do: {{:fractional, unquote(name)}, rest} 162 | end 163 | for {name, char} <- @types do 164 | defp compile_type(unquote(char) <> rest), 165 | do: {unquote(name), rest} 166 | end 167 | defp compile_type(""), 168 | do: {:display, ""} 169 | defp compile_type("%" <> _ = custom), 170 | do: {{:display, custom}, ""} 171 | defp compile_type(type), 172 | do: raise(ArgumentError, "unknown type: #{inspect type}") 173 | 174 | defp assert_done(""), 175 | do: :ok 176 | defp assert_done(_), 177 | do: raise(ArgumentError, "invalid format") 178 | 179 | defp compile_width(format) do 180 | case Integer.parse(format) do 181 | {int, "$" <> rest} -> 182 | {{:argument, int}, rest} 183 | {int, rest} -> 184 | {int, rest} 185 | :error -> 186 | {nil, format} 187 | end 188 | end 189 | 190 | defp compile_precision(<<".", digit, _::binary>> = format, mode) 191 | when digit in @digits do 192 | "." <> format = format 193 | case Integer.parse(format) do 194 | {int, "$" <> rest} when elem(mode, 0) == :positional -> 195 | {{:argument, int}, rest} 196 | {_int, "$" <> _rest} -> 197 | raise "named arguments cannot be mixed with positional ones" 198 | {int, rest} -> 199 | {int, rest} 200 | end 201 | end 202 | defp compile_precision("." <> format, :named) do 203 | case String.split(format, "$", parts: 2, trim: true) do 204 | [name, rest] -> 205 | {{:argument, String.to_atom(name)}, rest} 206 | _ -> 207 | raise ArgumentError, "invalid precision specification" 208 | end 209 | end 210 | defp compile_precision(rest, _mode) do 211 | {nil, rest} 212 | end 213 | 214 | defp compile_alternate("#" <> rest), do: {true, rest} 215 | defp compile_alternate(rest), do: {false, rest} 216 | 217 | defp compile_zero("0" <> rest, nil, ?\s), 218 | do: {true, ?0, rest} 219 | defp compile_zero("0" <> _rest, _sign, _fill), 220 | do: raise(ArgumentError, "the 0 option cannot be combined with +, - or fill character") 221 | defp compile_zero(rest, sign, fill), 222 | do: {sign, fill, rest} 223 | 224 | defp compile_sign("+" <> rest), do: {:plus, rest} 225 | defp compile_sign("-" <> rest), do: {:minus, rest} 226 | defp compile_sign(" " <> rest), do: {:space, rest} 227 | defp compile_sign(rest), do: {:minus, rest} 228 | 229 | @align [left: ?<, center: ?^, right: ?>] 230 | 231 | for {name, char} <- @align do 232 | defp compile_align(<>), 233 | do: {<>, unquote(name), rest} 234 | defp compile_align(<>), 235 | do: {" ", unquote(name), rest} 236 | end 237 | defp compile_align(rest), 238 | do: {" ", nil, rest} 239 | 240 | end 241 | -------------------------------------------------------------------------------- /lib/debug.ex: -------------------------------------------------------------------------------- 1 | defprotocol Format.Debug do 2 | @spec fmt(term, Format.t) :: Format.chardata 3 | def fmt(value, format) 4 | end 5 | 6 | # TODO: how this should work? 7 | -------------------------------------------------------------------------------- /lib/display.ex: -------------------------------------------------------------------------------- 1 | defprotocol Format.Display do 2 | @spec fmt(term, Format.t) :: Format.chardata 3 | def fmt(value, format) 4 | 5 | @spec fmt(term, String.t, Format.t) :: {:ok, Format.chardata} | :error 6 | def fmt(value, custom, format) 7 | end 8 | 9 | # TODO: protocol implementations 10 | -------------------------------------------------------------------------------- /lib/format.ex: -------------------------------------------------------------------------------- 1 | defmodule Format do 2 | alias Format.{Compiler, Interpreter} 3 | 4 | defstruct [:fragments, :original, :mode, newline: false] 5 | 6 | defmodule Specification do 7 | defstruct [:fill, :align, :sign, :alternate, :width, 8 | :grouping, :precision, :type] 9 | end 10 | 11 | @type chardata :: :unicode.chardata 12 | @type align :: :left | :right | :center | nil 13 | @type sign :: :+ | :- | nil 14 | @type count :: pos_integer | {:argument, pos_integer} 15 | @type integral :: :decimal | :octal | :hex | :upper_hex | :char 16 | @type fractional :: :float | :exponent | :upper_exponent | :general | :upper_general 17 | @type type :: :display | {:display, String.t} | :debug | :string 18 | | {:integral, integral} | {:fractional, fractional} 19 | @type spec :: %Specification{align: align, fill: String.t, 20 | sign: sign, alternate: boolean, width: count, 21 | grouping: String.t, precision: count, type: type} 22 | @type t :: %__MODULE__{fragments: [spec], original: String.t, 23 | mode: :positional | :named, newline: boolean} 24 | 25 | defmacro __using__(_) do 26 | quote do 27 | import Format, only: [sigil_F: 2] 28 | require Format 29 | end 30 | end 31 | 32 | ## Public interface 33 | 34 | def compile(format) when is_binary(format) do 35 | {fragments, original, mode} = Compiler.compile(format) 36 | %__MODULE__{fragments: fragments, original: original, mode: mode} 37 | end 38 | 39 | defmacro sigil_F({:<<>>, _, [format]}, _modifiers) when is_binary(format) do 40 | Macro.escape(compile(format)) 41 | end 42 | 43 | def fmt(%__MODULE__{fragments: fragments, mode: mode} = format, args) 44 | when is_list(args) or is_map(args) do 45 | [apply(Interpreter, mode, [fragments, args]) | newline(format)] 46 | end 47 | 48 | def string(format, args) do 49 | :unicode.characters_to_binary(fmt(format, args)) 50 | end 51 | 52 | def puts(device \\ :stdio, format, args) do 53 | format = append_newline(format) 54 | request(device, {:write, format, args}, :puts) 55 | end 56 | 57 | def write(device \\ :stdio, format, args) do 58 | request(device, {:write, format, args}, :write) 59 | end 60 | 61 | def binwrite(device \\ :stdio, format, args) do 62 | request(device, {:binwrite, format, args}, :binwrite) 63 | end 64 | 65 | ## Formatting 66 | 67 | defp newline(%{newline: true}), do: [?\n] 68 | defp newline(_), do: [] 69 | 70 | defp append_newline(format), do: %{format | newline: true} 71 | 72 | ## IO protocol handling 73 | 74 | defp request(device, request, func) do 75 | case request(device, request) do 76 | {:error, reason} -> 77 | [_name | args] = Tuple.to_list(request) 78 | try do 79 | throw(:error) 80 | catch 81 | :throw, :error -> 82 | [_current, stack] = System.stacktrace() 83 | new_stack = [{__MODULE__, func, [device | args]} | stack] 84 | reraise_convert(reason, new_stack) 85 | end 86 | other -> 87 | other 88 | end 89 | end 90 | 91 | # TODO: handle errors better 92 | defp reraise_convert(:arguments, stack) do 93 | reraise ArgumentError, stack 94 | end 95 | defp reraise_convert(:terminated, stack) do 96 | reraise "io device terminated during request", stack 97 | end 98 | defp reraise_convert({:no_translation, from, to}, stack) do 99 | reraise "couldn't encode from #{from} to #{to}", stack 100 | end 101 | defp reraise_convert(other, stack) do 102 | reraise ArgumentError, "error printing: #{inspect other}", stack 103 | end 104 | 105 | defp request(:stdio, request) do 106 | request(Process.group_leader(), request) 107 | end 108 | defp request(:stderr, request) do 109 | request(:standard_error, request) 110 | end 111 | defp request(pid, request) when is_pid(pid) do 112 | # Support only modern io servers that speak unicode 113 | true = :net_kernel.dflag_unicode_io(pid) 114 | execute_request(pid, io_request(pid, request)) 115 | end 116 | defp request(name, request) when is_atom(name) do 117 | case Process.whereis(name) do 118 | nil -> 119 | {:error, :arguments} 120 | pid -> 121 | request(pid, request) 122 | end 123 | end 124 | 125 | defp execute_request(pid, request) do 126 | ref = Process.monitor(pid) 127 | send(pid, {:io_request, self(), ref, request}) 128 | receive do 129 | {:io_reply, ^ref, reply} -> 130 | Process.demonitor(ref, [:flush]) 131 | reply 132 | {:DOWN, ^ref, _, _, _} -> 133 | receive do 134 | {:EXIT, ^pid, _} -> 135 | true 136 | after 137 | 0 -> 138 | true 139 | end 140 | {:error, :terminated} 141 | end 142 | end 143 | 144 | defp io_request(pid, {:write, format, args}) when node(pid) == node() do 145 | data = string(format, args) 146 | {:put_chars, :unicode, data} 147 | end 148 | defp io_request(_pid, {:write, format, args}) do 149 | {:put_chars, :unicode, __MODULE__, :fmt, [format, args]} 150 | end 151 | defp io_request(pid, {:binwrite, format, args}) when node(pid) == node() do 152 | data = IO.iodata_to_binary(fmt(format, args)) 153 | {:put_chars, :latin1, data} 154 | end 155 | defp io_request(_pid, {:binwrite, format, args}) do 156 | {:put_chars, :latin1, __MODULE__, :fmt, [format, args]} 157 | end 158 | end 159 | 160 | defimpl Inspect, for: Format do 161 | def inspect(format, _opts) do 162 | "~F" <> Kernel.inspect(format.original, binaries: :as_strings) 163 | end 164 | end 165 | -------------------------------------------------------------------------------- /lib/fractional.ex: -------------------------------------------------------------------------------- 1 | defprotocol Format.Fractional do 2 | @spec fmt(term, atom, Format.t) :: {:ok, Format.chardata} | {:error, float()} 3 | def fmt(value, type, format) 4 | end 5 | 6 | defimpl Format.Fractional, for: Float do 7 | def fmt(value, _type, _format) do 8 | {:error, value} 9 | end 10 | end 11 | 12 | defimpl Format.Fractional, for: Integer do 13 | def fmt(value, _type, _format) do 14 | {:error, value + 0.0} 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/integral.ex: -------------------------------------------------------------------------------- 1 | defprotocol Format.Integral do 2 | @spec fmt(term, atom, Format.t) :: {:ok, Format.chardata} | {:error, integer()} 3 | def fmt(value, type, format) 4 | end 5 | 6 | defimpl Format.Integral, for: Integer do 7 | def fmt(value, _type, _format) do 8 | {:error, value} 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /lib/interpreter.ex: -------------------------------------------------------------------------------- 1 | defmodule Format.Interpreter do 2 | def named(fragments, args) when is_list(args), 3 | do: do_named(fragments, Map.new(args)) 4 | def named(fragments, args) when is_map(args), 5 | do: do_named(fragments, args) 6 | 7 | defp do_named([text | rest], args) when is_binary(text), 8 | do: [text | do_named(rest, args)] 9 | defp do_named([{name, format} | rest], args), 10 | do: [format(format, Map.fetch!(args, name), &Map.fetch!(args, &1)) | do_named(rest, args)] 11 | defp do_named([], _args), 12 | do: [] 13 | 14 | def positional(fragments, args) when is_list(args), 15 | do: do_positional(fragments, List.to_tuple(args)) 16 | def positional(_fragments, args) when is_map(args), 17 | do: raise(ArgumentError) 18 | 19 | defp do_positional([text | rest], args) when is_binary(text), 20 | do: [text | do_positional(rest, args)] 21 | defp do_positional([{idx, format} | rest], args), 22 | do: [format(format, elem(args, idx), &elem(args, &1)) | do_positional(rest, args)] 23 | defp do_positional([], _args), 24 | do: [] 25 | 26 | defp format(%{precision: {:argument, precision}} = format, arg, fetch_arg) do 27 | format(%{format | precision: fetch_arg.(precision)}, arg, fetch_arg) 28 | end 29 | defp format(%{width: {:argument, width}} = format, arg, fetch_arg) do 30 | format(%{format | width: fetch_arg.(width)}, arg, fetch_arg) 31 | end 32 | defp format(format, arg, _fetch_arg) do 33 | dispatch_format(format.type, format, arg) 34 | end 35 | 36 | defp dispatch_format(:debug, format, value), 37 | do: Format.Debug.fmt(value, format) 38 | defp dispatch_format({:integral, type}, format, value), 39 | do: format_integral(value, type, format) 40 | defp dispatch_format({:fractional, type}, format, value), 41 | do: format_fractional(value, type, format) 42 | defp dispatch_format(:string, format, value), 43 | do: format_string(value, format) 44 | defp dispatch_format(:display, format, value), 45 | do: dispatch_display(value, format, [value, format]) 46 | defp dispatch_format({:display, custom}, format, value), 47 | do: dispatch_display(value, format, [value, custom, format]) 48 | 49 | defp dispatch_display(value, format, _args) when is_integer(value), 50 | do: format_integral(value, :decimal, format) 51 | defp dispatch_display(value, format, _args) when is_float(value), 52 | do: format_fractional(value, :float, format) 53 | defp dispatch_display(value, format, _args) when is_binary(value) or is_list(value), 54 | do: format_string(value, format) 55 | defp dispatch_display(_value, _format, args), 56 | do: apply(Format.Display, :fmt, args) 57 | 58 | defp format_string(value, format) when is_binary(value) or is_list(value) do 59 | %{fill: fill, align: align, width: width, precision: precision} = format 60 | format_string(value, width, precision, fill, align) 61 | end 62 | defp format_string(value, format) when is_atom(value) do 63 | %{fill: fill, align: align, width: width, precision: precision} = format 64 | value = Atom.to_string(value) 65 | format_string(value, width, precision, fill, align) 66 | end 67 | 68 | defp format_string(value, width, precision, fill, align) do 69 | if width || precision do 70 | value = :unicode.characters_to_binary(value) 71 | trimmed = trim(value, precision) 72 | length = precision || String.length(trimmed) 73 | pad(trimmed, length, width, fill, align || :left) 74 | else 75 | value 76 | end 77 | end 78 | 79 | defp trim(value, nil), do: value 80 | defp trim(value, len), do: String.slice(value, 0, len) 81 | 82 | defp pad(value, _length, nil, _fill, _align), 83 | do: value 84 | defp pad(value, length, width, fill, align) when length < width, 85 | do: pad(value, String.duplicate(fill, width - length), width - length, align) 86 | defp pad(value, length, width, _fill, _align) when length >= width, 87 | do: value 88 | 89 | defp pad(value, pad, _pad_len, :left), do: [pad | value] 90 | defp pad(value, pad, _pad_len, :right), do: [value | pad] 91 | defp pad(value, pad, pad_len, :center) do 92 | {left, right} = String.split_at(pad, div(pad_len, 2)) 93 | [left, value | right] 94 | end 95 | 96 | defp format_integral(value, type, format) when is_integer(value) do 97 | format_integer(value, type, format) 98 | end 99 | defp format_integral(value, type, format) do 100 | case Format.Integral.fmt(value, type, format) do 101 | {:ok, chardata} -> 102 | chardata 103 | {:error, integer} -> 104 | format_integer(integer, type, format) 105 | end 106 | end 107 | 108 | defp format_integer(value, type, format) do 109 | %{fill: fill, align: align, width: width, 110 | sign: sign, grouping: grouping, alternate: alternate} = format 111 | base_prefix = integer_base_prefix(alternate, type) 112 | base = integer_base(type) 113 | sign_prefix = sign_prefix(sign, value >= 0) 114 | prefix = [base_prefix | sign_prefix] 115 | value = 116 | value 117 | |> abs 118 | |> Integer.to_string(base) 119 | |> maybe_downcase(type == :hex) 120 | |> group(grouping) 121 | formatted = [prefix | value] 122 | if width do 123 | pad(formatted, IO.iodata_length(formatted), width, fill, align || :right) 124 | else 125 | formatted 126 | end 127 | end 128 | 129 | defp group(value, nil), do: value 130 | defp group(value, char) do 131 | first_len = rem(byte_size(value), 3) 132 | first_len = if first_len == 0, do: 3, else: first_len 133 | <> = value 134 | [first_part | do_group(rest, char)] 135 | end 136 | 137 | defp do_group("", _char), do: [] 138 | defp do_group(<>, char) do 139 | [char, left | do_group(rest, char)] 140 | end 141 | 142 | defp integer_base(:hex), do: 16 143 | defp integer_base(:upper_hex), do: 16 144 | defp integer_base(:octal), do: 8 145 | defp integer_base(:binary), do: 2 146 | defp integer_base(:decimal), do: 10 147 | 148 | defp integer_base_prefix(true, :hex), do: '0x' 149 | defp integer_base_prefix(true, :upper_hex), do: '0x' 150 | defp integer_base_prefix(true, :octal), do: '0o' 151 | defp integer_base_prefix(true, :binary), do: '0b' 152 | defp integer_base_prefix(_, _), do: '' 153 | 154 | defp sign_prefix(:plus, true), do: '+' 155 | defp sign_prefix(:plus, false), do: '-' 156 | defp sign_prefix(:minus, true), do: '' 157 | defp sign_prefix(:minus, false), do: '-' 158 | defp sign_prefix(:space, true), do: ' ' 159 | defp sign_prefix(:space, false), do: '-' 160 | 161 | defp format_fractional(value, type, format) when is_float(value) do 162 | format_float(value, type, format) 163 | end 164 | defp format_fractional(value, type, format) when is_integer(value) do 165 | format_float(value + 0.0, type, format) 166 | end 167 | defp format_fractional(value, type, format) do 168 | case Format.Fractional.fmt(value, type, format) do 169 | {:ok, chardata} -> 170 | chardata 171 | {:error, float} -> 172 | format_float(float, type, format) 173 | end 174 | end 175 | 176 | # TODO: reimplement float printing & don't rely on erlang 177 | defp format_float(value, type, format) do 178 | %{fill: fill, align: align, width: width, precision: precision, sign: sign} = format 179 | erlang_format = erlang_format_float(type, precision) 180 | formatted = :io_lib.format(erlang_format, [abs(value)]) 181 | 182 | sign_prefix = sign_prefix(sign, value >= 0) 183 | formatted = maybe_upcase(formatted, type in [:upper_exponent, :upper_general]) 184 | formatted = [sign_prefix | formatted] 185 | if width do 186 | pad(formatted, IO.iodata_length(formatted), width, fill, align || :right) 187 | else 188 | formatted 189 | end 190 | end 191 | 192 | defp erlang_format_float(:float, nil), 193 | do: '~f' 194 | defp erlang_format_float(:float, prec), 195 | do: '~.#{prec}f' 196 | defp erlang_format_float(type, nil) when type in [:exponent, :upper_exponent], 197 | do: '~e' 198 | defp erlang_format_float(type, prec) when type in [:exponent, :upper_exponent], 199 | do: '~.#{prec}e' 200 | defp erlang_format_float(type, nil) when type in [:general, :upper_general], 201 | do: '~g' 202 | defp erlang_format_float(type, prec) when type in [:general, :upper_general], 203 | do: '~.#{prec}g' 204 | 205 | defp maybe_downcase(binary, true), do: downcase(binary, "") 206 | defp maybe_downcase(binary, false), do: binary 207 | 208 | defp downcase(<<>>, acc), 209 | do: acc 210 | defp downcase(<>, acc) when char in ?A..?Z, 211 | do: downcase(rest, acc <> <>) 212 | defp downcase(<>, acc), 213 | do: downcase(rest, acc <> <>) 214 | 215 | defp maybe_upcase(charlist, true), do: upcase(charlist) 216 | defp maybe_upcase(charlist, false), do: charlist 217 | 218 | defp upcase([]), 219 | do: [] 220 | defp upcase([char | rest]) when char in ?a..?z, 221 | do: [char - ?a + ?A | upcase(rest)] 222 | defp upcase([nested | rest]) when is_list(nested), 223 | do: [upcase(nested) | upcase(rest)] 224 | defp upcase([char | rest]), 225 | do: [char | upcase(rest)] 226 | end 227 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Format.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [app: :format, 6 | version: "0.1.0", 7 | elixir: "~> 1.5-dev", 8 | build_embedded: Mix.env == :prod, 9 | start_permanent: Mix.env == :prod, 10 | deps: deps()] 11 | end 12 | 13 | # Configuration for the OTP application 14 | # 15 | # Type "mix help compile.app" for more information 16 | def application do 17 | # Specify extra applications you'll use from Erlang/Elixir 18 | [extra_applications: [:logger]] 19 | end 20 | 21 | # Dependencies can be Hex packages: 22 | # 23 | # {:my_dep, "~> 0.3.0"} 24 | # 25 | # Or git/path repositories: 26 | # 27 | # {:my_dep, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"} 28 | # 29 | # Type "mix help deps" for more examples and options 30 | defp deps do 31 | [] 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /test/format_test.exs: -------------------------------------------------------------------------------- 1 | defmodule FormatTest do 2 | use ExUnit.Case 3 | doctest Format 4 | 5 | test "the truth" do 6 | assert 1 + 1 == 2 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | --------------------------------------------------------------------------------