From 88dff5c510af64560e73b43a36a01bb85f01e725 Mon Sep 17 00:00:00 2001 From: ScottPJones Date: Wed, 10 Jan 2024 09:28:52 -0500 Subject: [PATCH 1/2] Add support for Python ^ (center justification) --- src/Format.jl | 4 --- src/fmt.jl | 5 +++ src/fmtcore.jl | 90 ++++++++++++++++++++++++++++--------------------- src/fmtspec.jl | 7 ++-- test/fmt.jl | 3 +- test/fmtspec.jl | 42 +++++++++++++++++++++++ 6 files changed, 103 insertions(+), 48 deletions(-) diff --git a/src/Format.jl b/src/Format.jl index 57ea417..33fd769 100644 --- a/src/Format.jl +++ b/src/Format.jl @@ -91,10 +91,6 @@ One can use ``pyfmt`` to format a single value into a string, or ``format`` to f At this point, this package implements a subset of Python's formatting language (with slight modification). Here is a summary of the differences: -- ``g`` and ``G`` for floating point formatting have not been supported yet. Please use ``f``, ``e``, or ``E`` instead. - -- The package currently provides default alignment, left alignment ``<`` and right alignment ``>``. Other form of alignment such as centered alignment ``^`` has not been supported yet. - - In terms of argument specification, it supports natural ordering (e.g. ``{} + {}``), explicit position (e.g. ``{1} + {2}``). It hasn't supported named arguments or fields extraction yet. Note that mixing these two modes is not allowed (e.g. ``{1} + {}``). - The package provides support for filtering (for explicitly positioned arguments), such as ``{1|>lowercase}`` by allowing one to embed the ``|>`` operator, which the Python counter part does not support. diff --git a/src/fmt.jl b/src/fmt.jl index 344b143..0f822c4 100644 --- a/src/fmt.jl +++ b/src/fmt.jl @@ -57,6 +57,8 @@ function _add_kwargs_from_symbols(kwargs, syms::Symbol...) d[:align] = '<' elseif s == :rjust || s == :right d[:align] = '>' + elseif s == :center + d[:align] = '^' elseif s == :commas d[:tsep] = true elseif s == :zpad || s == :zeropad @@ -146,6 +148,7 @@ function _optional_commas(x::Real, s::AbstractString, fspec::FormatSpec) if fspec.width > 0 && w > fspec.width && w > prevwidth # we may have made the string too wide with those commas... gotta fix it # left or right alignment ('<' is left) + # TODO: handle center alignment s = fspec.align == '<' ? rpad(strip(s), fspec.width) : lpad(strip(s), fspec.width) end s @@ -165,6 +168,7 @@ Symbol | Meaning ------------------|------------------------------------------ :ljust or :left | Left justified, same as < for FormatSpec :rjust or :right | Right justified, same as > for FormatSpec +:center | Center justified, same as ^ for FormatSpec :zpad or :zeropad | Pad with 0s on left :ipre or :prefix | Whether to prefix 0b, 0o, or 0x :commas | Add commas every 3 digits @@ -195,6 +199,7 @@ function fmt(x; kwargs...) fspec = fmt_default(x) isempty(kwargs) || (fspec = FormatSpec(fspec; kwargs...)) s = pyfmt(fspec, x) + # TODO: allow other thousands separators besides comma # add the commas now... I was confused as to when this is done currently fspec.tsep ? _optional_commas(x, s, fspec) : s end diff --git a/src/fmtcore.jl b/src/fmtcore.jl index 9eb89cd..e96a9af 100644 --- a/src/fmtcore.jl +++ b/src/fmtcore.jl @@ -15,15 +15,18 @@ end ### print string or char function _pfmt_s(out::IO, fs::FormatSpec, s::Union{AbstractString,AbstractChar}) - wid = fs.width - slen = length(s) - if wid <= slen + pad = fs.width - length(s) + if pad <= 0 print(out, s) elseif fs.align == '<' print(out, s) - _repprint(out, fs.fill, wid-slen) + _repprint(out, fs.fill, pad) + elseif fs.align == '^' + _repprint(out, fs.fill, pad>>1) + print(out, s) + _repprint(out, fs.fill, (pad+1)>>1) else - _repprint(out, fs.fill, wid-slen) + _repprint(out, fs.fill, pad) print(out, s) end end @@ -122,16 +125,20 @@ function _pfmt_imin(out::IO, fs::FormatSpec, x::Integer, op::Op) where {Op} end # printing - wid = fs.width - if wid <= xlen + pad = fs.width - xlen + if pad <= 0 _pfmt_intmin(out, ip, 0, s) elseif fs.zpad - _pfmt_intmin(out, ip, wid-xlen, s) + _pfmt_intmin(out, ip, pad, s) elseif fs.align == '<' _pfmt_intmin(out, ip, 0, s) - _repprint(out, fs.fill, wid-xlen) + _repprint(out, fs.fill, pad) + elseif fs.align == '^' + _repprint(out, fs.fill, pad>>1) + _pfmt_intmin(out, ip, 0, s) + _repprint(out, fs.fill, (pad+1)>>1) else - _repprint(out, fs.fill, wid-xlen) + _repprint(out, fs.fill, pad) _pfmt_intmin(out, ip, 0, s) end end @@ -153,16 +160,20 @@ function _pfmt_i(out::IO, fs::FormatSpec, x::Integer, op::Op) where {Op} end # printing - wid = fs.width - if wid <= xlen + pad = fs.width - xlen + if pad <= 0 _pfmt_int(out, sch, ip, 0, ax, op) elseif fs.zpad - _pfmt_int(out, sch, ip, wid-xlen, ax, op) + _pfmt_int(out, sch, ip, pad, ax, op) elseif fs.align == '<' _pfmt_int(out, sch, ip, 0, ax, op) - _repprint(out, fs.fill, wid-xlen) + _repprint(out, fs.fill, pad) + elseif fs.align == '^' + _repprint(out, fs.fill, pad>>1) + _pfmt_int(out, sch, ip, 0, ax, op) + _repprint(out, fs.fill, (pad+1)>>1) else - _repprint(out, fs.fill, wid-xlen) + _repprint(out, fs.fill, pad) _pfmt_int(out, sch, ip, 0, ax, op) end end @@ -205,20 +216,21 @@ function _pfmt_f(out::IO, fs::FormatSpec, x::AbstractFloat) sch != '\0' && (xlen += 1) # print - wid = fs.width - if wid <= xlen + pad = fs.width - xlen + if pad <= 0 _pfmt_float(out, sch, 0, intv, decv, fs.prec) elseif fs.zpad - _pfmt_float(out, sch, wid-xlen, intv, decv, fs.prec) + _pfmt_float(out, sch, pad, intv, decv, fs.prec) + elseif fs.align == '<' + _pfmt_float(out, sch, 0, intv, decv, fs.prec) + _repprint(out, fs.fill, pad) + elseif fs.align == '^' + _repprint(out, fs.fill, pad>>1) + _pfmt_float(out, sch, 0, intv, decv, fs.prec) + _repprint(out, fs.fill, (pad+1)>>1) else - a = fs.align - if a == '<' - _pfmt_float(out, sch, 0, intv, decv, fs.prec) - _repprint(out, fs.fill, wid-xlen) - else - _repprint(out, fs.fill, wid-xlen) - _pfmt_float(out, sch, 0, intv, decv, fs.prec) - end + _repprint(out, fs.fill, pad) + _pfmt_float(out, sch, 0, intv, decv, fs.prec) end end @@ -259,7 +271,7 @@ function _pfmt_e(out::IO, fs::FormatSpec, x::AbstractFloat) i += 1 i > 18 && (u = 0.0; e = 0; break) v10 *= 10 - u = v10 * rax * exp(-e - i) + u = v10 * rax * exp10(-e - i) end end @@ -270,20 +282,21 @@ function _pfmt_e(out::IO, fs::FormatSpec, x::AbstractFloat) # print ec = isuppercase(fs.typ) ? 'E' : 'e' - wid = fs.width - if wid <= xlen + pad = fs.width - xlen + if pad <= 0 _pfmt_floate(out, sch, 0, u, fs.prec, e, ec) elseif fs.zpad - _pfmt_floate(out, sch, wid-xlen, u, fs.prec, e, ec) + _pfmt_floate(out, sch, pad, u, fs.prec, e, ec) + elseif fs.align == '<' + _pfmt_floate(out, sch, 0, u, fs.prec, e, ec) + _repprint(out, fs.fill, pad) + elseif fs.align == '^' + _repprint(out, fs.fill, pad>>1) + _pfmt_floate(out, sch, 0, u, fs.prec, e, ec) + _repprint(out, fs.fill, (pad+1)>>1) else - a = fs.align - if a == '<' - _pfmt_floate(out, sch, 0, u, fs.prec, e, ec) - _repprint(out, fs.fill, wid-xlen) - else - _repprint(out, fs.fill, wid-xlen) - _pfmt_floate(out, sch, 0, u, fs.prec, e, ec) - end + _repprint(out, fs.fill, pad) + _pfmt_floate(out, sch, 0, u, fs.prec, e, ec) end end @@ -305,4 +318,3 @@ function _pfmt_specialf(out::IO, fs::FormatSpec, x::AbstractFloat) _pfmt_s(out, fs, "NaN") end end - diff --git a/src/fmtspec.jl b/src/fmtspec.jl index 19c97b6..c1ac2d8 100644 --- a/src/fmtspec.jl +++ b/src/fmtspec.jl @@ -4,7 +4,7 @@ # # spec ::= [[fill]align][sign][#][0][width][,][.prec][type] # fill ::= -# align ::= '<' | '>' +# align ::= '<' | '^' | '>' # sign ::= '+' | '-' | ' ' # width ::= # prec ::= @@ -84,7 +84,7 @@ end ## parse FormatSpec from a string -const _spec_regex = r"^(.?[<>])?([ +-])?(#)?(\d+)?(,)?(.\d+)?([bcdeEfFgGnosxX])?$" +const _spec_regex = r"^(.?[<^>])?([ +-])?(#)?(\d+)?(,)?(.\d+)?([bcdeEfFgGnosxX])?$" function FormatSpec(s::AbstractString) # default spec @@ -180,8 +180,7 @@ function printfmt(io::IO, fs::FormatSpec, x) fx = float(x) if isfinite(fx) 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).") + ty == 'e' || ty == 'E' ? _pfmt_e(io, fs, fx) : _pfmt_g(io, fs, fx) else _pfmt_specialf(io, fs, fx) end diff --git a/test/fmt.jl b/test/fmt.jl index 57b56ad..6b91534 100644 --- a/test/fmt.jl +++ b/test/fmt.jl @@ -7,7 +7,8 @@ x = 1234.56789 @test fmt(x,10,3,:left) == "1234.568 " @test fmt(x,10,3,:ljust) == "1234.568 " @test fmt(x,10,3,:right) == " 1234.568" -@test fmt(x,10,3,:lrjust) == " 1234.568" +@test fmt(x,10,3,:rjust) == " 1234.568" +@test fmt(x,10,3,:center) == " 1234.568 " @test fmt(x,10,3,:zpad) == "001234.568" @test fmt(x,10,3,:zeropad) == "001234.568" @test fmt(x,:commas) == "1,234.567890" diff --git a/test/fmtspec.jl b/test/fmtspec.jl index d5e21e8..2d38e50 100644 --- a/test/fmtspec.jl +++ b/test/fmtspec.jl @@ -46,6 +46,7 @@ end @test FormatSpec("<8d") == FormatSpec('d'; width=8, align='<') @test FormatSpec("#<8d") == FormatSpec('d'; width=8, fill='#', align='<') @test FormatSpec("⋆<8d") == FormatSpec('d'; width=8, fill='⋆', align='<') + @test FormatSpec("#^8d") == FormatSpec('d'; width=8, fill='#', align='^') @test FormatSpec("#8,d") == FormatSpec('d'; width=8, ipre=true, tsep=true) end @@ -66,8 +67,12 @@ end @test pyfmt("5s", "αβγ") == "αβγ " @test pyfmt(">5s", "abc") == " abc" @test pyfmt(">5s", "αβγ") == " αβγ" + @test pyfmt("^5s", "abc") == " abc " + @test pyfmt("^5s", "αβγ") == " αβγ " @test pyfmt("*>5s", "abc") == "**abc" @test pyfmt("⋆>5s", "αβγ") == "⋆⋆αβγ" + @test pyfmt("*^5s", "abc") == "*abc*" + @test pyfmt("⋆^5s", "αβγ") == "⋆αβγ⋆" @test pyfmt("*<5s", "abc") == "abc**" @test pyfmt("⋆<5s", "αβγ") == "αβγ⋆⋆" end @@ -86,8 +91,12 @@ end @test pyfmt("3c", 'γ') == "γ " @test pyfmt(">3c", 'c') == " c" @test pyfmt(">3c", 'γ') == " γ" + @test pyfmt("^3c", 'c') == " c " + @test pyfmt("^3c", 'γ') == " γ " @test pyfmt("*>3c", 'c') == "**c" @test pyfmt("⋆>3c", 'γ') == "⋆⋆γ" + @test pyfmt("*^3c", 'c') == "*c*" + @test pyfmt("⋆^3c", 'γ') == "⋆γ⋆" @test pyfmt("*<3c", 'c') == "c**" @test pyfmt("⋆<3c", 'γ') == "γ⋆⋆" end @@ -116,12 +125,17 @@ end @test pyfmt(" 6d", 123) == " 123" @test pyfmt("<6d", 123) == "123 " @test pyfmt(">6d", 123) == " 123" + @test pyfmt("^6d", 123) == " 123 " @test pyfmt("*<6d", 123) == "123***" @test pyfmt("⋆<6d", 123) == "123⋆⋆⋆" + @test pyfmt("*^6d", 123) == "*123**" + @test pyfmt("⋆^6d", 123) == "⋆123⋆⋆" @test pyfmt("*>6d", 123) == "***123" @test pyfmt("⋆>6d", 123) == "⋆⋆⋆123" @test pyfmt("< 6d", 123) == " 123 " @test pyfmt("<+6d", 123) == "+123 " + @test pyfmt("^ 6d", 123) == " 123 " + @test pyfmt("^+6d", 123) == " +123 " @test pyfmt("> 6d", 123) == " 123" @test pyfmt(">+6d", 123) == " +123" @@ -130,7 +144,14 @@ end @test pyfmt(" d", -123) == "-123" @test pyfmt("06d", -123) == "-00123" @test pyfmt("<6d", -123) == "-123 " + @test pyfmt("^6d", -123) == " -123 " @test pyfmt(">6d", -123) == " -123" + + # Issue #110 (in Formatting.jl) + f = FormatExpr("{:+d}") + for T in (Int8, Int16, Int32, Int64, Int128) + @test format(f, typemin(T)) = string(typemin(T)) + end end @testset "Format floating point (f)" begin @@ -150,22 +171,32 @@ end @test pyfmt("8.2f", 8.376) == " 8.38" @test pyfmt("<8.2f", 8.376) == "8.38 " + @test pyfmt("^8.2f", 8.376) == " 8.38 " @test pyfmt(">8.2f", 8.376) == " 8.38" @test pyfmt("8.2f", -8.376) == " -8.38" @test pyfmt("<8.2f", -8.376) == "-8.38 " + @test pyfmt("^8.2f", -8.376) == " -8.38 " @test pyfmt(">8.2f", -8.376) == " -8.38" @test pyfmt(".0f", 8.376) == "8" + # Note: these act differently than Python, but it looks like Python might be wrong + # in at least some of these cases (zero padding should be *after* the sign, IMO) @test pyfmt("<08.2f", 8.376) == "00008.38" + @test pyfmt("^08.2f", 8.376) == "00008.38" @test pyfmt(">08.2f", 8.376) == "00008.38" @test pyfmt("<08.2f", -8.376) == "-0008.38" + @test pyfmt("^08.2f", -8.376) == "-0008.38" @test pyfmt(">08.2f", -8.376) == "-0008.38" @test pyfmt("*<8.2f", 8.376) == "8.38****" @test pyfmt("⋆<8.2f", 8.376) == "8.38⋆⋆⋆⋆" + @test pyfmt("*^8.2f", 8.376) == "**8.38**" + @test pyfmt("⋆^8.2f", 8.376) == "⋆⋆8.38⋆⋆" @test pyfmt("*>8.2f", 8.376) == "****8.38" @test pyfmt("⋆>8.2f", 8.376) == "⋆⋆⋆⋆8.38" @test pyfmt("*<8.2f", -8.376) == "-8.38***" @test pyfmt("⋆<8.2f", -8.376) == "-8.38⋆⋆⋆" + @test pyfmt("*^8.2f", -8.376) == "*-8.38**" + @test pyfmt("⋆^8.2f", -8.376) == "⋆-8.38⋆⋆" @test pyfmt("*>8.2f", -8.376) == "***-8.38" @test pyfmt("⋆>8.2f", -8.376) == "⋆⋆⋆-8.38" @@ -190,9 +221,12 @@ end @test pyfmt("8e", 1234.5678) == "1.234568e+03" @test pyfmt("<12.2e", 13.89) == "1.39e+01 " + @test pyfmt("^12.2e", 13.89) == " 1.39e+01 " @test pyfmt(">12.2e", 13.89) == " 1.39e+01" @test pyfmt("*<12.2e", 13.89) == "1.39e+01****" @test pyfmt("⋆<12.2e", 13.89) == "1.39e+01⋆⋆⋆⋆" + @test pyfmt("*^12.2e", 13.89) == "**1.39e+01**" + @test pyfmt("⋆^12.2e", 13.89) == "⋆⋆1.39e+01⋆⋆" @test pyfmt("*>12.2e", 13.89) == "****1.39e+01" @test pyfmt("⋆>12.2e", 13.89) == "⋆⋆⋆⋆1.39e+01" @test pyfmt("012.2e", 13.89) == "00001.39e+01" @@ -218,6 +252,11 @@ end @test pyfmt("10.2e", 9.999e99) == " 1.00e+100" @test pyfmt("11.2e", BigFloat("9.999e999")) == " 1.00e+1000" @test pyfmt("10.2e", -9.999e-100) == " -1.00e-99" + + # issue #84 (from Formatting.jl) + @test pyfmt("+11.3e", 1.0e-309) == "+1.000e-309" + @test pyfmt("+11.3e", 1.0e-313) == "+1.000e-313" + end @testset "Format special floating point value" begin @@ -238,9 +277,12 @@ end @test pyfmt("e", -Inf32) == "-Inf" @test pyfmt("<5f", Inf) == "Inf " + @test pyfmt("^5f", Inf) == " Inf " @test pyfmt(">5f", Inf) == " Inf" @test pyfmt("*<5f", Inf) == "Inf**" @test pyfmt("⋆<5f", Inf) == "Inf⋆⋆" + @test pyfmt("*^5f", Inf) == "*Inf*" + @test pyfmt("⋆^5f", Inf) == "⋆Inf⋆" @test pyfmt("*>5f", Inf) == "**Inf" @test pyfmt("⋆>5f", Inf) == "⋆⋆Inf" end From d116e94035f8217487fef6a916a10168fe8d86b0 Mon Sep 17 00:00:00 2001 From: ScottPJones Date: Tue, 30 Jan 2024 17:11:16 -0500 Subject: [PATCH 2/2] Fix issue #63 - incorrect results with python format e --- src/fmtcore.jl | 4 ++-- test/fmtspec.jl | 6 +++++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/fmtcore.jl b/src/fmtcore.jl index e96a9af..7983a45 100644 --- a/src/fmtcore.jl +++ b/src/fmtcore.jl @@ -264,14 +264,14 @@ function _pfmt_e(out::IO, fs::FormatSpec, x::AbstractFloat) else rax = round(ax; sigdigits = fs.prec + 1) e = floor(Integer, log10(rax)) # exponent - u = rax * exp10(-e) # significand + u = round(rax * exp10(-e); sigdigits = fs.prec + 1) # significand i = 0 v10 = 1 while isinf(u) i += 1 i > 18 && (u = 0.0; e = 0; break) v10 *= 10 - u = v10 * rax * exp10(-e - i) + u = round(v10 * rax * exp10(-e - i); sigdigits = fs.prec + 1) end end diff --git a/test/fmtspec.jl b/test/fmtspec.jl index 2d38e50..ddf6377 100644 --- a/test/fmtspec.jl +++ b/test/fmtspec.jl @@ -150,7 +150,7 @@ end # Issue #110 (in Formatting.jl) f = FormatExpr("{:+d}") for T in (Int8, Int16, Int32, Int64, Int128) - @test format(f, typemin(T)) = string(typemin(T)) + @test format(f, typemin(T)) == string(typemin(T)) end end @@ -257,6 +257,9 @@ end @test pyfmt("+11.3e", 1.0e-309) == "+1.000e-309" @test pyfmt("+11.3e", 1.0e-313) == "+1.000e-313" + # issue #108 (from Formatting.jl) + @test pyfmt(".1e", 0.0003) == "3.0e-04" + @test pyfmt(".1e", 0.0006) == "6.0e-04" end @testset "Format special floating point value" begin @@ -279,6 +282,7 @@ end @test pyfmt("<5f", Inf) == "Inf " @test pyfmt("^5f", Inf) == " Inf " @test pyfmt(">5f", Inf) == " Inf" + @test pyfmt("*<5f", Inf) == "Inf**" @test pyfmt("⋆<5f", Inf) == "Inf⋆⋆" @test pyfmt("*^5f", Inf) == "*Inf*"