-
Notifications
You must be signed in to change notification settings - Fork 8
Expand file tree
/
Copy pathfmt.jl
More file actions
241 lines (197 loc) · 8.62 KB
/
Copy pathfmt.jl
File metadata and controls
241 lines (197 loc) · 8.62 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
# interface proposal by Tom Breloff (@tbreloff)... comments welcome
# This uses the more basic formatting based on FormatSpec and the pyfmt 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 specifically for the reset! function,
# to go back to the starting state
mutable struct DefaultSpec
typechar::Char
kwargs::Any
fspec::FormatSpec
end
DefaultSpec(c::AbstractChar) = DefaultSpec(Char(c), (), FormatSpec(c))
function DefaultSpec(c::AbstractChar, syms...; kwargs...)
# otherwise update the spec
if isempty(syms)
DefaultSpec(Char(c), kwargs, FormatSpec(c; kwargs...))
else
kw = _add_kwargs_from_symbols(kwargs, syms...)
DefaultSpec(Char(c), tuple(kw...), FormatSpec(c; kw...))
end
end
const DEFAULT_FORMATTERS = Dict{Type{<:Any}, DefaultSpec}()
# adds a new default formatter for this type
default_spec!(::Type{T}, c::AbstractChar) where {T} =
(DEFAULT_FORMATTERS[T] = DefaultSpec(c); nothing)
# note: types T and K will now both share K's default
default_spec!(::Type{T}, ::Type{K}) where {T,K} =
(DEFAULT_FORMATTERS[T] = DEFAULT_FORMATTERS[K]; nothing)
default_spec!(::Type{T}, c::AbstractChar, syms...; kwargs...) where {T} =
(DEFAULT_FORMATTERS[T] = DefaultSpec(c, syms...; kwargs...); nothing)
function reset!(::Type{T}) where {T}
dspec = default_spec(T)
dspec.fspec = FormatSpec(dspec.typechar; dspec.kwargs...)
nothing
end
# --------------------------------------------------------------------------------------------------
function _add_kwargs_from_symbols(kwargs, syms::Symbol...)
d = Dict{Symbol, Any}(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. AbstractFloat)
# you'll need to extend this method like here:
const ComplexInteger = Complex{<:Integer}
const ComplexFloat = Complex{<:AbstractFloat}
const ComplexRational = Complex{<: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(::ComplexInteger) = DEFAULT_FORMATTERS[ComplexInteger]
default_spec(::ComplexFloat) = DEFAULT_FORMATTERS[ComplexFloat]
default_spec(::ComplexRational) = DEFAULT_FORMATTERS[ComplexRational]
default_spec(::Type{T}) where {T} =
get(DEFAULT_FORMATTERS, T) do
error("Missing default spec for type $T... call default!(T, c): $DEFAULT_FORMATTERS")
end
default_spec(x) = default_spec(typeof(x))
fmt_default(::Type{T}) where {T} = default_spec(T).fspec
fmt_default(x) = default_spec(x).fspec
# first resets the fmt_default spec to the given arg,
# then continue by updating with args and kwargs
fmt_default!(::Type{T}, c::AbstractChar, args...; kwargs...) where {T} =
(default_spec!(T,c); fmt_default!(T, args...; kwargs...))
fmt_default!(::Type{T}, ::Type{K}, args...; kwargs...) where {T,K} =
(default_spec!(T,K); fmt_default!(T, args...; kwargs...))
# update the fmt_default for a specific type
function fmt_default!(::Type{T}, syms::Symbol...; kwargs...) where {T}
if isempty(syms)
# if there are no arguments, reset to initial defaults
if isempty(kwargs)
reset!(T)
return
end
# otherwise update the spec
dspec = default_spec(T)
dspec.fspec = FormatSpec(dspec.fspec; kwargs...)
else
d = _add_kwargs_from_symbols(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 = _add_kwargs_from_symbols(kwargs, syms...)
fmt_default!(; d...)
end
nothing
end
# --------------------------------------------------------------------------------------------------
# TODO: get rid of this entire hack by moving commas into pyfmt
function _optional_commas(x::Real, s::AbstractString, fspec::FormatSpec)
prevwidth = length(s)
dpos = findfirst( isequal('.'), s)
s = addcommas(s, prevwidth, dpos === nothing ? prevwidth : dpos - 1)
# 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
# left or right alignment ('<' is left)
s = fspec.align == '<' ? rpad(strip(s), fspec.width) : lpad(strip(s), fspec.width)
end
s
end
_optional_commas(x, s::AbstractString, fspec::FormatSpec) = s
# --------------------------------------------------------------------------------------------------
"""
Creates a new FormatSpec by overriding the defaults and passes it to pyfmt
Optionally width and precision can be passed positionally, after the value to be formatted.
Some keyword arguments can be passed simply as symbols:
```
Symbol | Meaning
------------------|------------------------------------------
:ljust or :left | Left justified, same as < for FormatSpec
:rjust or :right | Right 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
```
Also, keyword arguments can be given:
```
Keyword | Type | Meaning | Default
--------|------|---------------------------|-------
fill | Char | Fill character | ' '
align | Char | Alignment character | '\\0'
sign | Char | Sign character | '-'
width | Int | Field width | -1, i.e. ignored
prec | Int | Floating Precision | -1, i.e. ignored
ipre | Bool | Use 0b, 0o, or 0x prefix? | false
zpad | Bool | Pad with 0s on left | false
tsep | Bool | Use thousands separator? | false
```
"""
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.
# normally it will be much faster to change the fmt_default formatting as needed
function fmt(x; kwargs...)
fspec = fmt_default(x)
isempty(kwargs) || (fspec = FormatSpec(fspec; kwargs...))
s = pyfmt(fspec, x)
# add the commas now... I was confused as to when this is done currently
fspec.tsep ? _optional_commas(x, s, fspec) : s
end
# some helper method calls, which just convert to kwargs
fmt(x, width::Int, args...; kwargs...) = fmt(x, args...; width=width, kwargs...)
fmt(x, width::Int, prec::Int, args...; kwargs...) =
fmt(x, args...; width=width, prec=prec, kwargs...)
# integrate some symbol shorthands into the keyword args
# note: as above, this will generate relevant kwargs, so to format in a tight loop,
# you should probably update the fmt_default
function fmt(x, syms::Symbol...; kwargs...)
d = _add_kwargs_from_symbols(kwargs, syms...)
fmt(x; d...)
end
# --------------------------------------------------------------------------------------------------
# seed it with some basic default formatters
for (t, c) in [(Integer,'d'),
(AbstractFloat,'f'),
(AbstractChar,'c'),
(AbstractString,'s'),
(ComplexInteger,'d'),
(ComplexFloat,'f')]
default_spec!(t, c)
end
default_spec!(Rational, 's', :right)
default_spec!(AbstractIrrational, 's', :right)
default_spec!(ComplexRational, 's', :right)
default_spec!(Number, 's', :right)