diff --git a/Project.toml b/Project.toml index 6117506..6f51dc7 100644 --- a/Project.toml +++ b/Project.toml @@ -1,6 +1,6 @@ name = "GeneralUtils" uuid = "c6c72f09-b708-4ac8-ac7c-2084d70108fe" -version = "0.4.5" +version = "0.4.6" authors = ["tonaerospace "] [deps] diff --git a/src/interface.jl b/src/interface.jl index 54359ff..260a995 100644 --- a/src/interface.jl +++ b/src/interface.jl @@ -204,33 +204,43 @@ end """ Recursively convert dictionary-like variable (e.g. JSON.Object) into a dictionary. + +Two methods are available: + +**Method 1** (simple): Converts `AbstractDict` to `Dict` and `AbstractArray` to `Vector{Any}`. + +**Method 2** (advanced): Also accepts a `sort_order` keyword argument to control key ordering +in the output `OrderedDict`. When `sort_order` is specified, the function returns an `OrderedDict` +with keys arranged according to the provided order, followed by any remaining keys. + The function walks any nested structure composed of `AbstractDict` (e.g., `JSON.Object`, `Dict`, `OrderedDict`) and `AbstractArray` and produces a new tree where -every dictionary-like node is a plain `Dict` and every array-like node is a -`Vector{Any}`. Scalar values (numbers, strings, booleans, `nothing`, etc.) -are returned unchanged. +every dictionary-like node is a plain `Dict` (or `OrderedDict` when `sort_order` is used) +and every array-like node is a `Vector{Any}`. Scalar values (numbers, strings, booleans, +`nothing`, etc.) are returned unchanged. Does **not** mutate the input; it always allocates new containers. # Arguments - `x` - Any Julia value. If `x` is an `AbstractDict` it will be converted to a `Dict`; + Any Julia value. If `x` is an `AbstractDict` it will be converted to a `Dict` (or `OrderedDict`); if it is an `AbstractArray` its elements will be processed recursively. # Keyword Arguments - `keytype::Type=Any` The key type for the output Dict. Use `String` for `Dict{String,Any}`, `Symbol` for `Dict{Symbol,Any}`, or `Any` to preserve original key types. -- `stringkey::Bool=false` - If `true`, every dictionary key is converted to `String` via `string(k)`. This parameter is ignored when `keytype` is explicitly set. +- `sort_order::Union{Nothing, Vector}=nothing` (Method 2 only) + Vector of keys specifying the desired order. When provided, output uses `OrderedDict` with + keys arranged in the specified order first, then any remaining keys appended. # Return -- A newly allocated nested structure composed of `Dict{keytype,Any}` and - `Vector{Any}` that mirrors the input shape but uses plain Julia containers. +- A newly allocated nested structure composed of `Dict{keytype,Any}` (or `OrderedDict{keytype,Any}` + when `sort_order` is specified) and `Vector{Any}` that mirrors the input shape but uses plain Julia containers. # Notes - The function treats any `AbstractDict` as a mapping source, so it works with `JSON.Object`, `Dict`, `OrderedDict`, etc. -- Arrays are returned as `Vector{Any}` with their elements processed - recursively. +- Arrays are returned as `Vector{Any}` with their elements processed recursively. +- When `sort_order` is provided, the function uses `OrderedDict` to preserve key ordering. # Examples ```jldoctest @@ -249,7 +259,7 @@ julia> d = Dict( ) ) -julia jsonstring = JSON.json(d) +julia> jsonstring = JSON.json(d) julia> A1 = JSON.parse(jsonstring) # A1 type is JSON.Object julia> A2 = dictify(A1; keytype=String) Dict{String,Any} with 3 entries: @@ -265,6 +275,17 @@ Dict{Symbol,Any} with 3 entries: julia> B1 = dictify(d; keytype=String) Dict{String, Any} with 3 entries: +``` + +**With sort_order (returns OrderedDict):** +```jldoctest +julia> d = Dict("a"=>1, "b"=>2, "c"=>3) +julia> dictify(d; sort_order=["c", "a"]) +OrderedDict{String,Int} with 3 entries: + "c" => 3 + "a" => 1 + "b" => 2 +``` """ function dictify(x; keytype::Type=Any) # Dict-like objects @@ -291,6 +312,68 @@ function dictify(x; keytype::Type=Any) end end +function dictify(x; keytype::Type=Any, sort_order::Union{Nothing, Vector}=nothing) + # Dict-like objects + if x isa AbstractDict + out = OrderedDict{keytype, Any}() + + # 1. Process and normalize all keys from the input dictionary + processed_dict = OrderedDict{keytype, Any}() + for (k, v) in x + if keytype === String + newk = string(k) + elseif keytype === Symbol + newk = Symbol(string(k)) + else + newk = k + end + processed_dict[newk] = dictify(v; keytype=keytype, sort_order=sort_order) + end + + # 2. If a sort order is specified, apply it + if !isnothing(sort_order) + # Normalize the sort_order elements to match the requested keytype + normalized_order = map(sort_order) do tk + if keytype === String + return string(tk) + elseif keytype === Symbol + return Symbol(string(tk)) + else + return tk + end + end + + # First, insert keys that match the requested order + for target_key in normalized_order + if haskey(processed_dict, target_key) + out[target_key] = processed_dict[target_key] + end + end + + # Then, append any remaining keys that weren't in the sort_order + for (k, v) in processed_dict + if !haskey(out, k) + out[k] = v + end + end + else + # If no sort order is given, just use the processed dict + out = processed_dict + end + + return out + + # Arrays / vectors: map elements recursively + elseif x isa AbstractArray + return [dictify(element; keytype=keytype, sort_order=sort_order) for element in x] + + # Everything else: return as-is + else + return x + end +end + + # ---------------------------------------------- 100 --------------------------------------------- # """ Recursively convert dictionary-like variable (e.g. JSON.Object) into a dictionary. @@ -1679,7 +1762,7 @@ function dict_to_string_html(d::AbstractDict; indent_level=1, indent_str=" ") padding = indent_str ^ indent_level # Sort keys for predictable, clean output - for k in sort(collect(keys(d))) + for k in keys(d) v = d[k] if v isa AbstractDict @@ -1744,6 +1827,11 @@ end + + + + +