Skip to content

Commit

Permalink
Merge pull request #65 from JuliaString/spj/fixtypemin
Browse files Browse the repository at this point in the history
Add support for Python ^ (center justification), fix issue #63, and Formatting.jl#84
  • Loading branch information
ScottPJones authored Jan 30, 2024
2 parents 5f0fb70 + d116e94 commit f4c9b77
Show file tree
Hide file tree
Showing 6 changed files with 108 additions and 49 deletions.
4 changes: 0 additions & 4 deletions src/Format.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
5 changes: 5 additions & 0 deletions src/fmt.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
92 changes: 52 additions & 40 deletions src/fmtcore.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -252,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 * exp(-e - i)
u = round(v10 * rax * exp10(-e - i); sigdigits = fs.prec + 1)
end
end

Expand All @@ -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

Expand All @@ -305,4 +318,3 @@ function _pfmt_specialf(out::IO, fs::FormatSpec, x::AbstractFloat)
_pfmt_s(out, fs, "NaN")
end
end

7 changes: 3 additions & 4 deletions src/fmtspec.jl
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
#
# spec ::= [[fill]align][sign][#][0][width][,][.prec][type]
# fill ::= <any character>
# align ::= '<' | '>'
# align ::= '<' | '^' | '>'
# sign ::= '+' | '-' | ' '
# width ::= <integer>
# prec ::= <integer>
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion test/fmt.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
46 changes: 46 additions & 0 deletions test/fmtspec.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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"

Expand All @@ -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
Expand All @@ -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"

Expand All @@ -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"
Expand All @@ -218,6 +252,14 @@ 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"

# 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
Expand All @@ -238,9 +280,13 @@ 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

0 comments on commit f4c9b77

Please sign in to comment.