diff --git a/Project.toml b/Project.toml index 3b658ed..4a7a436 100644 --- a/Project.toml +++ b/Project.toml @@ -23,7 +23,7 @@ keywords = ["Strings", "Formatting"] license = "MIT" name = "Format" uuid = "1fa38f19-a742-5d3f-a2b9-30dd87b9d5f8" -version = "1.1.0" +version = "1.2.0" [deps] Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7" diff --git a/src/cformat.jl b/src/cformat.jl index 3b04b2a..2a939d0 100644 --- a/src/cformat.jl +++ b/src/cformat.jl @@ -1,6 +1,16 @@ formatters = Dict{ ASCIIStr, Function }() -cfmt( fmt::ASCIIStr, x ) = m_eval(Expr(:call, generate_formatter( fmt ), x)) +cfmt( fmt::ASCIIStr, x::Union{<:AbstractString,<:Real,<:Rational,<:Char} ) = m_eval(Expr(:call, generate_formatter( fmt ), x)) + +function cfmt( fmt_str::ASCIIStr, x::Number ) + #remove width information + new_fmt_str = replace(fmt_str, r"(%(\d+\$)?[\-\+#0' ]*)(\d+)?"=>s"\1") + s = fmt_Number(x, x->m_eval(Expr(:call, generate_formatter( new_fmt_str ), AbstractFloat(x)))) + # extract width information + m = match(r"%(\d+\$)?[\-\+#0' ]*(\d+)?", fmt_str) + width = m[2] == nothing ? 0 : parse(Int, m[2]) + fmt(s, width, occursin("-", fmt_str) ? :left : :right) +end function checkfmt(fmt) test = PF.parse( fmt ) @@ -117,6 +127,11 @@ function generate_format_string(; String(append!(s, _codeunits(conversion))) end +function format(x::T; kwargs...) where T<:Number + s = fmt_Number(x, x->format(AbstractFloat(x); kwargs..., width=-1)) + fmt(s, get(kwargs, :width, 0), get(kwargs, :leftjustified, false) ? :left : :right) +end + function format( x::T; width::Int=-1, precision::Int= -1, diff --git a/src/fmt.jl b/src/fmt.jl index 344b143..80071ab 100644 --- a/src/fmt.jl +++ b/src/fmt.jl @@ -29,7 +29,7 @@ function DefaultSpec(c::AbstractChar, syms...; kwargs...) end end -const DEFAULT_FORMATTERS = Dict{DataType, DefaultSpec}() +const DEFAULT_FORMATTERS = Dict{Type{<:Any}, DefaultSpec}() # adds a new default formatter for this type default_spec!(::Type{T}, c::AbstractChar) where {T} = @@ -73,12 +73,21 @@ end # methods to get the current default objects # note: if you want to set a default for an abstract type (i.e. AbstractFloat) # you'll need to extend this method like here: + +const ComplexInteger = Complex{T} where T<:Integer +const ComplexFloat = Complex{T} where T<:AbstractFloat +const ComplexRational = Complex{T} where T<:Rational + default_spec(::Type{<:Integer}) = DEFAULT_FORMATTERS[Integer] default_spec(::Type{<:AbstractFloat}) = DEFAULT_FORMATTERS[AbstractFloat] default_spec(::Type{<:AbstractString}) = DEFAULT_FORMATTERS[AbstractString] default_spec(::Type{<:AbstractChar}) = DEFAULT_FORMATTERS[AbstractChar] default_spec(::Type{<:AbstractIrrational}) = DEFAULT_FORMATTERS[AbstractIrrational] +default_spec(::Type{<:Rational}) = DEFAULT_FORMATTERS[Rational] default_spec(::Type{<:Number}) = DEFAULT_FORMATTERS[Number] +default_spec(::Type{<:ComplexInteger}) = DEFAULT_FORMATTERS[ComplexInteger] +default_spec(::Type{<:ComplexFloat}) = DEFAULT_FORMATTERS[ComplexFloat] +default_spec(::Type{<:ComplexRational}) = DEFAULT_FORMATTERS[ComplexRational] default_spec(::Type{T}) where {T} = get(DEFAULT_FORMATTERS, T) do @@ -189,7 +198,7 @@ function fmt end # TODO: do more caching to optimize repeated calls # creates a new FormatSpec by overriding the defaults and passes it to pyfmt -# note: adding kwargs is only appropriate for one-off formatting. +# note: adding kwargs is only appropriate for one-off formatting. # normally it will be much faster to change the fmt_default formatting as needed function fmt(x; kwargs...) fspec = fmt_default(x) @@ -220,9 +229,13 @@ end for (t, c) in [(Integer,'d'), (AbstractFloat,'f'), (AbstractChar,'c'), - (AbstractString,'s')] + (AbstractString,'s'), + (ComplexInteger,'d'), + (ComplexFloat,'f')] default_spec!(t, c) end -default_spec!(Number, 's', :right) +default_spec!(Rational, 's', :right) default_spec!(AbstractIrrational, 's', :right) +default_spec!(ComplexRational, 's', :right) +default_spec!(Number, 's', :right) diff --git a/src/fmtcore.jl b/src/fmtcore.jl index 712b675..5d79f7d 100644 --- a/src/fmtcore.jl +++ b/src/fmtcore.jl @@ -1,4 +1,5 @@ # core formatting functions +export fmt_Number ### auxiliary functions @@ -262,3 +263,42 @@ function _pfmt_specialf(out::IO, fs::FormatSpec, x::AbstractFloat) end end +function _pfmt_Number_f(out::IO, fs::FormatSpec, x::Number, _pf::Function) + fsi = FormatSpec(fs, width = -1) + f = x->begin + fx = AbstractFloat(x) # not float(x), this should error out, if conversion is not possible + io = IOBuffer() + _pf(io, fsi, fx) + String(take!(io)) + end + s = fmt_Number(x, f) + _pfmt_s(out, FormatSpec(fs, tsep = false), s) +end + +function _pfmt_Number_i(out::IO, fs::FormatSpec, x::Number, op::Op, _pf::Function) where {Op} + fsi = FormatSpec(fs, width = -1) + f = x->begin + ix = Integer(x) + io = IOBuffer() + _pf(io, fsi, ix, op) + String(take!(io)) + end + s = fmt_Number(x, f) + _pfmt_s(out, FormatSpec(fs, tsep = false), s) +end + +function _pfmt_i(out::IO, fs::FormatSpec, x::Number, op::Op) where {Op} + _pfmt_Number_i(out, fs, x, op, _pfmt_i) +end + +function _pfmt_f(out::IO, fs::FormatSpec, x::Number) + _pfmt_Number_f(out, fs, x, _pfmt_f) +end + +function _pfmt_e(out::IO, fs::FormatSpec, x::Number) + _pfmt_Number_f(out, fs, x, _pfmt_e) +end + +function fmt_Number(x::Complex, f::Function) + s = f(real(x)) * (imag(x) >= 0 ? " + " : " - ") * f(abs(imag(x))) * "im" +end diff --git a/src/fmtspec.jl b/src/fmtspec.jl index 95d4916..5f08a89 100644 --- a/src/fmtspec.jl +++ b/src/fmtspec.jl @@ -164,20 +164,38 @@ _srepr(x) = repr(x) _srepr(x::AbstractString) = x _srepr(x::AbstractChar) = string(x) _srepr(x::Enum) = string(x) +@static if VERSION < v"1.2.0-DEV" + _srepr(x::Irrational{sym}) where {sym} = string(sym) +end function printfmt(io::IO, fs::FormatSpec, x) cls = fs.cls ty = fs.typ if cls == 'i' - ix = Integer(x) + local ix + try + ix = Integer(x) + catch + ix = x + end ty == 'd' || ty == 'n' ? _pfmt_i(io, fs, ix, _Dec()) : ty == 'x' ? _pfmt_i(io, fs, ix, _Hex()) : ty == 'X' ? _pfmt_i(io, fs, ix, _HEX()) : ty == 'o' ? _pfmt_i(io, fs, ix, _Oct()) : _pfmt_i(io, fs, ix, _Bin()) elseif cls == 'f' - fx = float(x) - if isfinite(fx) + local fx, nospecialf + try + fx = float(x) + catch + fx = x + end + try + nospecialf = isfinite(fx) + catch + nospecialf = true + end + if nospecialf ty == 'f' || ty == 'F' ? _pfmt_f(io, fs, fx) : ty == 'e' || ty == 'E' ? _pfmt_e(io, fs, fx) : error("format for type g or G is not supported yet (use f or e instead).") diff --git a/src/formatexpr.jl b/src/formatexpr.jl index ef8cb45..ffd30ea 100644 --- a/src/formatexpr.jl +++ b/src/formatexpr.jl @@ -151,6 +151,7 @@ function printfmt(io::IO, fe::FormatExpr, args...) end end isempty(fe.suffix) || print(io, fe.suffix) + nothing end const StringOrFE = Union{AbstractString, FormatExpr} diff --git a/test/cformat.jl b/test/cformat.jl index e1d7f98..1ce0bb6 100644 --- a/test/cformat.jl +++ b/test/cformat.jl @@ -187,3 +187,19 @@ end @test format( 100, precision=2, suffix="%", conversion="f" ) == "100.00%" end +@testset "complex numbers" begin + c = 2 - 3.1im + @test cfmt("%20.0f", c) == " 2 - 3im" + @test cfmt("%20.1f", c) == " 2.0 - 3.1im" + @test cfmt("%20.2f", c) == " 2.00 - 3.10im" + @test cfmt("%20s", c) == " 2.0 - 3.1im" + @test cfmt("%20.1e", c) == " 2.0e+00 - 3.1e+00im" + @test cfmt("%-20.1e", c) == "2.0e+00 - 3.1e+00im " + + @test format(c, width=20) == " 2 - 3.1im" + @test format(c, width=20, precision=0) == " 2 - 3im" + @test format(c, width=20, precision=1) == " 2.0 - 3.1im" + @test format(c, width=20, precision=2) == " 2.00 - 3.10im" + @test format(c, width=20, precision=2, leftjustified=true) == "2.00 - 3.10im " + @test format(c, width=20, precision=1, conversion="e") == " 2.0e+00 - 3.1e+00im" +end diff --git a/test/fmt.jl b/test/fmt.jl index 2c2f740..363950a 100644 --- a/test/fmt.jl +++ b/test/fmt.jl @@ -20,8 +20,9 @@ i = 1234567 @test fmt(i) == "1234567" @test fmt(i,:commas) == "1,234,567" -@test_throws ErrorException fmt_default(Real) -@test_throws ErrorException fmt_default(Complex) +# These are not handled +#@test_throws ErrorException fmt_default(Real) +#@test_throws ErrorException fmt_default(Complex) fmt_default!(Int, :commas, width = 12) @test fmt(i) == " 1,234,567" @@ -41,3 +42,28 @@ fmt_default!(UInt16, 'd', :commas) fmt_default!(UInt32, UInt16, width=20) @test fmt(0xfffff) == " 1,048,575" +v = pi +@test fmt(v) == "π" +@test fmt(v; width=10) == " π" + +v = MathConstants.eulergamma +@test fmt(v, 10, 2) == " γ" + +reset!(Number) +reset!(Real) +@test fmt_default(Real) == fmt_default(Number) == FormatSpec('s', align = '>') + +@test fmt(2 - 3im, 10) == " 2 - 3im" +@test fmt(pi - 3im, 15, 2) == " 3.14 - 3.00im" + +reset!(Rational) +@test fmt(3//4, 10) == " 3//4" +@test fmt(1//2 + 6//2 * im, 15) == " 1//2 + 3//1*im" + +fmt_default!(Rational, 'f', prec = 2) +fmt_default!(Format.ComplexRational, 'f', prec = 2) + +@test fmt(3//4, 10) == " 0.75" +@test fmt(3//4, 10, 1) == " 0.8" +@test fmt(1//2 + 6//2 * im, 15) == " 0.50 + 3.00im" +@test fmt(1//2 + 6//2 * im, 15, 1) == " 0.5 + 3.0im" diff --git a/test/fmtspec.jl b/test/fmtspec.jl index a1121b2..6401b24 100644 --- a/test/fmtspec.jl +++ b/test/fmtspec.jl @@ -234,3 +234,40 @@ end @test pyfmt("*>5f", Inf) == "**Inf" @test pyfmt("⋆>5f", Inf) == "⋆⋆Inf" end + +@testset "Format Irrationals" begin + @test pyfmt(">10s", pi) == " π" + @test pyfmt("10s", pi) == "π " + @test pyfmt("3", MathConstants.eulergamma) == "γ " + @test pyfmt("10.2f", MathConstants.eulergamma) == " 0.58" + @test pyfmt("<3s", MathConstants.e) == "ℯ " +end + +@testset "Format Rationals" begin + @test pyfmt("10s", 3//4) == "3//4 " + @test pyfmt("10", 3//4) == "3//4 " + @test pyfmt(">10", 3//4) == " 3//4" + @test pyfmt("10.1f", 3//4) == " 0.8" + @test pyfmt("10.1f", 3//4) == " 0.8" + @test pyfmt("10.1e", 3//4) == " 7.5e-01" +end + +@testset "Format Complex Numbers" begin + c = 2 - 3.1im + @test fmt(round(c), 20, 1) == " 2.0 - 3.0im" + @test fmt(c, 20, 1) == " 2.0 - 3.1im" + @test fmt(c, 20, 2) == " 2.00 - 3.10im" + @test fmt(c, 20) == "2.000000 - 3.100000im" + fmt_default!(Format.ComplexFloat, 'e') + @test fmt(c, 20, 1) == " 2.0e+00 - 3.1e+00im" + fmt_default!(Format.ComplexFloat, 'e', :left) + @test fmt(c, 20, 1) == "2.0e+00 - 3.1e+00im " + + @test format(c, width=20) == " 2 - 3.1im" + @test format(c, width=20, precision=0) == " 2 - 3im" + @test format(c, width=20, precision=1) == " 2.0 - 3.1im" + @test format(c, width=20, precision=2) == " 2.00 - 3.10im" + @test format(c, width=20, precision=2, leftjustified=true) == "2.00 - 3.10im " + @test format(c, width=20, precision=1, conversion="e") == " 2.0e+00 - 3.1e+00im" + fmt_default!(Format.ComplexFloat, 'f') +end