diff --git a/src/Storage/Storage.jl b/src/Storage/Storage.jl index 46c819a..0d17bb1 100644 --- a/src/Storage/Storage.jl +++ b/src/Storage/Storage.jl @@ -197,6 +197,7 @@ isemptysub(s::AbstractStore, p) = isempty(subkeys(s,p)) && isempty(subdirs(s,p)) #during auto-check of storage format when doing zopen storageregexlist = Pair[] +include("formattedstore.jl") include("directorystore.jl") include("dictstore.jl") include("s3store.jl") diff --git a/src/Storage/formattedstore.jl b/src/Storage/formattedstore.jl new file mode 100644 index 0000000..750ec6e --- /dev/null +++ b/src/Storage/formattedstore.jl @@ -0,0 +1,230 @@ +# Default Zarr version +const DV = 2 + +# Default Zarr separator + +# Default Zarr v2 separator +const DS2 = '.' +# Default Zarr v3 separator +const DS3 = '/' + +default_sep(version) = version == 2 ? DS2 : + version == 3 ? DS3 : + error("Unknown version: $version") +const DS = default_sep(DV) + +# Chunk Key Encodings for Zarr v3 +# A Char is the separator for the default chunk key encoding +abstract type ChunkKeyEncoding end +struct V2ChunkKeyEncoding{SEP} <: ChunkKeyEncoding end +separator(c::Char) = c +separator(v2cke::V2ChunkKeyEncoding{SEP}) where SEP = SEP + +""" + FormattedStore{V,CKE,STORE <: AbstractStore} <: AbstractStore + +FormattedStore wraps an AbstractStore to indicate a specific Zarr format. +The path of a chunk depends on the version and chunk key encoding. + +# Type Parameters + +- V: Zarr format version +- CKE: Chunk key encoding or dimension separator. + CKE could be a `Char` or a subtype of `ChunkKeyEncoding`. +- STORE: Type of AbstractStore wrapped + +# Chunk Path Formats + +## Zarr version 2 + +### '.' dimension separator (default) + +Chunks are encoded as "1.2.3" + +### '/' dimension separator + +Chunks are encoded as "1/2/3" + +## Zarr version 3 + +### '/' dimension separator (default) + +Chunks are encoded as "c/1/2/3" + +### '.' dimension separator + +Chunks are encoded as "c.1.2.3" + +### V2ChunkKeyEncoding{SEP} + +See Zarr version 2 +""" +struct FormattedStore{V,SEP,STORE <: AbstractStore} <: AbstractStore + parent::STORE +end +FormattedStore(args...) = FormattedStore{DV,DS}(args...) +FormattedStore(s::FormattedStore) = s +FormattedStore{V}(args...) where V = FormattedStore{V, default_sep(V)}(args...) +FormattedStore{V}(s::FormattedStore{<:Any,S}) where {V,S} = FormattedStore{V, S}(s) +FormattedStore{<: Any, S}(args...) where S = FormattedStore{DV, S}(args...) +FormattedStore{<: Any, S}(s::FormattedStore{V}) where {V,S} = FormattedStore{V, S}(s) +function FormattedStore{V,S}(store::AbstractStore) where {V,S} + return FormattedStore{V,S,typeof(store)}(store) +end +function FormattedStore{V,S}(store::FormattedStore) where {V,S} + p = parent(store) + return FormattedStore{V,S,typeof(p)}(p) +end + +Base.parent(store::FormattedStore) = store.parent + +@inline citostring(i::CartesianIndex, version::Int, sep::Char=default_sep(version)) = (version == 3 ? "c$sep" : "" ) * join(reverse((i - oneunit(i)).I), sep) +@inline citostring(::CartesianIndex{0}, version::Int, sep::Char=default_sep(version)) = (version == 3 ? "c$(sep)0" : "0" ) +@inline citostring(i::CartesianIndex, ::Int, ::Type{V2ChunkKeyEncoding{S}}) where S = citostring(i, 2, S) +citostring(i::CartesianIndex, s::FormattedStore{V, S}) where {V,S} = citostring(i, V, S) + +Base.getindex(s::FormattedStore, p, i::CartesianIndex) = s[p, citostring(i,s)] +Base.delete!(s::FormattedStore, p, i::CartesianIndex) = delete!(s, p, citostring(i,s)) +Base.setindex!(s::FormattedStore, v, p, i::CartesianIndex) = s[p, citostring(i,s)]=v + +isinitialized(s::FormattedStore, p, i::CartesianIndex) = isinitialized(s,p,citostring(i, s)) + +""" +- [`storagesize(d::AbstractStore, p::AbstractString)`](@ref storagesize) +- [`subdirs(d::AbstractStore, p::AbstractString)`](@ref subdirs) +- [`subkeys(d::AbstractStore, p::AbstractString)`](@ref subkeys) +- [`isinitialized(d::AbstractStore, p::AbstractString)`](@ref isinitialized) +- [`storefromstring(::Type{<: AbstractStore}, s, _)`](@ref storefromstring) +- `Base.getindex(d::AbstractStore, i::AbstractString)`: return the data stored in key `i` as a Vector{UInt8} +- `Base.setindex!(d::AbstractStore, v, i::AbstractString)`: write the values in `v` to the key `i` of the given store `d` +""" + +storagesize(d::FormattedStore, p::AbstractString) = storagesize(parent(d), p) +subdirs(d::FormattedStore, p::AbstractString) = subdirs(parent(d), p) +subkeys(d::FormattedStore, p::AbstractString) = subkeys(parent(d), p) +isinitialized(d::FormattedStore, p::AbstractString) = isinitialized(parent(d), p) +storefromstring(::Type{FormattedStore{<: Any, <: Any, STORE}}, s, _) where STORE = FormattedStore{DV,DS}(storefromstring(STORE, s)) +storefromstring(::Type{FormattedStore{V,S}}, s, _) where {V,S} = FormattedStore{DV,DS}(storefromstring(s)) +storefromstring(::Type{FormattedStore{V,S,STORE}}, s, _) where {V,S,STORE} = FormattedStore{V,S,STORE}(storefromstring(STORE, s)) +Base.getindex(d::FormattedStore, i::AbstractString) = getindex(parent(d), i) +Base.setindex!(d::FormattedStore, v, i::AbstractString) = setindex!(parent(d), v, i) +Base.delete!(d::FormattedStore, i::AbstractString) = delete!(parent(d), i) + + +function Base.getproperty(store::FormattedStore{V,S}, sym::Symbol) where {V,S} + if sym == :dimension_separator + return S + elseif sym == :zarr_format + return V + elseif sym ∈ propertynames(getfield(store, :parent)) + # Support forwarding of properties to parent + return getproperty(store.parent, sym) + else + getfield(store, sym) + end +end +function Base.propertynames(store::FormattedStore) + return (:dimension_separator, :zarr_format, fieldnames(typeof(store))..., propertynames(store.parent)...) +end + + +""" + Zarr.set_dimension_separator(store::FormattedStore{V}, sep::Char)::FormattedStore{V,sep} + +Returns a FormattedStore of the same type with the same `zarr_format` parameter, `V`, +but with a dimension separator of `sep`. Note that this does not mutate the original store. + +# Examples + +``` +julia> Zarr.set_dimension_separator(Zarr.FormattedStore{2, '.'}(Zarr.DictStore(), '/')) |> typeof +Zarr.FormattedStore{2, '/',Zarr.DictStore} +``` + +""" +function set_dimension_separator(store::FormattedStore{V}, sep::Char) where V + return FormattedStore{V,sep}(store) +end +function set_dimension_separator(store::AbstractStore, sep::Char) + return FormattedStore{<: Any,sep}(store) +end + +""" + set_zarr_format(::FormattedStore{<: Any, S}, zarr_format::Int)::FormattedStore{zarr_format,S} + +Returns a FormattedStore of the same type with the same `dimension_separator` parameter, `S`, +but with the specified `zarr_format` parameter. Note that this does not mutate the original store. + +# Examples + +``` +julia> Zarr.set_zarr_format(Zarr.FormattedStore{2, '.'}(Zarr.DictStore(), 3)) |> typeof +Zarr.FormattedStore{3, '.', DictStore} +``` + +""" +function set_zarr_format(store::FormattedStore{<: Any, S}, zarr_format::Int) where S + return FormattedStore{zarr_format,S}(store) +end +function set_zarr_format(store::AbstractStore, zarr_format::Int) + return FormattedStore{zarr_format}(store) +end + +dimension_separator(::AbstractStore) = DS +dimension_separator(::FormattedStore{<: Any,S}) where S = S +zarr_format(::AbstractStore) = DV +zarr_format(::FormattedStore{V}) where V = V + +is_zgroup(s::FormattedStore{3}, p, metadata=getmetadata(s, p, false)) = + isinitialized(s,_concatpath(p,"zarr.json")) && + metadata.node_type == "group" +is_zarray(s::FormattedStore{3}, p, metadata=getmetadata(s, p, false)) = + isinitialized(s,_concatpath(p,"zarr.json")) && + metadata.node_type == "array" + +getmetadata(s::FormattedStore{3}, p,fill_as_missing) = Metadata(String(maybecopy(s[p,"zarr.json"])),fill_as_missing) +function writemetadata(s::FormattedStore{3}, p, m::Metadata; indent_json::Bool= false) + met = IOBuffer() + + if indent_json + JSON.print(met,m,4) + else + JSON.print(met,m) + end + + s[p,"zarr.json"] = take!(met) + m +end + +function getattrs(s::FormattedStore{3}) + md = s[p,"zarr.json"] + if md === nothing + error("zarr.json not found") + else + md = JSON.parse(replace(String(maybecopy(md)),": NaN,"=>": \"NaN\",")) + return get(md, "attributes", Dict{String, Any}()) + end +end + +function writeattrs(s::FormattedStore{3}, p, att::Dict; indent_json::Bool= false) + # This is messy, we need to open zarr.json and replace the attributes section + md = s[p,"zarr.json"] + if md === nothing + error("zarr.json not found") + else + md = JSON.parse(replace(String(maybecopy(md)),": NaN,"=>": \"NaN\",")) + end + md = Dict(md) + md["attributes"] = att + + b = IOBuffer() + + if indent_json + JSON.print(b,md,4) + else + JSON.print(b,md) + end + + s[p,"zarr.json"] = take!(b) + att +end diff --git a/src/Storage/http.jl b/src/Storage/http.jl index 9b68cb1..980284f 100644 --- a/src/Storage/http.jl +++ b/src/Storage/http.jl @@ -13,8 +13,8 @@ python package. In case you experience performance issues, one can try to use struct HTTPStore <: AbstractStore url::String allowed_codes::Set{Int} + HTTPStore(url, allowed_codes = Set((404,))) = new(url, allowed_codes) end -HTTPStore(url) = HTTPStore(url,Set((404,))) function Base.getindex(s::HTTPStore, k::String) r = HTTP.request("GET",string(s.url,"/",k),status_exception = false,socket_type_tls=OpenSSL.SSLStream) @@ -39,7 +39,21 @@ end push!(storageregexlist,r"^https://"=>HTTPStore) push!(storageregexlist,r"^http://"=>HTTPStore) -storefromstring(::Type{<:HTTPStore}, s,_) = ConsolidatedStore(HTTPStore(s),""),"" +function storefromstring(::Type{<:HTTPStore}, s,_) + http_store = HTTPStore(s) + try + if http_store["", ".zmetadata"] !== nothing + http_store = ConsolidatedStore(http_store,"") + end + if is_zarray(http_store, "") + meta = getmetadata(http_store, "", false) + http_store = FormattedStore{meta.zarr_format, meta.dimension_separator}(http_store) + end + catch err + @warn exception=err "Additional metadata was not available for HTTPStore." + end + return http_store,"" +end """ missing_chunk_return_code!(s::HTTPStore, code::Union{Int,AbstractVector{Int}}) diff --git a/src/ZArray.jl b/src/ZArray.jl index b095568..951639f 100644 --- a/src/ZArray.jl +++ b/src/ZArray.jl @@ -311,16 +311,24 @@ Creates a new empty zarr array with element type `T` and array dimensions `dims` * `attrs=Dict()` a dict containing key-value pairs with metadata attributes associated to the array * `writeable=true` determines if the array is opened in read-only or write mode * `indent_json=false` determines if indents are added to format the json files `.zarray` and `.zattrs`. This makes them more readable, but increases file size. +* `dimension_separator='.'` sets how chunks are encoded. The Zarr v2 default is '.' such that the first 3D chunk would be `0.0.0`. The Zarr v3 default is `/`. """ function zcreate(::Type{T}, dims::Integer...; name="", path=nothing, + dimension_separator='.', kwargs... ) where T + + if dimension_separator isa AbstractString + # Convert AbstractString to Char + dimension_separator = only(dimension_separator) + end + if path===nothing - store = DictStore() + store = FormattedStore{DV, dimension_separator}(DictStore()) else - store = DirectoryStore(joinpath(path,name)) + store = FormattedStore{DV, dimension_separator}(DirectoryStore(joinpath(path,name))) end zcreate(T, store, dims...; kwargs...) end @@ -335,14 +343,22 @@ function zcreate(::Type{T},storage::AbstractStore, filters = filterfromtype(T), attrs=Dict(), writeable=true, - indent_json=false - ) where T + indent_json=false, + dimension_separator=nothing + ) where {T} + + if isnothing(dimension_separator) + dimension_separator = Zarr.dimension_separator(storage) + elseif dimension_separator != Zarr.dimension_separator(storage) + error("The dimension separator keyword value, $dimension_separator, + must agree with the dimension separator type parameter, $(Zarr.dimension_separator(storage))") + end length(dims) == length(chunks) || throw(DimensionMismatch("Dims must have the same length as chunks")) N = length(dims) C = typeof(compressor) T2 = (fill_value === nothing || !fill_as_missing) ? T : Union{T,Missing} - metadata = Metadata{T2, N, C, typeof(filters)}( + metadata = Metadata{T2, N, C, typeof(filters), dimension_separator}( 2, dims, chunks, diff --git a/src/ZGroup.jl b/src/ZGroup.jl index 35515ed..0164096 100644 --- a/src/ZGroup.jl +++ b/src/ZGroup.jl @@ -20,10 +20,16 @@ function ZGroup(s::T,mode="r",path="";fill_as_missing=false) where T <: Abstract for d in subdirs(s,path) dshort = split(d,'/')[end] - m = zopen_noerr(s,mode,path=_concatpath(path,dshort),fill_as_missing=fill_as_missing) - if isa(m, ZArray) + subpath = _concatpath(path,dshort) + if is_zarray(s, subpath) + meta = getmetadata(s, subpath, false) + if dimension_separator(s) != meta.dimension_separator + s = set_dimension_separator(s, meta.dimension_separator) + end + m = zopen_noerr(s,mode,path=_concatpath(path,dshort),fill_as_missing=fill_as_missing) arrays[dshort] = m - elseif isa(m, ZGroup) + elseif is_zgroup(s, subpath) + m = zopen_noerr(s,mode,path=_concatpath(path,dshort),fill_as_missing=fill_as_missing) groups[dshort] = m end end @@ -39,7 +45,7 @@ Works like `zopen` with the single difference that no error is thrown when the path or store does not point to a valid zarr array or group, but nothing is returned instead. """ -function zopen_noerr(s::AbstractStore, mode="r"; +function zopen_noerr(s::AbstractStore, mode="r"; consolidated = false, path="", lru = 0, @@ -116,8 +122,18 @@ function storefromstring(s, create=true) return storefromstring(t,s,create) end end - if create || isdir(s) - return DirectoryStore(s), "" + if create + return FormattedStore(DirectoryStore(s)), "" + elseif isdir(s) + # parse metadata to determine store kind + temp_store = DirectoryStore(s) + if is_zarray(temp_store, "") + meta = getmetadata(temp_store, "", false) + store = FormattedStore{meta.zarr_format, meta.dimension_separator}(temp_store) + else + store = FormattedStore(temp_store) + end + return store, "" else throw(ArgumentError("Path $s is not a directory.")) end diff --git a/src/metadata.jl b/src/metadata.jl index d80e7c1..fa564e3 100644 --- a/src/metadata.jl +++ b/src/metadata.jl @@ -17,7 +17,7 @@ using .MaxLengthStrings: MaxLengthString primitive type ASCIIChar <: AbstractChar 8 end ASCIIChar(x::UInt8) = reinterpret(ASCIIChar, x) ASCIIChar(x::Integer) = ASCIIChar(UInt8(x)) -UInt8(x::ASCIIChar) = reinterpret(UInt8, x) +Base.UInt8(x::ASCIIChar) = reinterpret(UInt8, x) Base.codepoint(x::ASCIIChar) = UInt8(x) Base.show(io::IO, x::ASCIIChar) = print(io, Char(x)) Base.zero(::Union{ASCIIChar,Type{ASCIIChar}}) = ASCIIChar(Base.zero(UInt8)) @@ -91,9 +91,18 @@ Each array requires essential configuration metadata to be stored, enabling corr interpretation of the stored data. This metadata is encoded using JSON and stored as the value of the “.zarray” key within an array store. +# Type Parameters +* T - element type of the array +* N - dimensionality of the array +* C - compressor +* F - filters +* S - dimension separator + +# See Also + https://zarr.readthedocs.io/en/stable/spec/v2.html#metadata """ -struct Metadata{T, N, C, F} +struct Metadata{T, N, C, F, S} zarr_format::Int shape::Base.RefValue{NTuple{N, Int}} chunks::NTuple{N, Int} @@ -102,17 +111,49 @@ struct Metadata{T, N, C, F} fill_value::Union{T, Nothing} order::Char filters::F # not yet supported - function Metadata{T2, N, C, F}(zarr_format, shape, chunks, dtype, compressor,fill_value, order, filters) where {T2,N,C,F} + function Metadata{T2, N, C, F, S}(zarr_format, shape, chunks, dtype, compressor, fill_value, order, filters) where {T2,N,C,F,S} #We currently only support version zarr_format == 2 || throw(ArgumentError("Zarr.jl currently only support v2 of the protocol")) #Do some sanity checks to make sure we have a sane array any(<(0), shape) && throw(ArgumentError("Size must be positive")) any(<(1), chunks) && throw(ArgumentError("Chunk size must be >= 1 along each dimension")) order === 'C' || throw(ArgumentError("Currently only 'C' storage order is supported")) - new{T2, N, C, F}(zarr_format, Base.RefValue{NTuple{N,Int}}(shape), chunks, dtype, compressor,fill_value, order, filters) + new{T2, N, C, F, S}(zarr_format, Base.RefValue{NTuple{N,Int}}(shape), chunks, dtype, compressor,fill_value, order, filters) end + function Metadata{T2, N, C, F}( + zarr_format, + shape, + chunks, + dtype, + compressor, + fill_value, + order, + filters, + dimension_separator::Char = '.' + ) where {T2,N,C,F} + return Metadata{T2, N, C, F, dimension_separator}( + zarr_format, + shape, + chunks, + dtype, + compressor, + fill_value, + order + ) + end + end +const DimensionSeparatedMetadata{S} = Metadata{<: Any, <: Any, <: Any, <: Any, S} + +function Base.getproperty(m::DimensionSeparatedMetadata{S}, name::Symbol) where S + if name == :dimension_separator + return S + end + return getfield(m, name) +end +Base.propertynames(m::Metadata) = (fieldnames(Metadata)..., :dimension_separator) + #To make unit tests pass with ref shape import Base.== function ==(m1::Metadata, m2::Metadata) @@ -123,7 +164,8 @@ function ==(m1::Metadata, m2::Metadata) m1.compressor == m2.compressor && m1.fill_value == m2.fill_value && m1.order == m2.order && - m1.filters == m2.filters + m1.filters == m2.filters && + m1.dimension_separator == m2.dimension_separator end @@ -135,9 +177,10 @@ function Metadata(A::AbstractArray{T, N}, chunks::NTuple{N, Int}; order::Char='C', filters::Nothing=nothing, fill_as_missing = false, + dimension_separator::Char = '.' ) where {T, N, C} T2 = (fill_value === nothing || !fill_as_missing) ? T : Union{T,Missing} - Metadata{T2, N, C, typeof(filters)}( + Metadata{T2, N, C, typeof(filters), dimension_separator}( zarr_format, size(A), chunks, @@ -175,7 +218,9 @@ function Metadata(d::AbstractDict, fill_as_missing) TU = (fv === nothing || !fill_as_missing) ? T : Union{T,Missing} - Metadata{TU, N, C, F}( + S = only(get(d, "dimension_separator", '.')) + + Metadata{TU, N, C, F, S}( d["zarr_format"], NTuple{N, Int}(d["shape"]) |> reverse, NTuple{N, Int}(d["chunks"]) |> reverse, @@ -197,7 +242,8 @@ function JSON.lower(md::Metadata) "compressor" => md.compressor, "fill_value" => fill_value_encoding(md.fill_value), "order" => md.order, - "filters" => md.filters + "filters" => md.filters, + "dimension_separator" => md.dimension_separator ) end diff --git a/test/runtests.jl b/test/runtests.jl index c472eb1..c01f441 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -15,8 +15,9 @@ CondaPkg.add("zarr"; version="2.*") @testset "fields" begin z = zzeros(Int64, 2, 3) @test z isa ZArray{Int64, 2, Zarr.BloscCompressor, - Zarr.DictStore} + Zarr.FormattedStore{2, '.', Zarr.DictStore}} + @test :a ∈ propertynames(z.storage) @test length(z.storage.a) === 3 @test length(z.storage.a["0.0"]) === 64 @test eltype(z.storage.a["0.0"]) === UInt8 @@ -31,6 +32,8 @@ CondaPkg.add("zarr"; version="2.*") @test z.metadata.compressor.shuffle === 1 @test z.attrs == Dict{Any, Any}() @test z.writeable === true + @test z.metadata.dimension_separator === Zarr.DS + @test :dimension_separator ∈ propertynames(z.metadata) @test_throws ArgumentError zzeros(Int64,2,3, chunks = (0,1)) @test_throws ArgumentError zzeros(Int64,0,-1) @test_throws ArgumentError Zarr.Metadata(zeros(2,2), (2,2), zarr_format = 3) @@ -40,7 +43,7 @@ CondaPkg.add("zarr"; version="2.*") @testset "methods" begin z = zzeros(Int64, 2, 3) @test z isa ZArray{Int64, 2, Zarr.BloscCompressor, - Zarr.DictStore} + Zarr.FormattedStore{2, '.', Zarr.DictStore}} @test eltype(z) === Int64 @test ndims(z) === 2 @@ -60,7 +63,7 @@ CondaPkg.add("zarr"; version="2.*") compressor=Zarr.NoCompressor()) @test z.metadata.compressor === Zarr.NoCompressor() - @test z.storage === Zarr.DirectoryStore("$dir/$name") + @test z.storage === Zarr.FormattedStore{2 ,'.'}(Zarr.DirectoryStore("$dir/$name")) @test isdir("$dir/$name") @test ispath("$dir/$name/.zarray") @test ispath("$dir/$name/.zattrs") @@ -69,12 +72,14 @@ CondaPkg.add("zarr"; version="2.*") @test JSON.parsefile("$dir/$name/.zarray") == Dict{String, Any}( "dtype" => " nothing, - "shape" => [3, 2], + "shape" => Any[3, 2], "order" => "C", "zarr_format" => 2, - "chunks" => [3, 2], + "chunks" => Any[3, 2], "fill_value" => nothing, - "compressor" => nothing) + "compressor" => nothing, + "dimension_separator" => "." + ) # call gc to avoid unlink: operation not permitted (EPERM) on Windows # might be because files are left open # from https://github.com/JuliaLang/julia/blob/f6344d32d3ebb307e2b54a77e042559f42d2ebf6/stdlib/SharedArrays/test/runtests.jl#L146 diff --git a/test/storage.jl b/test/storage.jl index 9e4fac7..1ef34d7 100644 --- a/test/storage.jl +++ b/test/storage.jl @@ -8,10 +8,39 @@ @test Zarr.normalize_path("/path/to/a") == "/path/to/a" end +@testset "Version and Dimension Separator" begin + v2cke_period = Zarr.V2ChunkKeyEncoding{'.'} + v2cke_slash = Zarr.V2ChunkKeyEncoding{'/'} + let ci = CartesianIndex() + @test Zarr.citostring(ci, 2, '.') == "0" + @test Zarr.citostring(ci, 2, '/') == "0" + @test Zarr.citostring(ci, 3, v2cke_period) == "0" + @test Zarr.citostring(ci, 3, v2cke_slash) == "0" + @test Zarr.citostring(ci, 3, '.') == "c.0" + @test Zarr.citostring(ci, 3, '/') == "c/0" + end + let ci = CartesianIndex(1,1,1) + @test Zarr.citostring(ci, 2, '.') == "0.0.0" + @test Zarr.citostring(ci, 2, '/') == "0/0/0" + @test Zarr.citostring(ci, 3, v2cke_period) == "0.0.0" + @test Zarr.citostring(ci, 3, v2cke_slash) == "0/0/0" + @test Zarr.citostring(ci, 3, '.') == "c.0.0.0" + @test Zarr.citostring(ci, 3, '/') == "c/0/0/0" + end + let ci = CartesianIndex(1,3,5) + @test Zarr.citostring(ci, 2, '.') == "4.2.0" + @test Zarr.citostring(ci, 2, '/') == "4/2/0" + @test Zarr.citostring(ci, 3, v2cke_period) == "4.2.0" + @test Zarr.citostring(ci, 3, v2cke_slash) == "4/2/0" + @test Zarr.citostring(ci, 3, '.') == "c.4.2.0" + @test Zarr.citostring(ci, 3, '/') == "c/4/2/0" + end +end + """ Function to test the interface of AbstractStore. Every complete implementation should pass this test. """ -function test_store_common(ds) +function test_store_common(ds::Zarr.AbstractStore) @test !Zarr.is_zgroup(ds,"") ds[".zgroup"]=rand(UInt8,50) @test haskey(ds,".zgroup") @@ -31,17 +60,23 @@ function test_store_common(ds) @test Zarr.subdirs(ds,"bar") == String[] #Test getindex and setindex data = rand(UInt8,50) - ds["bar/0.0.0"] = data + V = Zarr.zarr_format(ds) + S = Zarr.dimension_separator(ds) + first_ci_str = Zarr.citostring(CartesianIndex(1,1,1), V, S) + second_ci_str = Zarr.citostring(CartesianIndex(2,1,1), V, S) + ds["bar/" * first_ci_str] = data @test ds["bar/0.0.0"]==data @test Zarr.storagesize(ds,"bar")==50 - @test Zarr.isinitialized(ds,"bar/0.0.0") - @test !Zarr.isinitialized(ds,"bar/0.0.1") + @test Zarr.isinitialized(ds,"bar/" * first_ci_str) + @test !Zarr.isinitialized(ds,"bar/" * second_ci_str) Zarr.writeattrs(ds,"bar",Dict("a"=>"b")) @test Zarr.getattrs(ds,"bar")==Dict("a"=>"b") - delete!(ds,"bar/0.0.0") - @test !Zarr.isinitialized(ds,"bar",CartesianIndex((0,0,0))) - @test !Zarr.isinitialized(ds,"bar/0.0.0") - ds["bar/0.0.0"] = data + delete!(ds,"bar/" * first_ci_str) + @test !Zarr.isinitialized(ds,"bar",CartesianIndex((1,1,1))) + @test !Zarr.isinitialized(ds,"bar/" * first_ci_str) + ds["bar/" * first_ci_str] = data + @test !Zarr.isinitialized(ds, "bar", CartesianIndex(0,0,0)) + @test Zarr.isinitialized(ds, "bar", CartesianIndex(1,1,1)) #Add tests for empty storage @test Zarr.isemptysub(ds,"ba") @test Zarr.isemptysub(ds,"ba/")