Skip to content
This repository was archived by the owner on Mar 1, 2024. It is now read-only.

WIP: New interface proposal for numerical formatting: fmt and fmt_default! #10

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion src/Formatting.jl
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,18 @@ module Formatting

export
FormatSpec, FormatExpr,
printfmt, printfmtln, fmt, format,
printfmt, printfmtln, cfmt, format,
sprintf1, generate_formatter

export
fmt, fmt_default, fmt_default!

using Compat

include("cformat.jl" )
include("fmtspec.jl")
include("fmtcore.jl")
include("formatexpr.jl")
include("fmt.jl")

end # module
173 changes: 173 additions & 0 deletions src/fmt.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@

# interface proposal by Tom Breloff (@tbreloff)... comments welcome
# This uses the more basic formatting based on FormatSpec and the cfmt method (formerly called fmt, which I repurposed)

# TODO: swap out FormatSpec for something that is able to use the "format" method, which has more options for units, prefixes, etc
# TODO: support rational numbers, autoscale, etc as in "format"

# --------------------------------------------------------------------------------------------------

# the DefaultSpec object is just something to hold onto the current FormatSpec.
# we keep the typechar around specically for the reset! function, to go back to the starting state

type DefaultSpec
typechar::Char
fspec::FormatSpec
DefaultSpec(c::Char) = new(c, FormatSpec(c))
end

const DEFAULT_FORMATTERS = Dict{DataType, DefaultSpec}()

# adds a new default formatter for this type
defaultSpec!{T}(::Type{T}, c::Char) = (DEFAULT_FORMATTERS[T] = DefaultSpec(c); nothing)

# note: types T and K will now both share K's default
defaultSpec!{T,K}(::Type{T}, ::Type{K}) = (DEFAULT_FORMATTERS[T] = DEFAULT_FORMATTERS[K]; nothing)

# seed it with some basic default formatters
for (t, c) in [(Integer,'d'), (FloatingPoint,'f'), (Char,'c'), (String,'s')]
defaultSpec!(t, c)
end

reset!{T}(::Type{T}) = (dspec = defaultSpec(T); dspec.fspec = FormatSpec(dspec.typechar); nothing)


# --------------------------------------------------------------------------------------------------


function addKWArgsFromSymbols(kwargs, syms::Symbol...)
d = Dict(kwargs)
for s in syms
if s == :ljust || s == :left
d[:align] = '<'
elseif s == :rjust || s == :right
d[:align] = '>'
elseif s == :commas
d[:tsep] = true
elseif s == :zpad || s == :zeropad
d[:zpad] = true
elseif s == :ipre || s == :prefix
d[:ipre] = true
end
end
d
end

# --------------------------------------------------------------------------------------------------

# methods to get the current default objects
# note: if you want to set a default for an abstract type (i.e. FloatingPoint) you'll need to extend this method like here:
defaultSpec{T<:Integer}(::Type{T}) = DEFAULT_FORMATTERS[Integer]
defaultSpec{T<:FloatingPoint}(::Type{T}) = DEFAULT_FORMATTERS[FloatingPoint]
defaultSpec{T<:String}(::Type{T}) = DEFAULT_FORMATTERS[String]
function defaultSpec{T}(::Type{T})
get(DEFAULT_FORMATTERS, T) do
error("Missing default spec for type $T... call default!(T, c): $DEFAULT_FORMATTERS")
end
end
defaultSpec(x) = defaultSpec(typeof(x))

fmt_default{T}(::Type{T}) = defaultSpec(T).fspec
fmt_default(x) = defaultSpec(x).fspec



# first resets the fmt_default spec to the given arg, then continue by updating with args and kwargs
fmt_default!{T}(::Type{T}, c::Char, args...; kwargs...) = (defaultSpec!(T,c); fmt_default!(T, args...; kwargs...))
fmt_default!{T,K}(::Type{T}, ::Type{K}, args...; kwargs...) = (defaultSpec!(T,K); fmt_default!(T, args...; kwargs...))

# update the fmt_default for a specific type
function fmt_default!{T}(::Type{T}, syms::Symbol...; kwargs...)
if isempty(syms)

# if there are no arguments, reset to initial defaults
if isempty(kwargs)
reset!(T)
return
end

# otherwise update the spec
dspec = defaultSpec(T)
dspec.fspec = FormatSpec(dspec.fspec; kwargs...)

else
d = addKWArgsFromSymbols(kwargs, syms...)
fmt_default!(T; d...)
end
nothing
end

# update the fmt_default for all types
function fmt_default!(syms::Symbol...; kwargs...)
if isempty(syms)
for k in keys(DEFAULT_FORMATTERS)
fmt_default!(k; kwargs...)
end
else
d = addKWArgsFromSymbols(kwargs, syms...)
fmt_default!(; d...)
end
nothing
end


# --------------------------------------------------------------------------------------------------

# TODO: get rid of this entire hack by moving commas into cfmt

function optionalCommas(x::Real, s::String, fspec::FormatSpec)
dpos = findfirst(s, '.')
prevwidth = length(s)

if dpos == 0
s = addcommas(s)
else
s = string(addcommas(s[1:dpos-1]), '.', s[dpos+1:end])
end

# check for excess width from commas
w = length(s)
if fspec.width > 0 && w > fspec.width && w > prevwidth
# we may have made the string too wide with those commas... gotta fix it
s = strip(s)
n = fspec.width - length(s)
if fspec.align == '<' # left alignment
s = string(s, " "^n)
else
s = string(" "^n, s)
end
end

s
end
optionalCommas(x, s::String, fspec::FormatSpec) = s

# --------------------------------------------------------------------------------------------------


# TODO: do more caching to optimize repeated calls

# creates a new FormatSpec by overriding the defaults and passes it to cfmt
# 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 = isempty(kwargs) ? fmt_default(x) : FormatSpec(fmt_default(x); kwargs...)
s = cfmt(fspec, x)

# add the commas now... I was confused as to when this is done currently
if fspec.tsep
return optionalCommas(x, s, fspec)
end
s
end

# some helper method calls, which just convert to kwargs
fmt(x, prec::Int, args...; kwargs...) = fmt(x, args...; prec=prec, kwargs...)
fmt(x, prec::Int, width::Int, args...; kwargs...) = fmt(x, args...; prec=prec, width=width, kwargs...)

# integrate some symbol shorthands into the keyword args
# note: as above, this will generate relavent kwargs, so to format in a tight loop, you should probably update the fmt_default
function fmt(x, syms::Symbol...; kwargs...)
d = addKWArgsFromSymbols(kwargs, syms...)
fmt(x; d...)
end
17 changes: 15 additions & 2 deletions src/fmtspec.jl
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,19 @@ immutable FormatSpec
end
new(cls, typ, fill, align, sign, width, prec, ipre, zpad, tsep)
end

# copy constructor with overrides
function FormatSpec(spec::FormatSpec;
fill::Char=spec.fill,
align::Char=spec.align,
sign::Char=spec.sign,
width::Int=spec.width,
prec::Int=spec.prec,
ipre::Bool=spec.ipre,
zpad::Bool=spec.zpad,
tsep::Bool=spec.tsep)
new(spec.cls, spec.typ, fill, align, sign, width, prec, ipre, zpad, tsep)
end
end

function show(io::IO, fs::FormatSpec)
Expand Down Expand Up @@ -196,5 +209,5 @@ end

printfmt(fs::FormatSpec, x) = printfmt(STDOUT, fs, x)

fmt(fs::FormatSpec, x) = (buf = IOBuffer(); printfmt(buf, fs, x); bytestring(buf))
fmt(spec::String, x) = fmt(FormatSpec(spec), x)
cfmt(fs::FormatSpec, x) = (buf = IOBuffer(); printfmt(buf, fs, x); bytestring(buf))
cfmt(spec::String, x) = cfmt(FormatSpec(spec), x)
31 changes: 31 additions & 0 deletions test/fmt.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@

using Formatting
using Base.Test

# some basic functionality testing
x = 1234.56789

@test fmt(x) == "1234.567890"
@test fmt(x,2) == "1234.57"
@test fmt(x,3,10) == " 1234.568"
@test fmt(x,3,10,:left) == "1234.568 "
@test fmt(x,3,10,:ljust) == "1234.568 "
@test fmt(x,:commas) == "1,234.567890"

i = 1234567

@test fmt(i) == "1234567"
@test fmt(i,:commas) == "1,234,567"

fmt_default!(Int, :commas, width = 12)
@test fmt(i) == " 1,234,567"
@test fmt(x) == "1234.567890" # default hasn't changed

fmt_default!(:commas)
@test fmt(i) == " 1,234,567"
@test fmt(x) == "1,234.567890" # width hasn't changed, but added commas

fmt_default!(Int) # resets Integer defaults
@test fmt(i) == "1234567"
@test fmt(i,:commas) == "1,234,567"

7 changes: 4 additions & 3 deletions test/runtests.jl
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using Formatting
using Base.Test

include( "cformat.jl" )
include( "fmtspec.jl" )
include( "formatexpr.jl" )
# include( "cformat.jl" )
# include( "fmtspec.jl" )
# include( "formatexpr.jl" )
include( "fmt.jl" )