Skip to content
Merged
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
4 changes: 2 additions & 2 deletions Project.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name = "JSON"
uuid = "682c06a0-de6a-54ab-a142-c8b1cf79cde6"
version = "1.7.0"
version = "1.7.1"

[deps]
Dates = "ade2ca70-3891-5945-98fb-dc099432e06a"
Expand All @@ -22,7 +22,7 @@ Arrow = "2.8.0"
ArrowTypes = "2.2"
Parsers = "1, 2"
PrecompileTools = "1"
StructUtils = "2.8.3"
StructUtils = "2.8.4"
julia = "1.9"

[extras]
Expand Down
94 changes: 94 additions & 0 deletions src/write.jl
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,103 @@ sizeguess(_) = 512

const StringLike = Union{Enum, AbstractChar, VersionNumber, Cstring, Cwstring, UUID, Dates.TimeType, Type, Logging.LogLevel}

# Date, DateTime, and Time serialize as exactly `string(x)`, but through
# fixed-format printers instead of the Dates format machinery: DateFormat
# token handling and its diagnostics are too dynamic for static compilation,
# so JSON output of temporal fields would otherwise fail `juliac --trim`
# verification.
@inline function _writetemporal!(buf::Vector{UInt8}, i::Int, value::Integer, width::Int)
for offset in (width - 1):-1:0
buf[i + offset] = UInt8('0') + UInt8(value % 10)
value = divrem(value, 10)[1]
end
return i + width
end

function _datestring!(buf::Vector{UInt8}, i::Int, x::Dates.Date)
y = Dates.year(x)
if y < 0
buf[i] = UInt8('-')
i += 1
y = -y
end
i = _writetemporal!(buf, i, y, max(4, ndigits(y)))
buf[i] = UInt8('-')
i = _writetemporal!(buf, i + 1, Dates.month(x), 2)
buf[i] = UInt8('-')
return _writetemporal!(buf, i + 1, Dates.day(x), 2)
end

function _lowerdate(x::Dates.Date)
y = Dates.year(x)
buf = Vector{UInt8}(undef, 6 + max(4, ndigits(abs(y))) + (y < 0 ? 1 : 0))
_datestring!(buf, 1, x)
return String(buf)
end

function _timestring!(buf::Vector{UInt8}, i::Int, x::Dates.Time)
i = _writetemporal!(buf, i, Dates.hour(x), 2)
buf[i] = UInt8(':')
i = _writetemporal!(buf, i + 1, Dates.minute(x), 2)
buf[i] = UInt8(':')
i = _writetemporal!(buf, i + 1, Dates.second(x), 2)
# `string(::Time)` prints the shortest subsecond field that loses
# nothing — milliseconds, microseconds, or nanoseconds — and then strips
# the fraction's trailing zeros.
ms, us, ns = Dates.millisecond(x), Dates.microsecond(x), Dates.nanosecond(x)
if ns != 0
fraction, width = ms * 1_000_000 + us * 1_000 + ns, 9
elseif us != 0
fraction, width = ms * 1_000 + us, 6
elseif ms != 0
fraction, width = ms, 3
else
return i
end
while fraction % 10 == 0
fraction = divrem(fraction, 10)[1]
width -= 1
end
buf[i] = UInt8('.')
return _writetemporal!(buf, i + 1, fraction, width)
end

function _lowertime(x::Dates.Time)
n = Dates.nanosecond(x) != 0 ? 18 :
Dates.microsecond(x) != 0 ? 15 :
Dates.millisecond(x) != 0 ? 12 : 8
buf = Vector{UInt8}(undef, n)
return String(resize!(buf, _timestring!(buf, 1, x) - 1))
end

function _lowerdatetime(x::Dates.DateTime)
d = Dates.Date(x)
y = Dates.year(d)
ms = Dates.millisecond(x)
n = 6 + max(4, ndigits(abs(y))) + (y < 0 ? 1 : 0) + 9 + (ms != 0 ? 4 : 0)
buf = Vector{UInt8}(undef, n)
i = _datestring!(buf, 1, d)
buf[i] = UInt8('T')
i = _writetemporal!(buf, i + 1, Dates.hour(x), 2)
buf[i] = UInt8(':')
i = _writetemporal!(buf, i + 1, Dates.minute(x), 2)
buf[i] = UInt8(':')
i = _writetemporal!(buf, i + 1, Dates.second(x), 2)
# `string(::DateTime)` omits the fraction entirely at zero milliseconds
# and always prints three digits otherwise.
if ms != 0
buf[i] = UInt8('.')
_writetemporal!(buf, i + 1, ms, 3)
end
return String(buf)
end

StructUtils.lower(::JSONStyle, ::Missing) = nothing
StructUtils.lower(::JSONStyle, x::Symbol) = String(x)
StructUtils.lower(::JSONStyle, x::StringLike) = string(x)
StructUtils.lower(::JSONStyle, x::Dates.Date) = _lowerdate(x)
StructUtils.lower(::JSONStyle, x::Dates.Time) = _lowertime(x)
StructUtils.lower(::JSONStyle, x::Dates.DateTime) = _lowerdatetime(x)
StructUtils.lower(::JSONStyle, x::Regex) = x.pattern
StructUtils.lower(::JSONStyle, x::Complex) = (re=real(x), im=imag(x))
StructUtils.lower(::JSONStyle, x::AbstractArray{<:Any,0}) = x[1]
Expand Down
29 changes: 28 additions & 1 deletion test/json_trim_public_entrypoints.jl
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using JSON
using Dates, JSON

const ARRAY_JSON = "[1,2,3]"
const STRING_JSON = "\"Ada\""
Expand Down Expand Up @@ -31,6 +31,12 @@ JSON.StructUtils.@noarg mutable struct TrimMutable
value::Int = 0
end

struct TrimTemporal
day::Dates.Date
stamp::Dates.DateTime
tick::Dates.Time
end

function checked(cond::Bool, msg::String)::Nothing
cond || error(msg)
return nothing
Expand Down Expand Up @@ -110,6 +116,27 @@ function exercise_write_entrypoints()::Nothing
checked(JSON.json(JSON.JSONText("{\"raw\":true}")) == "{\"raw\":true}", "JSONText write failed")
checked(JSON.json(JSON.Null()) == "null", "JSON.Null write failed")
checked(JSON.json(TrimCode("beta")) == "\"beta\"", "custom lower write failed")

# Date, DateTime, and Time round-trip through their canonical ISO text.
temporal = TrimTemporal(
Dates.Date(2026, 8, 7),
Dates.DateTime(2026, 8, 7, 15, 0, 0, 76),
Dates.Time(12, 30, 15, 250),
)
temporal_json = JSON.json(temporal)
checked(
temporal_json ==
"{\"day\":\"2026-08-07\",\"stamp\":\"2026-08-07T15:00:00.076\",\"tick\":\"12:30:15.25\"}",
"temporal write failed",
)
checked(
JSON.parse(temporal_json, TrimTemporal) == temporal,
"temporal read failed",
)
checked(
JSON.json(Dates.DateTime(2026, 1, 1)) == "\"2026-01-01T00:00:00\"",
"zero-millisecond DateTime write failed",
)
return nothing
end

Expand Down
4 changes: 4 additions & 0 deletions test/trim/Project.toml
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
[deps]
JuliaC = "acedd4c2-ced6-4a15-accc-2607eb759ba2"
Parsers = "69de0a69-1ddd-5017-9359-2bf0b02dc9f0"
StructUtils = "ec057cc2-7a8d-4b58-b3b3-92acb9f63b42"

[compat]
StructUtils = "2.8.4"
Loading