Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

adding dynamic width and precision to printf #40105

Merged
merged 7 commits into from
Feb 2, 2023
Merged
Show file tree
Hide file tree
Changes from 4 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
2 changes: 1 addition & 1 deletion NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ Standard library changes


#### Printf

* Format specifiers now support dynamic width and precision, e.g. `%*s` and `%*.*g` ([#40105]).

#### Profile

Expand Down
131 changes: 111 additions & 20 deletions stdlib/Printf/src/Printf.jl
Original file line number Diff line number Diff line change
Expand Up @@ -34,19 +34,29 @@ struct Spec{T} # T => %type => Val{'type'}
hash::Bool
width::Int
precision::Int
dynamic_width::Bool
dynamic_precision::Bool
end

# recreate the format specifier string from a typed Spec
Base.string(f::Spec{T}; modifier::String="") where {T} =
string("%", f.leftalign ? "-" : "", f.plus ? "+" : "", f.space ? " " : "",
f.zero ? "0" : "", f.hash ? "#" : "", f.width > 0 ? f.width : "",
f.precision == 0 ? ".0" : f.precision > 0 ? ".$(f.precision)" : "", modifier, char(T))
string("%",
f.leftalign ? "-" : "",
f.plus ? "+" : "",
f.space ? " " : "",
f.zero ? "0" : "",
f.hash ? "#" : "",
f.dynamic_width ? "*" : (f.width > 0 ? f.width : ""),
f.dynamic_precision ? ".*" : (f.precision == 0 ? ".0" : (f.precision > 0 ? ".$(f.precision)" : "")),
modifier,
char(T))

Base.show(io::IO, f::Spec) = print(io, string(f))

floatfmt(s::Spec{T}) where {T} =
Spec{Val{'f'}}(s.leftalign, s.plus, s.space, s.zero, s.hash, s.width, 0)
Spec{Val{'f'}}(s.leftalign, s.plus, s.space, s.zero, s.hash, s.width, 0, s.dynamic_width, s.dynamic_precision)
ptrfmt(s::Spec{T}, x) where {T} =
Spec{Val{'x'}}(s.leftalign, s.plus, s.space, s.zero, true, s.width, sizeof(x) == 8 ? 16 : 8)
Spec{Val{'x'}}(s.leftalign, s.plus, s.space, s.zero, true, s.width, sizeof(x) == 8 ? 16 : 8, s.dynamic_width, s.dynamic_precision)

"""
Printf.Format(format_str)
Expand Down Expand Up @@ -75,6 +85,7 @@ struct Format{S, T}
# and so on, then at the end, str[substringranges[end]]
substringranges::Vector{UnitRange{Int}}
formats::T # Tuple of Specs
numarguments::Int # required for dynamic format specifiers
end

# what number base should be used for a given format specifier?
Expand Down Expand Up @@ -115,6 +126,8 @@ function Format(f::AbstractString)
bytes = codeunits(f)
len = length(bytes)
pos = 1
numarguments = 0

b = 0x00
local last_percent_pos

Expand Down Expand Up @@ -165,26 +178,43 @@ function Format(f::AbstractString)
end
# parse width
width = 0
while b - UInt8('0') < 0x0a
width = 10 * width + (b - UInt8('0'))
dynamic_width = false
if b == UInt8('*')
dynamic_width = true
numarguments += 1
b = bytes[pos]
pos += 1
pos > len && break
else
while b - UInt8('0') < 0x0a
width = 10 * width + (b - UInt8('0'))
b = bytes[pos]
pos += 1
pos > len && break
end
end
# parse precision
precision = 0
parsedprecdigits = false
dynamic_precision = false
if b == UInt8('.')
pos > len && throw(InvalidFormatStringError("Precision specifier is missing precision", f, last_percent_pos, pos-1))
parsedprecdigits = true
b = bytes[pos]
pos += 1
if pos <= len
while b - UInt8('0') < 0x0a
precision = 10precision + (b - UInt8('0'))
if b == UInt8('*')
dynamic_precision = true
numarguments += 1
b = bytes[pos]
pos += 1
pos > len && break
else
precision = 0
while b - UInt8('0') < 0x0a
precision = 10precision + (b - UInt8('0'))
b = bytes[pos]
pos += 1
pos > len && break
end
end
end
end
Expand All @@ -208,6 +238,8 @@ function Format(f::AbstractString)
!(b in b"diouxXDOUeEfFgGaAcCsSpn") && throw(InvalidFormatStringError("'$(Char(b))' is not a valid type specifier", f, last_percent_pos, pos-1))
type = Val{Char(b)}
if type <: Ints && precision > 0
# note - we should also set zero to false if dynamic precison > 0
# this is taken care of in fmt() for Ints
zero = false
elseif (type <: Strings || type <: Chars) && !parsedprecdigits
precision = -1
Expand All @@ -216,7 +248,8 @@ function Format(f::AbstractString)
elseif type <: Floats && !parsedprecdigits
precision = 6
end
push!(fmts, Spec{type}(leftalign, plus, space, zero, hash, width, precision))
numarguments += 1
push!(fmts, Spec{type}(leftalign, plus, space, zero, hash, width, precision, dynamic_width, dynamic_precision))
start = pos
while pos <= len
b = bytes[pos]
Expand All @@ -235,7 +268,7 @@ function Format(f::AbstractString)
end
push!(strs, start:pos - 1 - (b == UInt8('%')))
end
return Format(bytes, strs, Tuple(fmts))
return Format(bytes, strs, Tuple(fmts), numarguments)
end

macro format_str(str)
Expand All @@ -257,6 +290,46 @@ const HEX = b"0123456789ABCDEF"
return pos
end


@inline function rmdynamic(spec::Spec{T}, args, argp) where {T}
zero, width, precision = spec.zero, spec.width, spec.precision
if spec.dynamic_width
width = args[argp]
argp += 1
end
if spec.dynamic_precision
precision = args[argp]
if zero && T <: Ints && precision > 0
zero = false
end
argp += 1
end
(Spec{T}(spec.leftalign, spec.plus, spec.space, zero, spec.hash, width, precision, false, false), argp)
end


@inline function rmdynamic(spec::Spec{T}, args, argp) where {T}
zero, width, precision = spec.zero, spec.width, spec.precision
if spec.dynamic_width
width = args[argp]
argp += 1
end
if spec.dynamic_precision
precision = args[argp]
if zero && T <: Ints && precision > 0
zero = false
end
argp += 1
end
(Spec{T}(spec.leftalign, spec.plus, spec.space, zero, spec.hash, width, precision, false, false), argp)
end


@inline function fmt(buf, pos, args, argp, spec::Spec{T}) where {T}
stevengj marked this conversation as resolved.
Show resolved Hide resolved
spec, argp = rmdynamic(spec, args, argp)
(fmt(buf, pos, args[argp], spec), argp+1)
end

@inline function fmt(buf, pos, arg, spec::Spec{T}) where {T <: Chars}
leftalign, width = spec.leftalign, spec.width
c = Char(first(arg))
Expand Down Expand Up @@ -772,9 +845,10 @@ const UNROLL_UPTO = 16
# for each format, write out arg and next substring
# unroll up to 16 formats
N = length(f.formats)
argp = 1
Base.@nexprs 16 i -> begin
if N >= i
pos = fmt(buf, pos, args[i], f.formats[i])
pos, argp = fmt(buf, pos, args, argp, f.formats[i])
for j in f.substringranges[i + 1]
b = f.str[j]
if !escapechar
Expand All @@ -789,7 +863,7 @@ const UNROLL_UPTO = 16
end
if N > 16
for i = 17:length(f.formats)
pos = fmt(buf, pos, args[i], f.formats[i])
pos, argp = fmt(buf, pos, args, argp, f.formats[i])
for j in f.substringranges[i + 1]
b = f.str[j]
if !escapechar
Expand All @@ -805,11 +879,20 @@ const UNROLL_UPTO = 16
return pos
end


@inline function plength(f::Spec{T}, args, argp) where {T}
f, argp = rmdynamic(f, args, argp)
(plength(f, args[argp]), argp+1)
end



function plength(f::Spec{T}, x) where {T <: Chars}
stevengj marked this conversation as resolved.
Show resolved Hide resolved
c = Char(first(x))
w = textwidth(c)
return max(f.width, w) + (ncodeunits(c) - w)
end

plength(f::Spec{Pointer}, x) = max(f.width, 2 * sizeof(x) + 2)

function plength(f::Spec{T}, x) where {T <: Strings}
Expand Down Expand Up @@ -837,14 +920,17 @@ plength(::Spec{PositionCounter}, x) = 0
len = sum(length, substringranges)
N = length(formats)
# unroll up to 16 formats
argp = 1
Base.@nexprs 16 i -> begin
if N >= i
len += plength(formats[i], args[i])
l, argp = plength(formats[i], args, argp)
len += l
end
end
if N > 16
for i = 17:length(formats)
len += plength(formats[i], args[i])
l, argp = plength(formats[i], args, argp)
len += l
end
end
return len
Expand All @@ -864,15 +950,15 @@ for more details on C `printf` support.
function format end

function format(io::IO, f::Format, args...) # => Nothing
length(f.formats) == length(args) || argmismatch(length(f.formats), length(args))
f.numarguments == length(args) || argmismatch(f.numarguments, length(args))
buf = Base.StringVector(computelen(f.substringranges, f.formats, args))
pos = format(buf, 1, f, args...)
write(io, resize!(buf, pos - 1))
return
end

function format(f::Format, args...) # => String
length(f.formats) == length(args) || argmismatch(length(f.formats), length(args))
f.numarguments == length(args) || argmismatch(f.numarguments, length(args))
buf = Base.StringVector(computelen(f.substringranges, f.formats, args))
pos = format(buf, 1, f, args...)
return String(resize!(buf, pos - 1))
Expand Down Expand Up @@ -906,8 +992,10 @@ Padded with zeros to length 6 000123

julia> @printf "Use shorter of decimal or scientific %g %g" 1.23 12300000.0
Use shorter of decimal or scientific 1.23 1.23e+07
```

julia> @printf "Use dynamic width and precision %*.*f" 10 2 0.12345
Use dynamic width and precision 0.12
```
For a systematic specification of the format, see [here](https://www.cplusplus.com/reference/cstdio/printf/).
See also [`@sprintf`](@ref) to get the result as a `String` instead of it being printed.

Expand All @@ -931,6 +1019,9 @@ julia> @printf "%.0f %.1f %f" 0.5 0.025 -0.0078125
using [`textwidth`](@ref), which e.g. ignores zero-width characters
(such as combining characters for diacritical marks) and treats certain
"wide" characters (e.g. emoji) as width `2`.

!!! compat "Julia 1.10"
Dynamic width specifiers like `%*s` and `%0*.*f` require Julia 1.10.
"""
macro printf(io_or_fmt, args...)
if io_or_fmt isa String
Expand Down
Loading