From 8fa81075277d809686e418bf56128e59ee583d44 Mon Sep 17 00:00:00 2001 From: Matthijs Cox Date: Wed, 5 Nov 2025 16:28:25 +0100 Subject: [PATCH 01/19] refactor: extract build_refs! from cell array writer --- src/MAT_HDF5.jl | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/src/MAT_HDF5.jl b/src/MAT_HDF5.jl index b0d36d6..4b61602 100644 --- a/src/MAT_HDF5.jl +++ b/src/MAT_HDF5.jl @@ -456,6 +456,19 @@ end # Write cell arrays function m_write(mfile::MatlabHDF5File, parent::HDF5Parent, name::String, data::AbstractArray{T}) where T data = _normalize_arr(data) + refs = _write_references!(mfile, parent, data) + # Write the references as the chosen variable + cset, ctype = create_dataset(parent, name, refs) + try + write_dataset(cset, ctype, refs) + write_attribute(cset, name_type_attr_matlab, "cell") + finally + close(ctype) + close(cset) + end +end + +function _write_references!(mfile::MatlabHDF5File, parent::HDF5Parent, data::AbstractArray) pathrefs = "/#refs#" fid = HDF5.file(parent) local g @@ -499,15 +512,7 @@ function m_write(mfile::MatlabHDF5File, parent::HDF5Parent, name::String, data:: finally close(g) end - # Write the references as the chosen variable - cset, ctype = create_dataset(parent, name, refs) - try - write_dataset(cset, ctype, refs) - write_attribute(cset, name_type_attr_matlab, "cell") - finally - close(ctype) - close(cset) - end + return refs end # Check that keys are valid for a struct, and convert them to an array of ASCIIStrings From cf83cd5aa5ebbea0fb0c224fa9c754d5c12cf39c Mon Sep 17 00:00:00 2001 From: Matthijs Cox Date: Wed, 5 Nov 2025 16:58:29 +0100 Subject: [PATCH 02/19] feature: write Array{Dict} as struct array --- src/MAT_HDF5.jl | 41 +++++++++++++++++++++++++++++++++++++++++ test/write.jl | 17 ++++++++++++++++- 2 files changed, 57 insertions(+), 1 deletion(-) diff --git a/src/MAT_HDF5.jl b/src/MAT_HDF5.jl index 4b61602..1403dae 100644 --- a/src/MAT_HDF5.jl +++ b/src/MAT_HDF5.jl @@ -515,6 +515,47 @@ function _write_references!(mfile::MatlabHDF5File, parent::HDF5Parent, data::Abs return refs end + +# Struct array: Array of Dict => MATLAB struct array +function m_write(mfile::MatlabHDF5File, parent::HDF5Parent, name::String, + arr::AbstractArray{<:AbstractDict}) + + # Collect and validate fields from first element + fields = collect(keys(first(arr))) + asckeys = check_struct_keys(fields) # validates & String-ifies + # Ensure same field set for all elements + for d in arr + if !issetequal(keys(d), fields) + error("All struct elements must share identical field names. If you want a cell array, please use `Array{Any}` instead") + end + end + + g = create_group(parent, name) + try + write_attribute(g, name_type_attr_matlab, "struct") + write_attribute(g, "MATLAB_fields", HDF5.VLen(asckeys)) + + # For each field, build an array dataset with same shape as arr + for f in asckeys + vals = Array{Any}(undef, size(arr)) + for (idx, d) in enumerate(arr) + vals[idx] = d[f] + end + refs = _write_references!(mfile, parent, vals) + + # Create field dataset from refs WITHOUT tagging as MATLAB cell + dset, dtype = create_dataset(g, f, refs) + try + write_dataset(dset, dtype, refs) + finally + close(dtype); close(dset) + end + end + finally + close(g) + end +end + # Check that keys are valid for a struct, and convert them to an array of ASCIIStrings function check_struct_keys(k::Vector) asckeys = Vector{String}(undef, length(k)) diff --git a/test/write.jl b/test/write.jl index ae4a2eb..090c5b2 100644 --- a/test/write.jl +++ b/test/write.jl @@ -135,4 +135,19 @@ test_write(Dict("adjoint_arr"=>[1 2 3;4 5 6;7 8 9]')) test_write(Dict("reshape_arr"=>reshape([1 2 3;4 5 6;7 8 9]',1,9))) test_write(Dict("adjoint_arr"=>Any[1 2 3;4 5 6;7 8 9]')) -test_write(Dict("reshape_arr"=>reshape(Any[1 2 3;4 5 6;7 8 9]',1,9))) \ No newline at end of file +test_write(Dict("reshape_arr"=>reshape(Any[1 2 3;4 5 6;7 8 9]',1,9))) + +# test struct array +sarr = Dict{String, Any}[ + Dict("x"=>[1.0,2.0], "y"=>[3.0,4.0]), + Dict("x"=>[5.0,6.0], "y"=>[7.0,8.0]) +] +test_write(Dict("s_array" => sarr)) + +# test error of unequal structs +wrong_arr = Dict{String, Any}[ + Dict("x"=>[1.0,2.0], "y"=>[3.0,4.0]), + Dict("x"=>[5.0,6.0], "z"=>[7.0,8.0]) +] +msg = "All struct elements must share identical field names. If you want a cell array, please use `Array{Any}` instead" +@test_throws ErrorException(msg) matwrite(tmpfile, Dict("s_array" => sarr)) From d4d04006a1f5b804078f9299d4a53d55d99ae91d Mon Sep 17 00:00:00 2001 From: Matthijs Cox Date: Thu, 6 Nov 2025 10:50:25 +0100 Subject: [PATCH 03/19] refactor and fix tests for struct arrays --- src/MAT_HDF5.jl | 8 ++++---- test/write.jl | 22 +++++++++++++++------- 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/src/MAT_HDF5.jl b/src/MAT_HDF5.jl index 1403dae..4c83b81 100644 --- a/src/MAT_HDF5.jl +++ b/src/MAT_HDF5.jl @@ -526,7 +526,7 @@ function m_write(mfile::MatlabHDF5File, parent::HDF5Parent, name::String, # Ensure same field set for all elements for d in arr if !issetequal(keys(d), fields) - error("All struct elements must share identical field names. If you want a cell array, please use `Array{Any}` instead") + error("Incorrect struct array. All elements must share identical field names. If you want a cell array, please use `Array{Any}` instead") end end @@ -537,11 +537,11 @@ function m_write(mfile::MatlabHDF5File, parent::HDF5Parent, name::String, # For each field, build an array dataset with same shape as arr for f in asckeys - vals = Array{Any}(undef, size(arr)) + field_values = Array{Any}(undef, size(arr)) for (idx, d) in enumerate(arr) - vals[idx] = d[f] + field_values[idx] = d[f] end - refs = _write_references!(mfile, parent, vals) + refs = _write_references!(mfile, parent, field_values) # Create field dataset from refs WITHOUT tagging as MATLAB cell dset, dtype = create_dataset(g, f, refs) diff --git a/test/write.jl b/test/write.jl index 090c5b2..34257be 100644 --- a/test/write.jl +++ b/test/write.jl @@ -139,15 +139,23 @@ test_write(Dict("reshape_arr"=>reshape(Any[1 2 3;4 5 6;7 8 9]',1,9))) # test struct array sarr = Dict{String, Any}[ - Dict("x"=>[1.0,2.0], "y"=>[3.0,4.0]), - Dict("x"=>[5.0,6.0], "y"=>[7.0,8.0]) + Dict("x"=>[1.0,2.0], SubString("y")=>[3.0,4.0]), + Dict("x"=>[5.0,6.0], "y"=>[Dict("a"=>7), Dict("a"=>8)]) ] -test_write(Dict("s_array" => sarr)) +# we have to test Array size is maintained inside mat files +sarr = reshape(sarr, 1, 2) +# we cannot yet use `test_write()` because struct arrays are read as a struct with arrays of field values +matwrite(tmpfile, Dict("s_array" => sarr)) +read_sarr = matread(tmpfile)["s_array"] +@test size(read_sarr["x"]) == size(sarr) +@test size(read_sarr["y"]) == size(sarr) +@test read_sarr["y"][1] == sarr[1]["y"] +@test read_sarr["y"][2]["a"] == [7,8] # test error of unequal structs -wrong_arr = Dict{String, Any}[ +wrong_sarr = Dict{String, Any}[ Dict("x"=>[1.0,2.0], "y"=>[3.0,4.0]), - Dict("x"=>[5.0,6.0], "z"=>[7.0,8.0]) + Dict("x"=>[5.0,6.0]) ] -msg = "All struct elements must share identical field names. If you want a cell array, please use `Array{Any}` instead" -@test_throws ErrorException(msg) matwrite(tmpfile, Dict("s_array" => sarr)) +msg = "Incorrect struct array. All elements must share identical field names. If you want a cell array, please use `Array{Any}` instead" +@test_throws ErrorException(msg) matwrite(tmpfile, Dict("s_array" => wrong_sarr)) From 56aff161b4304246f9b4440934a5e28449eeabca Mon Sep 17 00:00:00 2001 From: Matthijs Cox Date: Thu, 6 Nov 2025 11:04:58 +0100 Subject: [PATCH 04/19] update readme for struct array writing --- README.md | 53 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/README.md b/README.md index 632e012..0811f5e 100644 --- a/README.md +++ b/README.md @@ -86,6 +86,59 @@ end close(file) ``` +## Cell versus struct array writing + +Cell arrays are written for Any arrays `Array{Any}` or any other unsupported element type: + +```julia +sarr = Any[ + Dict("x"=>1.0, "y"=>2.0), + Dict("x"=>3.0, "y"=>4.0) +] +matwrite("matfile.mat", Dict("cell" => sarr)) + +``` + +Inside MATLAB you will find: + +```matlab +>> load('matfile.mat') +>> cell + +cell = + + 2×1 cell array + + {1×1 struct} + {1×1 struct} +``` + +Struct arrays, instead of cell arrays, can now be written with MAT.jl using Dict arrays `AbstractArray{<:AbstractDict}` if all the Dicts have equal keys: + +```julia +sarr = Dict{String, Any}[ + Dict("x"=>1.0, "y"=>2.0), + Dict("x"=>3.0, "y"=>4.0) +] +matwrite("matfile.mat", Dict("struct_array" => sarr)) + +``` + +Now you'll find the following inside MATLAB: + +```matlab +>> load('matfile.mat') +>> struct_array + +struct_array = + +[2x1 struct, 576 bytes] +x: 1 +y: 2 +``` + +Note that MAT.jl v0.10 will read struct arrays as a struct with arrays in the fields, which is how they are stored inside the .mat HDF5 file. This behavior this might change in future versions. + ## Caveats * All files are written in MATLAB v7.3 format by default. From fc6c1172ceec6b6ec92ef7c2d07931443137952f Mon Sep 17 00:00:00 2001 From: Matthijs Cox Date: Mon, 10 Nov 2025 13:39:14 +0100 Subject: [PATCH 05/19] fix docs: avoid Literate examples if they dont exist --- docs/make.jl | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/docs/make.jl b/docs/make.jl index baf27b0..669323e 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -26,14 +26,17 @@ binder_root_url = repo = eval(:($reps)) DocMeta.setdocmeta!(repo, :DocTestSetup, :(using $reps); recursive=true) -for (root, _, files) in walkdir(lit), file in files - splitext(file)[2] == ".jl" || continue # process .jl files only - ipath = joinpath(root, file) - opath = splitdir(replace(ipath, lit => gen))[1] - Literate.markdown(ipath, opath; documenter = execute, # run examples - repo_root_url, nbviewer_root_url, binder_root_url) - Literate.notebook(ipath, opath; execute = false, # no-run notebooks - repo_root_url, nbviewer_root_url, binder_root_url) +# can all Literate docs code be removed? because there is no folder docs/src/lit +if isdir(lit) + for (root, _, files) in walkdir(lit), file in files + splitext(file)[2] == ".jl" || continue # process .jl files only + ipath = joinpath(root, file) + opath = splitdir(replace(ipath, lit => gen))[1] + Literate.markdown(ipath, opath; documenter = execute, # run examples + repo_root_url, nbviewer_root_url, binder_root_url) + Literate.notebook(ipath, opath; execute = false, # no-run notebooks + repo_root_url, nbviewer_root_url, binder_root_url) + end end From 392537a50ef12b0857e63e8276275710f95dbfd4 Mon Sep 17 00:00:00 2001 From: Matthijs Cox Date: Thu, 13 Nov 2025 12:04:35 +0100 Subject: [PATCH 06/19] read-write consistency for v7 struct arrays via MatlabStructArray --- src/MAT.jl | 3 + src/MAT_HDF5.jl | 148 ++++++++++++++++++++++++----------------------- src/MAT_types.jl | 100 ++++++++++++++++++++++++++++++++ test/read.jl | 4 +- test/write.jl | 12 +++- 5 files changed, 190 insertions(+), 77 deletions(-) create mode 100644 src/MAT_types.jl diff --git a/src/MAT.jl b/src/MAT.jl index a11efee..e4e6bd8 100644 --- a/src/MAT.jl +++ b/src/MAT.jl @@ -26,6 +26,9 @@ module MAT using HDF5, SparseArrays +include("MAT_types.jl") +using .MAT_types + include("MAT_HDF5.jl") include("MAT_v5.jl") include("MAT_v4.jl") diff --git a/src/MAT_HDF5.jl b/src/MAT_HDF5.jl index 4c83b81..6507d79 100644 --- a/src/MAT_HDF5.jl +++ b/src/MAT_HDF5.jl @@ -32,6 +32,7 @@ using HDF5, SparseArrays import Base: names, read, write, close import HDF5: Reference +import ..MAT_types: MatlabStructArray, StructArrayField, convert_struct_array const HDF5Parent = Union{HDF5.File, HDF5.Group} const HDF5BitsOrBool = Union{HDF5.BitsType,Bool} @@ -128,6 +129,21 @@ function read_complex(dtype::HDF5.Datatype, dset::HDF5.Dataset, ::Type{T}) where return read(dset, Complex{T}) end +function read_references(dset::HDF5.Dataset) + refs = read(dset, Reference) + out = Array{Any}(undef, size(refs)) + f = HDF5.file(dset) + for i = 1:length(refs) + dset = f[refs[i]] + try + out[i] = m_read(dset) + finally + close(dset) + end + end + return out +end + function m_read(dset::HDF5.Dataset) if haskey(dset, empty_attr_matlab) # Empty arrays encode the dimensions as the dataset @@ -149,22 +165,14 @@ function m_read(dset::HDF5.Dataset) end end - mattype = haskey(dset, name_type_attr_matlab) ? read_attribute(dset, name_type_attr_matlab) : "cell" + mattype = haskey(dset, name_type_attr_matlab) ? read_attribute(dset, name_type_attr_matlab) : "struct_array_field" if mattype == "cell" # Cell arrays, represented as an array of refs - refs = read(dset, Reference) - out = Array{Any}(undef, size(refs)) - f = HDF5.file(dset) - for i = 1:length(refs) - dset = f[refs[i]] - try - out[i] = m_read(dset) - finally - close(dset) - end - end - return out + return read_references(dset) + elseif mattype == "struct_array_field" + # TODO: check it has references? + return StructArrayField(read_references(dset)) elseif !haskey(str2type_matlab,mattype) @warn "MATLAB $mattype values are currently not supported" return missing @@ -192,46 +200,37 @@ function add!(A, x) A end -# reading a struct, struct array, or sparse matrix -function m_read(g::HDF5.Group) - mattype = read_attribute(g, name_type_attr_matlab) - if mattype != "struct" - # Check if this is a sparse matrix. - fn = keys(g) - if haskey(attributes(g), sparse_attr_matlab) - # This is a sparse matrix. - # ir is the row indices, jc is the column boundaries. - # We add one to account for the zero-based (MATLAB) to one-based (Julia) transition - jc = add!(convert(Vector{Int}, read(g, "jc")), 1) - if "data" in fn && "ir" in fn && "jc" in fn - # This matrix is not empty. - ir = add!(convert(Vector{Int}, read(g, "ir")), 1) - dset = g["data"] - T = str2type_matlab[mattype] - try - dtype = datatype(dset) - class_id = HDF5.API.h5t_get_class(dtype.id) - try - data = class_id == HDF5.API.H5T_COMPOUND ? read_complex(dtype, dset, T) : read(dset, T) - finally - close(dtype) - end - finally - close(dset) - end - else - # This matrix is empty. - ir = Int[] - data = str2type_matlab[mattype][] +function read_sparse_matrix(g::HDF5.Group, mattype::String) + local data + fn = keys(g) + # ir is the row indices, jc is the column boundaries. + # We add one to account for the zero-based (MATLAB) to one-based (Julia) transition + jc = add!(convert(Vector{Int}, read(g, "jc")), 1) + if "data" in fn && "ir" in fn && "jc" in fn + # This matrix is not empty. + ir = add!(convert(Vector{Int}, read(g, "ir")), 1) + dset = g["data"] + T = str2type_matlab[mattype] + try + dtype = datatype(dset) + class_id = HDF5.API.h5t_get_class(dtype.id) + try + data = class_id == HDF5.API.H5T_COMPOUND ? read_complex(dtype, dset, T) : read(dset, T) + finally + close(dtype) end - return SparseMatrixCSC(convert(Int, read_attribute(g, sparse_attr_matlab)), length(jc)-1, jc, ir, data) - elseif mattype == "function_handle" - @warn "MATLAB $mattype values are currently not supported" - return missing - else - @warn "Unknown non-struct group of type $mattype detected; attempting to read as struct" + finally + close(dset) end + else + # This matrix is empty. + ir = Int[] + data = str2type_matlab[mattype][] end + return SparseMatrixCSC(convert(Int, read_attribute(g, sparse_attr_matlab)), length(jc)-1, jc, ir, data) +end + +function read_struct_as_dict(g::HDF5.Group) if haskey(g, "MATLAB_fields") fn = [join(f) for f in read_attribute(g, "MATLAB_fields")] else @@ -246,7 +245,26 @@ function m_read(g::HDF5.Group) close(dset) end end - s + return s +end + +# reading a struct, struct array, or sparse matrix +function m_read(g::HDF5.Group) + mattype = read_attribute(g, name_type_attr_matlab) + if mattype != "struct" + # Check if this is a sparse matrix. + if haskey(attributes(g), sparse_attr_matlab) + return read_sparse_matrix(g, mattype) + elseif mattype == "function_handle" + @warn "MATLAB $mattype values are currently not supported" + return missing + else + @warn "Unknown non-struct group of type $mattype detected; attempting to read as struct" + end + end + s = read_struct_as_dict(g) + out = convert_struct_array(s) + return out end """ @@ -519,32 +537,18 @@ end # Struct array: Array of Dict => MATLAB struct array function m_write(mfile::MatlabHDF5File, parent::HDF5Parent, name::String, arr::AbstractArray{<:AbstractDict}) + m_write(mfile, parent, name, MatlabStructArray(arr)) +end - # Collect and validate fields from first element - fields = collect(keys(first(arr))) - asckeys = check_struct_keys(fields) # validates & String-ifies - # Ensure same field set for all elements - for d in arr - if !issetequal(keys(d), fields) - error("Incorrect struct array. All elements must share identical field names. If you want a cell array, please use `Array{Any}` instead") - end - end - +# Struct array: Array of Dict => MATLAB struct array +function m_write(mfile::MatlabHDF5File, parent::HDF5Parent, name::String, arr::MatlabStructArray) g = create_group(parent, name) try write_attribute(g, name_type_attr_matlab, "struct") - write_attribute(g, "MATLAB_fields", HDF5.VLen(asckeys)) - - # For each field, build an array dataset with same shape as arr - for f in asckeys - field_values = Array{Any}(undef, size(arr)) - for (idx, d) in enumerate(arr) - field_values[idx] = d[f] - end + write_attribute(g, "MATLAB_fields", HDF5.VLen(arr.names)) + for (fieldname, field_values) in zip(arr.names, arr.values) refs = _write_references!(mfile, parent, field_values) - - # Create field dataset from refs WITHOUT tagging as MATLAB cell - dset, dtype = create_dataset(g, f, refs) + dset, dtype = create_dataset(g, fieldname, refs) try write_dataset(dset, dtype, refs) finally diff --git a/src/MAT_types.jl b/src/MAT_types.jl new file mode 100644 index 0000000..ef20b79 --- /dev/null +++ b/src/MAT_types.jl @@ -0,0 +1,100 @@ +# MAT_types.jl +# Internal types used by MAT.jl +# +# Copyright (C) 2012 Matthijs Cox +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +########################################### +## Reading and writing MATLAB .mat files ## +########################################### + +module MAT_types + + export MatlabStructArray, StructArrayField, convert_struct_array + + # struct arrays are stored as columns per field name + struct MatlabStructArray{N} + names::Vector{String} + values::Vector{Array{Any,N}} + end + + function Base.isequal(m1::MatlabStructArray{N},m2::MatlabStructArray{N}) where N + return isequal(m1.names, m2.names) && isequal(m1.values, m2.values) + end + + function find_index(m::MatlabStructArray, s::AbstractString) + idx = findfirst(isequal(s), m.names) + if isnothing(idx) + error("field \"$s\" not found in MatlabStructArray") + end + return idx + end + + function Base.getindex(m::MatlabStructArray, s::AbstractString) + idx = find_index(m, s) + return getindex(m.values, idx) + end + + # convert Dict array to MatlabStructArray + function MatlabStructArray(arr::Array{<:AbstractDict{T}, N}) where {T<:AbstractString, N} + field_names = string.(keys(first(arr))) + # Ensure same field set for all elements + for d in arr + if !issetequal(keys(d), field_names) + error("Cannot convert Dict array to MatlabStructArray. All elements must share identical field names") + end + end + field_values = Vector{Array{Any,N}}(undef, length(field_names)) + for (idx,f) in enumerate(field_names) + this_field_values = Array{Any, N}(undef, size(arr)) + for (idx, d) in enumerate(arr) + this_field_values[idx] = d[f] + end + field_values[idx] = this_field_values + end + return MatlabStructArray{N}(field_names, field_values) + end + + function Base.Dict(arr::MatlabStructArray) + return Base.Dict{String, Any}(arr) + end + function Base.Dict{String, Any}(arr::MatlabStructArray) + Base.Dict{String, Any}(arr.names .=> arr.values) + end + + struct StructArrayField{N} + values::Array{Any,N} + end + + function convert_struct_array(d::Dict{String, Any}) + # there is no possibility of having cell arrays mixed with struct arrays (afaik) + field_values = first(values(d)) + if field_values isa StructArrayField + return MatlabStructArray( + collect(keys(d)), + [arr.values for arr in values(d)], + ) + else + return d + end + end + +end \ No newline at end of file diff --git a/test/read.jl b/test/read.jl index 4d8c9d4..75b8c85 100644 --- a/test/read.jl +++ b/test/read.jl @@ -107,9 +107,9 @@ for _format in ["v6", "v7", "v7.3"] "b" => [1.0 2.0], "c" => [1.0 2.0 3.0] ), - "s2" => Dict{String,Any}("a" => Any[1.0 2.0]) + "s2" => MAT.MatlabStructArray(["a"], [Any[1.0 2.0]]) ) - check("struct.mat", result) + #check("struct.mat", result) result = Dict( "logical" => false, diff --git a/test/write.jl b/test/write.jl index 34257be..8ac71a7 100644 --- a/test/write.jl +++ b/test/write.jl @@ -137,25 +137,31 @@ test_write(Dict("reshape_arr"=>reshape([1 2 3;4 5 6;7 8 9]',1,9))) test_write(Dict("adjoint_arr"=>Any[1 2 3;4 5 6;7 8 9]')) test_write(Dict("reshape_arr"=>reshape(Any[1 2 3;4 5 6;7 8 9]',1,9))) -# test struct array +# test nested struct array - interface via Dict array sarr = Dict{String, Any}[ Dict("x"=>[1.0,2.0], SubString("y")=>[3.0,4.0]), Dict("x"=>[5.0,6.0], "y"=>[Dict("a"=>7), Dict("a"=>8)]) ] # we have to test Array size is maintained inside mat files sarr = reshape(sarr, 1, 2) -# we cannot yet use `test_write()` because struct arrays are read as a struct with arrays of field values matwrite(tmpfile, Dict("s_array" => sarr)) read_sarr = matread(tmpfile)["s_array"] +@test read_sarr isa MAT.MatlabStructArray @test size(read_sarr["x"]) == size(sarr) @test size(read_sarr["y"]) == size(sarr) @test read_sarr["y"][1] == sarr[1]["y"] @test read_sarr["y"][2]["a"] == [7,8] +sarr = Dict{String, Any}[ + Dict("x"=>[1.0,2.0], SubString("y")=>[3.0,4.0]), + Dict("x"=>[5.0,6.0], "y"=>[]) +] +test_write(Dict("s_array" => MAT.MatlabStructArray(sarr))) + # test error of unequal structs wrong_sarr = Dict{String, Any}[ Dict("x"=>[1.0,2.0], "y"=>[3.0,4.0]), Dict("x"=>[5.0,6.0]) ] -msg = "Incorrect struct array. All elements must share identical field names. If you want a cell array, please use `Array{Any}` instead" +msg = "Cannot convert Dict array to MatlabStructArray. All elements must share identical field names" @test_throws ErrorException(msg) matwrite(tmpfile, Dict("s_array" => wrong_sarr)) From 4c46cc107fa09aed73386395bf8120407d26f372 Mon Sep 17 00:00:00 2001 From: Matthijs Cox Date: Thu, 13 Nov 2025 12:56:35 +0100 Subject: [PATCH 07/19] MatlabStructArray conversion testing --- src/MAT_types.jl | 11 +++++++++++ test/runtests.jl | 1 + test/types.jl | 17 +++++++++++++++++ test/write.jl | 5 +---- 4 files changed, 30 insertions(+), 4 deletions(-) create mode 100644 test/types.jl diff --git a/src/MAT_types.jl b/src/MAT_types.jl index ef20b79..94f6ba1 100644 --- a/src/MAT_types.jl +++ b/src/MAT_types.jl @@ -80,6 +80,17 @@ module MAT_types Base.Dict{String, Any}(arr.names .=> arr.values) end + function Base.Array(arr::MatlabStructArray{N}) where N + first_field = first(arr.values) + sz = size(first_field) + result = Array{Dict{String,Any}, N}(undef, sz) + for idx in eachindex(first_field) + element_values = [v[idx] for v in arr.values] + result[idx] = Dict{String, Any}(arr.names .=> element_values) + end + return result + end + struct StructArrayField{N} values::Array{Any,N} end diff --git a/test/runtests.jl b/test/runtests.jl index 159a125..5ba9e68 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,5 +1,6 @@ using SparseArrays, LinearAlgebra +include("types.jl") include("read.jl") include("readwrite4.jl") include("write.jl") diff --git a/test/types.jl b/test/types.jl new file mode 100644 index 0000000..474019a --- /dev/null +++ b/test/types.jl @@ -0,0 +1,17 @@ +# MatlabStructArray construction from dict array +d_arr = Dict{String, Any}[ + Dict("x"=>[1.0,2.0], SubString("y")=>[3.0,4.0]), + Dict("x"=>[5.0,6.0], "y"=>[]) +] +s_arr = MAT.MatlabStructArray(d_arr) +@test s_arr["y"][2] == d_arr[2]["y"] +@test s_arr["x"][1] == d_arr[1]["y"] + +# convert to Dict (legacy read behavior) +d = Dict(s_arr) +@test d isa Dict{String, Any} +@test collect(keys(d)) == s_arr.names +@test collect(values(d)) == s_arr.values + +# convert back to dict array +@test Array(s_arr) == d_arr \ No newline at end of file diff --git a/test/write.jl b/test/write.jl index 8ac71a7..f0086b0 100644 --- a/test/write.jl +++ b/test/write.jl @@ -147,10 +147,7 @@ sarr = reshape(sarr, 1, 2) matwrite(tmpfile, Dict("s_array" => sarr)) read_sarr = matread(tmpfile)["s_array"] @test read_sarr isa MAT.MatlabStructArray -@test size(read_sarr["x"]) == size(sarr) -@test size(read_sarr["y"]) == size(sarr) -@test read_sarr["y"][1] == sarr[1]["y"] -@test read_sarr["y"][2]["a"] == [7,8] +@test read_sarr["y"][2] isa MAT.MatlabStructArray sarr = Dict{String, Any}[ Dict("x"=>[1.0,2.0], SubString("y")=>[3.0,4.0]), From dd2ec580587505d2ea3fc5e1c26e53333617350e Mon Sep 17 00:00:00 2001 From: Matthijs Cox Date: Thu, 13 Nov 2025 14:03:49 +0100 Subject: [PATCH 08/19] fix tests for MatlabStructArray --- test/read.jl | 9 ++++++--- test/types.jl | 4 +++- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/test/read.jl b/test/read.jl index 75b8c85..ea724e9 100644 --- a/test/read.jl +++ b/test/read.jl @@ -101,15 +101,18 @@ for _format in ["v6", "v7", "v7.3"] ) check("cell.mat", result) - result = Dict( + result = Dict{String,Any}( "s" => Dict{String,Any}( "a" => 1.0, "b" => [1.0 2.0], "c" => [1.0 2.0 3.0] ), - "s2" => MAT.MatlabStructArray(["a"], [Any[1.0 2.0]]) + "s2" => Dict{String,Any}("a" => Any[1.0 2.0]) ) - #check("struct.mat", result) + if _format == "v7.3" + result["s2"] = MAT.MatlabStructArray(["a"], [Any[1.0 2.0]]) + end + check("struct.mat", result) result = Dict( "logical" => false, diff --git a/test/types.jl b/test/types.jl index 474019a..a4d9a32 100644 --- a/test/types.jl +++ b/test/types.jl @@ -1,3 +1,5 @@ +using MAT, Test + # MatlabStructArray construction from dict array d_arr = Dict{String, Any}[ Dict("x"=>[1.0,2.0], SubString("y")=>[3.0,4.0]), @@ -5,7 +7,7 @@ d_arr = Dict{String, Any}[ ] s_arr = MAT.MatlabStructArray(d_arr) @test s_arr["y"][2] == d_arr[2]["y"] -@test s_arr["x"][1] == d_arr[1]["y"] +@test s_arr["x"][1] == d_arr[1]["x"] # convert to Dict (legacy read behavior) d = Dict(s_arr) From 9bba4f144c7849e5a40d51862c4651259b5e4393 Mon Sep 17 00:00:00 2001 From: Matthijs Cox Date: Thu, 13 Nov 2025 15:02:49 +0100 Subject: [PATCH 09/19] MatlabStructArray iteration and readme --- README.md | 26 ++++++++++++++++++++++++-- src/MAT_HDF5.jl | 2 +- src/MAT_types.jl | 21 ++++++++++++++++++++- test/types.jl | 16 ++++++++++++++-- test/write.jl | 8 -------- 5 files changed, 59 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 0811f5e..7de9280 100644 --- a/README.md +++ b/README.md @@ -113,7 +113,7 @@ cell = {1×1 struct} ``` -Struct arrays, instead of cell arrays, can now be written with MAT.jl using Dict arrays `AbstractArray{<:AbstractDict}` if all the Dicts have equal keys: +Read and write behavior for struct arrays is different. For struct arrays (in .mat v7.3 files) we use the `MAT.MatlabStructArray` type. You can also write with MAT.jl using Dict arrays `AbstractArray{<:AbstractDict}` if all the Dicts have equal keys, which will automatically convert internally to `MAT.MatlabStructArray`. ```julia sarr = Dict{String, Any}[ @@ -122,6 +122,8 @@ sarr = Dict{String, Any}[ ] matwrite("matfile.mat", Dict("struct_array" => sarr)) +# which is the same as: +matwrite("matfile.mat", Dict("struct_array" => MAT.MatlabStructArray(sarr))) ``` Now you'll find the following inside MATLAB: @@ -137,7 +139,27 @@ x: 1 y: 2 ``` -Note that MAT.jl v0.10 will read struct arrays as a struct with arrays in the fields, which is how they are stored inside the .mat HDF5 file. This behavior this might change in future versions. +Note that when you read the file again, you'll find the `MAT.MatlabStructArray`, which you can convert back to the Dict array with `Array`: + +```julia +julia> sarr = matread("matfile.mat")["struct_array"] +MAT.MAT_types.MatlabStructArray{1} + "x": Any[1.0, 3.0] + "y": Any[2.0, 4.0] + +julia> sarr["x"] +2-element Vector{Any}: + 1.0 + 3.0 + +julia> Array(sarr) +2-element Vector{Dict{String, Any}}: + Dict("x" => 1.0, "y" => 2.0) + Dict("x" => 3.0, "y" => 4.0) + +``` + +Note that in MAT.jl v0.10 and older, or .mat versions before v7.3, will read struct arrays as a Dict with concatenated arrays in the fields/keys, which is equal to `Dict(sarr)`. ## Caveats diff --git a/src/MAT_HDF5.jl b/src/MAT_HDF5.jl index 6507d79..b2debb1 100644 --- a/src/MAT_HDF5.jl +++ b/src/MAT_HDF5.jl @@ -546,7 +546,7 @@ function m_write(mfile::MatlabHDF5File, parent::HDF5Parent, name::String, arr::M try write_attribute(g, name_type_attr_matlab, "struct") write_attribute(g, "MATLAB_fields", HDF5.VLen(arr.names)) - for (fieldname, field_values) in zip(arr.names, arr.values) + for (fieldname, field_values) in arr refs = _write_references!(mfile, parent, field_values) dset, dtype = create_dataset(g, fieldname, refs) try diff --git a/src/MAT_types.jl b/src/MAT_types.jl index 94f6ba1..e462dfc 100644 --- a/src/MAT_types.jl +++ b/src/MAT_types.jl @@ -36,6 +36,25 @@ module MAT_types values::Vector{Array{Any,N}} end + Base.eltype(::Type{MatlabStructArray{N}}) where N = Pair{String, Array{Any,N}} + Base.length(arr::MatlabStructArray) = length(arr.names) + + function Base.iterate(arr::T, i=next_state(arr)) where T<:MatlabStructArray + if i == 0 + return nothing + else + return (eltype(T)(arr.names[i], arr.values[i]), next_state(arr,i)) + end + end + next_state(arr, i=0) = length(arr)==i ? 0 : i+1 + + function Base.show(io::IO, ::MIME"text/plain", arr::MatlabStructArray) + summary(io, arr) + for (k,v) in arr + print(io, "\n \"$k\": $v") + end + end + function Base.isequal(m1::MatlabStructArray{N},m2::MatlabStructArray{N}) where N return isequal(m1.names, m2.names) && isequal(m1.values, m2.values) end @@ -54,7 +73,7 @@ module MAT_types end # convert Dict array to MatlabStructArray - function MatlabStructArray(arr::Array{<:AbstractDict{T}, N}) where {T<:AbstractString, N} + function MatlabStructArray(arr::AbstractArray{<:AbstractDict{T}, N}) where {T<:AbstractString, N} field_names = string.(keys(first(arr))) # Ensure same field set for all elements for d in arr diff --git a/test/types.jl b/test/types.jl index a4d9a32..bd8b095 100644 --- a/test/types.jl +++ b/test/types.jl @@ -15,5 +15,17 @@ d = Dict(s_arr) @test collect(keys(d)) == s_arr.names @test collect(values(d)) == s_arr.values -# convert back to dict array -@test Array(s_arr) == d_arr \ No newline at end of file +# iteration similar to Dict +@test length(s_arr) == 2 +@test collect(Dict(s_arr)) == collect(s_arr) + +# possibility to convert back to dict array via `Array` +@test Array(s_arr) == d_arr + +# test error of unequal structs +wrong_sarr = Dict{String, Any}[ + Dict("x"=>[1.0,2.0], "y"=>[3.0,4.0]), + Dict("x"=>[5.0,6.0]) +] +msg = "Cannot convert Dict array to MatlabStructArray. All elements must share identical field names" +@test_throws ErrorException(msg) MAT.MatlabStructArray(wrong_sarr) \ No newline at end of file diff --git a/test/write.jl b/test/write.jl index f0086b0..c92e4c9 100644 --- a/test/write.jl +++ b/test/write.jl @@ -154,11 +154,3 @@ sarr = Dict{String, Any}[ Dict("x"=>[5.0,6.0], "y"=>[]) ] test_write(Dict("s_array" => MAT.MatlabStructArray(sarr))) - -# test error of unequal structs -wrong_sarr = Dict{String, Any}[ - Dict("x"=>[1.0,2.0], "y"=>[3.0,4.0]), - Dict("x"=>[5.0,6.0]) -] -msg = "Cannot convert Dict array to MatlabStructArray. All elements must share identical field names" -@test_throws ErrorException(msg) matwrite(tmpfile, Dict("s_array" => wrong_sarr)) From 2c088118b3786ad5a7acf5d99d7d9053f5ea6095 Mon Sep 17 00:00:00 2001 From: Matthijs Cox Date: Fri, 14 Nov 2025 09:42:43 +0100 Subject: [PATCH 10/19] support empty struct arrays --- src/MAT_HDF5.jl | 48 +++++++++----- src/MAT_types.jl | 59 ++++++++++++++---- test/read.jl | 26 +++++--- test/types.jl | 25 +++++++- test/v6/empty_cell_struct.mat | Bin 0 -> 9248 bytes test/v6/empty_struct_arrays.mat | Bin 0 -> 344 bytes test/v7.3/empty_cell_struct.mat | Bin 0 -> 9248 bytes .../empty_struct_arrays.mat} | Bin 6752 -> 7128 bytes test/v7/empty_cell_struct.mat | Bin 0 -> 9248 bytes test/v7/empty_struct_arrays.mat | Bin 0 -> 299 bytes test/write.jl | 3 + 11 files changed, 124 insertions(+), 37 deletions(-) create mode 100644 test/v6/empty_cell_struct.mat create mode 100644 test/v6/empty_struct_arrays.mat create mode 100644 test/v7.3/empty_cell_struct.mat rename test/{empty_struct.mat => v7.3/empty_struct_arrays.mat} (74%) create mode 100644 test/v7/empty_cell_struct.mat create mode 100644 test/v7/empty_struct_arrays.mat diff --git a/src/MAT_HDF5.jl b/src/MAT_HDF5.jl index b2debb1..0b6420a 100644 --- a/src/MAT_HDF5.jl +++ b/src/MAT_HDF5.jl @@ -155,7 +155,8 @@ function m_read(dset::HDF5.Dataset) # Not sure if this check is necessary but it is checked in # `m_read(g::HDF5.Group)` if haskey(dset, "MATLAB_fields") - return Dict{String,Any}(join(n)=>[] for n in read_attribute(dset, "MATLAB_fields")) + field_names = [join(n) for n in read_attribute(dset, "MATLAB_fields")] + return MatlabStructArray(field_names, tuple(dims...)) else return Dict{String,Any}() end @@ -171,7 +172,7 @@ function m_read(dset::HDF5.Dataset) # Cell arrays, represented as an array of refs return read_references(dset) elseif mattype == "struct_array_field" - # TODO: check it has references? + # This will be converted into MatlabStructArray in `m_read(g::HDF5.Group)` return StructArrayField(read_references(dset)) elseif !haskey(str2type_matlab,mattype) @warn "MATLAB $mattype values are currently not supported" @@ -540,23 +541,38 @@ function m_write(mfile::MatlabHDF5File, parent::HDF5Parent, name::String, m_write(mfile, parent, name, MatlabStructArray(arr)) end -# Struct array: Array of Dict => MATLAB struct array +# MATLAB struct array function m_write(mfile::MatlabHDF5File, parent::HDF5Parent, name::String, arr::MatlabStructArray) - g = create_group(parent, name) - try - write_attribute(g, name_type_attr_matlab, "struct") - write_attribute(g, "MATLAB_fields", HDF5.VLen(arr.names)) - for (fieldname, field_values) in arr - refs = _write_references!(mfile, parent, field_values) - dset, dtype = create_dataset(g, fieldname, refs) - try - write_dataset(dset, dtype, refs) - finally - close(dtype); close(dset) + first_value = first(arr.values) + if isempty(first_value) + # write an empty struct array + adata = [size(first_value)...] + dset, dtype = create_dataset(parent, name, adata) + try + write_attribute(dset, empty_attr_matlab, 0x01) + write_attribute(dset, name_type_attr_matlab, "struct") + write_attribute(dset, "MATLAB_fields", HDF5.VLen(arr.names)) + write_dataset(dset, dtype, adata) + finally + close(dtype); close(dset) + end + else + g = create_group(parent, name) + try + write_attribute(g, name_type_attr_matlab, "struct") + write_attribute(g, "MATLAB_fields", HDF5.VLen(arr.names)) + for (fieldname, field_values) in arr + refs = _write_references!(mfile, parent, field_values) + dset, dtype = create_dataset(g, fieldname, refs) + try + write_dataset(dset, dtype, refs) + finally + close(dtype); close(dset) + end end + finally + close(g) end - finally - close(g) end end diff --git a/src/MAT_types.jl b/src/MAT_types.jl index e462dfc..40f7820 100644 --- a/src/MAT_types.jl +++ b/src/MAT_types.jl @@ -34,8 +34,38 @@ module MAT_types struct MatlabStructArray{N} names::Vector{String} values::Vector{Array{Any,N}} + function MatlabStructArray(names::Vector{String}, values::Vector{Array{Any,N}}) where N + # call MatlabStructArray{N}() to avoid the check + check_struct_array(names, values) + return new{N}(names, values) + end + function MatlabStructArray{N}(names::Vector{String}, values::Vector{Array{Any,N}}) where N + return new{N}(names, values) + end + end + + function check_struct_array(names::Vector{String}, values::Vector{Array{Any,N}}) where N + if length(names) != length(values) + error("MatlabStructArray requires equal number of names and values") + end + first_value, rest_values = Iterators.peel(values) + first_len = length(first_value) + if !all(x->length(x)==first_len, rest_values) + error("MatlabStructArray requires all value columns to be of equal length") + end end + function MatlabStructArray(names::AbstractVector{<:AbstractString}, values::AbstractArray{<:AbstractArray{T,N}}) where {T,N} + MatlabStructArray{N}(string.(names), Vector{Array{Any,N}}(values)) + end + + # empty array + function MatlabStructArray(names::AbstractVector{<:AbstractString}, dims::Tuple) + N = length(dims) + return MatlabStructArray{N}(names, [Array{Any, N}(undef, dims...) for n in names]) + end + MatlabStructArray(names::AbstractVector{<:AbstractString}) = MatlabStructArray(names, (0,0)) + Base.eltype(::Type{MatlabStructArray{N}}) where N = Pair{String, Array{Any,N}} Base.length(arr::MatlabStructArray) = length(arr.names) @@ -55,10 +85,14 @@ module MAT_types end end - function Base.isequal(m1::MatlabStructArray{N},m2::MatlabStructArray{N}) where N + function Base.:(==)(m1::MatlabStructArray{N},m2::MatlabStructArray{N}) where N return isequal(m1.names, m2.names) && isequal(m1.values, m2.values) end + function Base.isapprox(m1::MatlabStructArray,m2::MatlabStructArray; kwargs...) + return isequal(m1.names, m2.names) && isapprox(m1.values, m2.values; kwargs...) + end + function find_index(m::MatlabStructArray, s::AbstractString) idx = findfirst(isequal(s), m.names) if isnothing(idx) @@ -73,19 +107,20 @@ module MAT_types end # convert Dict array to MatlabStructArray - function MatlabStructArray(arr::AbstractArray{<:AbstractDict{T}, N}) where {T<:AbstractString, N} - field_names = string.(keys(first(arr))) + function MatlabStructArray(arr::AbstractArray{<:AbstractDict, N}) where N + first_keys = keys(first(arr)) + field_names = string.(first_keys) # Ensure same field set for all elements for d in arr - if !issetequal(keys(d), field_names) + if !issetequal(keys(d), first_keys) error("Cannot convert Dict array to MatlabStructArray. All elements must share identical field names") end end field_values = Vector{Array{Any,N}}(undef, length(field_names)) - for (idx,f) in enumerate(field_names) + for (idx,k) in enumerate(first_keys) this_field_values = Array{Any, N}(undef, size(arr)) for (idx, d) in enumerate(arr) - this_field_values[idx] = d[f] + this_field_values[idx] = d[k] end field_values[idx] = this_field_values end @@ -99,13 +134,14 @@ module MAT_types Base.Dict{String, Any}(arr.names .=> arr.values) end - function Base.Array(arr::MatlabStructArray{N}) where N + Base.Array(arr::MatlabStructArray) = Array{Dict{String,Any}}(arr) + function Base.Array{D}(arr::MatlabStructArray{N}) where {T,D<:AbstractDict{T},N} first_field = first(arr.values) sz = size(first_field) - result = Array{Dict{String,Any}, N}(undef, sz) + result = Array{D, N}(undef, sz) for idx in eachindex(first_field) - element_values = [v[idx] for v in arr.values] - result[idx] = Dict{String, Any}(arr.names .=> element_values) + element_values = (v[idx] for v in arr.values) + result[idx] = D(T.(arr.names) .=> element_values) end return result end @@ -113,12 +149,13 @@ module MAT_types struct StructArrayField{N} values::Array{Any,N} end + dimension(::StructArrayField{N}) where N = N function convert_struct_array(d::Dict{String, Any}) # there is no possibility of having cell arrays mixed with struct arrays (afaik) field_values = first(values(d)) if field_values isa StructArrayField - return MatlabStructArray( + return MatlabStructArray{dimension(field_values)}( collect(keys(d)), [arr.values for arr in values(d)], ) diff --git a/test/read.jl b/test/read.jl index ea724e9..619e554 100644 --- a/test/read.jl +++ b/test/read.jl @@ -200,14 +200,6 @@ let objtestfile = "obj.mat" @test vars["A"]["class"] == "Assoc" end -# test reading of empty struct -let objtestfile = "empty_struct.mat" - vars = matread(joinpath(dirname(@__FILE__), objtestfile)) - @test "a" in keys(vars) - @test vars["a"]["size"] == [] - @test vars["a"]["params"] == [] -end - # test reading of a Matlab figure let objtestfile = "figure.fig" vars = matread(joinpath(dirname(@__FILE__), objtestfile)) @@ -240,3 +232,21 @@ let objtestfile = "old_class.mat" @test "tc_old" in keys(vars) @test "foo" in keys(vars["tc_old"]) end + +let objtestfile = "empty_struct_arrays.mat" + folder = dirname(@__FILE__) + for _format in ["v6", "v7", "v7.3"] + vars = matread(joinpath(folder, _format, objtestfile)) + @test vars["s00"]["a"] == Matrix{Any}(undef, 0, 0) + @test vars["s10"]["a"] == Matrix{Any}(undef, 1, 0) + @test vars["s01"]["a"] == Matrix{Any}(undef, 0, 1) + end +end + +let objtestfile = "empty_cell_struct.mat" + folder = dirname(@__FILE__) + for _format in ["v6", "v7", "v7.3"] + vars = matread(joinpath(folder, _format, objtestfile)) + @test vars["s"]["a"] == Matrix{Any}(undef, 0, 0) + end +end diff --git a/test/types.jl b/test/types.jl index bd8b095..4ca9c4e 100644 --- a/test/types.jl +++ b/test/types.jl @@ -9,18 +9,39 @@ s_arr = MAT.MatlabStructArray(d_arr) @test s_arr["y"][2] == d_arr[2]["y"] @test s_arr["x"][1] == d_arr[1]["x"] -# convert to Dict (legacy read behavior) +# constructor errors to protect the user +@test_throws ErrorException MAT.MatlabStructArray(["a", "b"], [[]]) +@test_throws ErrorException MAT.MatlabStructArray(["a", "b"], [[],[0.1, 0.2]]) + +# equality checks +@test isequal(MAT.MatlabStructArray(["a"], [[0.1, 0.2]]), MAT.MatlabStructArray(["a"], [[0.1, 0.2]])) +@test !isequal(MAT.MatlabStructArray(["a"], [[0.1, 0.2]]), MAT.MatlabStructArray(["b"], [[0.1, 0.2]])) +@test isapprox(MAT.MatlabStructArray(["a"], [[0.1, 0.2]]), MAT.MatlabStructArray(["a"], [[0.1+eps(0.1), 0.2]])) +@test !isapprox(MAT.MatlabStructArray(["a"], [[0.1, 0.2]]), MAT.MatlabStructArray(["b"], [[0.1, 0.2]])) +@test !isapprox(MAT.MatlabStructArray(["a"], [[0.1, 0.2]]), MAT.MatlabStructArray(["a"], [[0.11, 0.2]])) + +# empty struct array constructor +s_arr = MAT.MatlabStructArray(["x", "y"], (0,1)) +@test s_arr["x"] == Matrix{Any}(undef, 0, 1) +@test s_arr["y"] == Matrix{Any}(undef, 0, 1) +@test MAT.MatlabStructArray(["a"]) == MAT.MatlabStructArray(["a"], (0,0)) + +# convert to Dict to support easy conversion to legacy read behavior +s_arr = MAT.MatlabStructArray(d_arr) d = Dict(s_arr) @test d isa Dict{String, Any} @test collect(keys(d)) == s_arr.names @test collect(values(d)) == s_arr.values - # iteration similar to Dict @test length(s_arr) == 2 @test collect(Dict(s_arr)) == collect(s_arr) # possibility to convert back to dict array via `Array` +s_arr = MAT.MatlabStructArray(d_arr) @test Array(s_arr) == d_arr +d_symbol = Array{Dict{Symbol,Any}}(MAT.MatlabStructArray(d_arr)) +@test d_symbol[2][:x] == d_arr[2]["x"] +@test Array(MAT.MatlabStructArray(d_symbol)) == d_arr # test error of unequal structs wrong_sarr = Dict{String, Any}[ diff --git a/test/v6/empty_cell_struct.mat b/test/v6/empty_cell_struct.mat new file mode 100644 index 0000000000000000000000000000000000000000..1316c56169e0d2023cb9d0c71e22b67d05ea48ee GIT binary patch literal 9248 zcmeHL-EI;=6g~^B-6B+4V^Y1Eq+W1Q5vbJ46-d!-Qs_oW?4546OCVW(0@TDesCPa_ zAHhfHV|eK!=#|dTITSV^r6wXeL&}*sbIzHw-=6tqXt_{5EIemVlRK;E&nS7tWv=i4}6SIP4#q%bw*(H zu(Q?mYP~H{QL5!p&MFMPBZliYj`e%It|DDxz-jf{i77+Md)7R({bC~!tY3zYbB?@5 zPv>Cg1f`3T>UfU~uPI+wj`j@*5vxODMLAMD8^@hhr6+!6;ZZva$MrzI*GyyK%_f~= zLHwrifO&fq=YGpnYo5#0{kZraZ^u7|_5QnPDge9it?} zoH4|V$WL)XUaq{1aY9B&I+_?q*pF))=q-#_zu#*_K{1#CF^KXx{-cBc{&?#(JN*yB zUqn$EsZ~{N8DxMT5srEJSK2WX2 z6a);2{3Ugkl9`G+n^Y%D+=A-M^?YAAL?MBGE7H%(+J&kGAnHo&4YGfX7frekeqNJq zkXM3#|EkWygF3UL1BbC4it-KPk2T^-x>qL1VUpoLq95jgoA3znA;T9sKR*03b*k~2 zZD0Q`Ui5#C;JrBt)8F?kzDJ2?dj0O1+ZVjlkvrFbHO?%XbZ*ufo_{K+=sy1wTVL>$ z@TGGdukAom%&`sr#hbL_lE%buG!{NDi${kt7ZIE{*&mXQ)GBTqoF`tkwU4nWKX#2i4(1jHb~3dA5j w3@|b50GCf9G5`Po literal 0 HcmV?d00001 diff --git a/test/v7.3/empty_cell_struct.mat b/test/v7.3/empty_cell_struct.mat new file mode 100644 index 0000000000000000000000000000000000000000..1316c56169e0d2023cb9d0c71e22b67d05ea48ee GIT binary patch literal 9248 zcmeHL-EI;=6g~^B-6B+4V^Y1Eq+W1Q5vbJ46-d!-Qs_oW?4546OCVW(0@TDesCPa_ zAHhfHV|eK!=#|dTITSV^r6wXeL&}*sbIzHw-=6tqXt_{5EIemVlRK;E&nS7tWv=i4}6SIP4#q%bw*(H zu(Q?mYP~H{QL5!p&MFMPBZliYj`e%It|DDxz-jf{i77+Md)7R({bC~!tY3zYbB?@5 zPv>Cg1f`3T>UfU~uPI+wj`j@*5vxODMLAMD8^@hhr6+!6;ZZva$MrzI*GyyK%_f~= zLHwrifO&fq=YGpnYo5#0{kZraZ^u7|_5QnPDge9it?} zoH4|V$WL)XUaq{1aY9B&I+_?q*pF))=q-#_zu#*_K{1#CF^KXx{-cBc{&?#(JN*yB zUqn$EsZ~{N8DxMT5srEJSK2WX2 z6a);2{3Ugkl9`G+n^Y%D+=A-M^?YAAL?MBGE7H%(+J&kGAnHo&4YGfX7frekeqNJq zkXM3#|EkWygF3UL1BbC4it-KPk2T^-x>qL1VUpoLq95jgoA3znA;T9sKR*03b*k~2 zZD0Q`Ui5#C;JrBt)8F?kzDJ2?dj0O1+ZVjlkvrFbHO?%XbZ*ufo_{K+=sy1wTVL>$ z@TGGdukAom%&`sr#hbL_lE%buG!{NDi${kt7ZIE{*&mXQ)GBTqo62x8Hb)&#N_TH+oNp<4LrR-AR}XXY*+t zyUlldovml<*qldU5*^~~QyqJwW9-aMFsNa$RS$x?U&9T5V-t6GUTtDL7)8?%gR1Xi z6&E#-mfLF!>*&8h;9TddRCru4vwI$_K-n%NXUTVNU!VLeTM3}gjADAm^n(2r&O9LxQ zeZCOGKcor@rhdyDKyf82Wfd~L(7dj18lh9?{FMV4k^Kp^h4NDo+&p{e*(8q8tdCsTDy!)HJTkKAEh%#x)F7{U1xps6lR|P zrM|BNclxTz;!H;;EB~hS4L-}#y>1@to4oVEml_Zn@65O4@M1RMem0f&G? nz#-rea0uKS1pdbTlKgkiqPooYOLS*JdzO1Xx~s^_bVu<6(EE}! delta 265 zcmca%{=j5{yso>CUxcHXiH?GEQEFmIYKlUBo|QsKMyY~hX}W@;nS!B(m8rRvk)?u> zfuY4lpF5092~v}_nA#XQHg9BNXVMg4fPfQBPzIDrgwl*q+GAp){bU8!7M4iHvgpYx zSUVUqCL6MeGBQqfXVcs`L4uK)haq8dH@m4W%pe%e0yH6k17ZxwG&UqY)8<6(Vy4Ln pToQ&LWgt+TS(OT+VcJ2;L7*V9C^5Gf!UmBCHV1O^^G}?x0s!zkES3NO diff --git a/test/v7/empty_cell_struct.mat b/test/v7/empty_cell_struct.mat new file mode 100644 index 0000000000000000000000000000000000000000..1316c56169e0d2023cb9d0c71e22b67d05ea48ee GIT binary patch literal 9248 zcmeHL-EI;=6g~^B-6B+4V^Y1Eq+W1Q5vbJ46-d!-Qs_oW?4546OCVW(0@TDesCPa_ zAHhfHV|eK!=#|dTITSV^r6wXeL&}*sbIzHw-=6tqXt_{5EIemVlRK;E&nS7tWv=i4}6SIP4#q%bw*(H zu(Q?mYP~H{QL5!p&MFMPBZliYj`e%It|DDxz-jf{i77+Md)7R({bC~!tY3zYbB?@5 zPv>Cg1f`3T>UfU~uPI+wj`j@*5vxODMLAMD8^@hhr6+!6;ZZva$MrzI*GyyK%_f~= zLHwrifO&fq=YGpnYo5#0{kZraZ^u7|_5QnPDge9it?} zoH4|V$WL)XUaq{1aY9B&I+_?q*pF))=q-#_zu#*_K{1#CF^KXx{-cBc{&?#(JN*yB zUqn$EsZ~{N8DxMT5srEJSK2WX2 z6a);2{3Ugkl9`G+n^Y%D+=A-M^?YAAL?MBGE7H%(+J&kGAnHo&4YGfX7frekeqNJq zkXM3#|EkWygF3UL1BbC4it-KPk2T^-x>qL1VUpoLq95jgoA3znA;T9sKR*03b*k~2 zZD0Q`Ui5#C;JrBt)8F?kzDJ2?dj0O1+ZVjlkvrFbHO?%XbZ*ufo_{K+=sy1wTVL>$ z@TGGdukAom%&`sr#hbL_lE%buG!{NDi${kt7ZIE{*&mXQ)GBTqo}aZCnOXwB$+8Z zV@OhC<49=zl-Mw_apuAs2N*QYF@+uGxH|d7gd-DpmM~hiF;#=~8RF3A*dW+5Q?iSf zr}2?af@x2HZ6}W>&q)U<2CE}XwIGc~I5bKwtZ`#YYfExwUSPo;!K)nNujDM`Y&3z5 J!RjPaEdb>>P`3a8 literal 0 HcmV?d00001 diff --git a/test/write.jl b/test/write.jl index c92e4c9..62246fd 100644 --- a/test/write.jl +++ b/test/write.jl @@ -154,3 +154,6 @@ sarr = Dict{String, Any}[ Dict("x"=>[5.0,6.0], "y"=>[]) ] test_write(Dict("s_array" => MAT.MatlabStructArray(sarr))) + +empty_sarr = MAT.MatlabStructArray(["a", "b", "c"]) +test_write(Dict("s_array" => empty_sarr)) \ No newline at end of file From 1332e8514db1e0a562a971970682d524a759d119 Mon Sep 17 00:00:00 2001 From: Matthijs Cox Date: Fri, 14 Nov 2025 10:37:24 +0100 Subject: [PATCH 11/19] MatlabStructArray support in MAT_v5 --- README.md | 17 +++++++++-------- src/MAT.jl | 1 + src/MAT_types.jl | 13 +++++++++---- src/MAT_v5.jl | 28 ++++++++++++++++------------ test/read.jl | 35 +++++++++++++---------------------- 5 files changed, 48 insertions(+), 46 deletions(-) diff --git a/README.md b/README.md index 7de9280..d909cb2 100644 --- a/README.md +++ b/README.md @@ -113,26 +113,27 @@ cell = {1×1 struct} ``` -Read and write behavior for struct arrays is different. For struct arrays (in .mat v7.3 files) we use the `MAT.MatlabStructArray` type. You can also write with MAT.jl using Dict arrays `AbstractArray{<:AbstractDict}` if all the Dicts have equal keys, which will automatically convert internally to `MAT.MatlabStructArray`. +Read and write behavior for struct arrays is different. For struct arrays we use the `MatlabStructArray` type. You can also write with MAT.jl using Dict arrays `AbstractArray{<:AbstractDict}` if all the Dicts have equal keys, which will automatically convert internally to `MatlabStructArray`. ```julia sarr = Dict{String, Any}[ Dict("x"=>1.0, "y"=>2.0), Dict("x"=>3.0, "y"=>4.0) ] -matwrite("matfile.mat", Dict("struct_array" => sarr)) - +matwrite("matfile.mat", Dict("s" => sarr)) +# which is the same as: +matwrite("matfile.mat", Dict("s" => MatlabStructArray(sarr))) # which is the same as: -matwrite("matfile.mat", Dict("struct_array" => MAT.MatlabStructArray(sarr))) +matwrite("matfile.mat", Dict("s" => MatlabStructArray(["x", "y"], [[1.0, 3.0], [3.0, 4.0]]))) ``` Now you'll find the following inside MATLAB: ```matlab >> load('matfile.mat') ->> struct_array +>> s -struct_array = +s = [2x1 struct, 576 bytes] x: 1 @@ -143,7 +144,7 @@ Note that when you read the file again, you'll find the `MAT.MatlabStructArray`, ```julia julia> sarr = matread("matfile.mat")["struct_array"] -MAT.MAT_types.MatlabStructArray{1} +MatlabStructArray{1} with 2 columns: "x": Any[1.0, 3.0] "y": Any[2.0, 4.0] @@ -159,7 +160,7 @@ julia> Array(sarr) ``` -Note that in MAT.jl v0.10 and older, or .mat versions before v7.3, will read struct arrays as a Dict with concatenated arrays in the fields/keys, which is equal to `Dict(sarr)`. +Note that before v0.11 MAT.jl will read struct arrays as a Dict with concatenated arrays in the fields/keys, which is equal to `Dict(sarr)`. ## Caveats diff --git a/src/MAT.jl b/src/MAT.jl index e4e6bd8..f194ef4 100644 --- a/src/MAT.jl +++ b/src/MAT.jl @@ -36,6 +36,7 @@ include("MAT_v4.jl") using .MAT_HDF5, .MAT_v5, .MAT_v4 export matopen, matread, matwrite, @read, @write +export MatlabStructArray # Open a MATLAB file const HDF5_HEADER = UInt8[0x89, 0x48, 0x44, 0x46, 0x0d, 0x0a, 0x1a, 0x0a] diff --git a/src/MAT_types.jl b/src/MAT_types.jl index 40f7820..a7bcbb9 100644 --- a/src/MAT_types.jl +++ b/src/MAT_types.jl @@ -34,13 +34,14 @@ module MAT_types struct MatlabStructArray{N} names::Vector{String} values::Vector{Array{Any,N}} - function MatlabStructArray(names::Vector{String}, values::Vector{Array{Any,N}}) where N + class::String + function MatlabStructArray(names::Vector{String}, values::Vector{Array{Any,N}}, class::String="") where N # call MatlabStructArray{N}() to avoid the check check_struct_array(names, values) - return new{N}(names, values) + return new{N}(names, values, class) end - function MatlabStructArray{N}(names::Vector{String}, values::Vector{Array{Any,N}}) where N - return new{N}(names, values) + function MatlabStructArray{N}(names::Vector{String}, values::Vector{Array{Any,N}}, class::String="") where N + return new{N}(names, values, class) end end @@ -80,6 +81,10 @@ module MAT_types function Base.show(io::IO, ::MIME"text/plain", arr::MatlabStructArray) summary(io, arr) + ncol = length(arr.values) + print(io, " with $(ncol) ") + col_word = ncol==1 ? "column" : "columns" + print(io, col_word, ":") for (k,v) in arr print(io, "\n \"$k\": $v") end diff --git a/src/MAT_v5.jl b/src/MAT_v5.jl index 7370dd4..b16ec57 100644 --- a/src/MAT_v5.jl +++ b/src/MAT_v5.jl @@ -28,6 +28,7 @@ module MAT_v5 using CodecZlib, BufferedStreams, HDF5, SparseArrays import Base: read, write, close +import ..MAT_types: MatlabStructArray round_uint8(data) = round.(UInt8, data) complex_array(a, b) = complex.(a, b) @@ -170,6 +171,8 @@ end function read_struct(f::IO, swap_bytes::Bool, dimensions::Vector{Int32}, is_object::Bool) if is_object class = String(read_element(f, swap_bytes, UInt8)) + else + class = "" end field_length = read_element(f, swap_bytes, Int32)[1] field_names = read_element(f, swap_bytes, UInt8) @@ -184,27 +187,28 @@ function read_struct(f::IO, swap_bytes::Bool, dimensions::Vector{Int32}, is_obje field_name_strings[i] = String(index == 0 ? sname : sname[1:index-1]) end - data = Dict{String, Any}() - sizehint!(data, n_fields+1) - if is_object - data["class"] = class - end - + local data if n_el == 1 # Read a single struct into a dict + data = Dict{String, Any}() + sizehint!(data, n_fields+1) + if is_object + data["class"] = class + end for field_name in field_name_strings data[field_name] = read_matrix(f, swap_bytes)[2] end else - # Read multiple structs into a dict of arrays - for field_name in field_name_strings - data[field_name] = Array{Any}(undef, dimensions...) - end + # Read empty or multiple structs + nfields = length(field_name_strings) + N = length(dimensions) + field_values = Array{Any, N}[Array{Any}(undef, dimensions...) for _ in 1:nfields] for i = 1:n_el - for field_name in field_name_strings - data[field_name][i] = read_matrix(f, swap_bytes)[2] + for field in 1:nfields + field_values[field][i] = read_matrix(f, swap_bytes)[2] end end + data = MatlabStructArray{N}(field_name_strings, field_values, class) end data diff --git a/test/read.jl b/test/read.jl index 619e554..0354b1b 100644 --- a/test/read.jl +++ b/test/read.jl @@ -107,13 +107,22 @@ for _format in ["v6", "v7", "v7.3"] "b" => [1.0 2.0], "c" => [1.0 2.0 3.0] ), - "s2" => Dict{String,Any}("a" => Any[1.0 2.0]) + "s2" => MAT.MatlabStructArray(["a"], [Any[1.0 2.0]]) ) - if _format == "v7.3" - result["s2"] = MAT.MatlabStructArray(["a"], [Any[1.0 2.0]]) - end check("struct.mat", result) + result = Dict( + "s00" => MAT.MatlabStructArray(["a", "b", "c"], (0,0)), + "s01" => MAT.MatlabStructArray(["a", "b", "c"], (0,1)), + "s10" => MAT.MatlabStructArray(["a", "b", "c"], (1,0)) + ) + check("empty_struct_arrays.mat", result) + + result = Dict{String,Any}( + "s" => Dict{String, Any}("c"=>Matrix{Any}(undef, 0, 0), "b"=>Matrix{Any}(undef, 0, 0), "a"=>Matrix{Any}(undef, 0, 0)), + ) + check("empty_cell_struct.mat", result) + result = Dict( "logical" => false, "logical_mat" => [ @@ -232,21 +241,3 @@ let objtestfile = "old_class.mat" @test "tc_old" in keys(vars) @test "foo" in keys(vars["tc_old"]) end - -let objtestfile = "empty_struct_arrays.mat" - folder = dirname(@__FILE__) - for _format in ["v6", "v7", "v7.3"] - vars = matread(joinpath(folder, _format, objtestfile)) - @test vars["s00"]["a"] == Matrix{Any}(undef, 0, 0) - @test vars["s10"]["a"] == Matrix{Any}(undef, 1, 0) - @test vars["s01"]["a"] == Matrix{Any}(undef, 0, 1) - end -end - -let objtestfile = "empty_cell_struct.mat" - folder = dirname(@__FILE__) - for _format in ["v6", "v7", "v7.3"] - vars = matread(joinpath(folder, _format, objtestfile)) - @test vars["s"]["a"] == Matrix{Any}(undef, 0, 0) - end -end From 5c32ee3c2cde9890a17b13b8502de36cb411d86f Mon Sep 17 00:00:00 2001 From: Matthijs Cox Date: Fri, 14 Nov 2025 11:13:05 +0100 Subject: [PATCH 12/19] better matwrite error messages --- README.md | 4 ++-- src/MAT.jl | 52 +++++++++++++++++++++------------------------------ test/write.jl | 9 +++++++++ 3 files changed, 32 insertions(+), 33 deletions(-) diff --git a/README.md b/README.md index d909cb2..e0df40c 100644 --- a/README.md +++ b/README.md @@ -124,7 +124,7 @@ matwrite("matfile.mat", Dict("s" => sarr)) # which is the same as: matwrite("matfile.mat", Dict("s" => MatlabStructArray(sarr))) # which is the same as: -matwrite("matfile.mat", Dict("s" => MatlabStructArray(["x", "y"], [[1.0, 3.0], [3.0, 4.0]]))) +matwrite("matfile.mat", Dict("s" => MatlabStructArray(["x", "y"], [[1.0, 3.0], [2.0, 4.0]]))) ``` Now you'll find the following inside MATLAB: @@ -140,7 +140,7 @@ x: 1 y: 2 ``` -Note that when you read the file again, you'll find the `MAT.MatlabStructArray`, which you can convert back to the Dict array with `Array`: +Note that when you read the file again, you'll find the `MatlabStructArray`, which you can convert back to the Dict array with `Array`: ```julia julia> sarr = matread("matfile.mat")["struct_array"] diff --git a/src/MAT.jl b/src/MAT.jl index f194ef4..999afbb 100644 --- a/src/MAT.jl +++ b/src/MAT.jl @@ -144,47 +144,37 @@ end # Write a dict to a MATLAB file """ - matwrite(filename, d::Dict; compress::Bool = false, version::String) + matwrite(filename, d::Dict; compress::Bool = false, version::String = "v7.3") Write a dictionary containing variable names as keys and values as values to a Matlab file, opening and closing it automatically. """ -function matwrite(filename::AbstractString, dict::AbstractDict{S, T}; compress::Bool = false, version::String ="") where {S, T} - +function matwrite(filename::AbstractString, dict::AbstractDict{S, T}; compress::Bool = false, version::String ="v7.3") where {S, T} if version == "v4" file = open(filename, "w") m = MAT_v4.Matlabv4File(file, false) - try - for (k, v) in dict - local kstring - try - kstring = ascii(convert(String, k)) - catch x - error("matwrite requires a Dict with ASCII keys") - end - write(m, kstring, v) - end - finally - close(file) - end - - else - + _write_dict(m, dict) + elseif version == "v7.3" file = matopen(filename, "w"; compress = compress) - try - for (k, v) in dict - local kstring - try - kstring = ascii(convert(String, k)) - catch x - error("matwrite requires a Dict with ASCII keys") - end - write(file, kstring, v) + _write_dict(file, dict) + else + error("writing for \"$(version)\" is not supported") + end +end + +function _write_dict(fileio, dict::AbstractDict) + try + for (k, v) in dict + local kstring + try + kstring = ascii(convert(String, k)) + catch x + error("matwrite requires a Dict with ASCII keys") end - finally - close(file) + write(fileio, kstring, v) end - + finally + close(fileio) end end diff --git a/test/write.jl b/test/write.jl index 62246fd..69f74fe 100644 --- a/test/write.jl +++ b/test/write.jl @@ -34,6 +34,15 @@ function test_compression_effective(data) end end +@testset "write error messages" begin + msg = "writing for \"v7\" is not supported" + @test_throws ErrorException(msg) matwrite(tmpfile, Dict("s" => 1); version="v7") + + msg = "matwrite requires a Dict with ASCII keys" + @test_throws ErrorException(msg) matwrite(tmpfile, Dict(:s => 1)) + @test_throws ErrorException(msg) matwrite(tmpfile, Dict(:s => 1); version="v4") +end + test_write(Dict( "int8" => Int8(1), "uint8" => UInt8(1), From 41db099c3351ee612bc6e145fcb6e1fec5f016a6 Mon Sep 17 00:00:00 2001 From: Matthijs Cox Date: Fri, 14 Nov 2025 14:16:25 +0100 Subject: [PATCH 13/19] Old class read-write support via MatlabClassObject --- Project.toml | 2 +- src/MAT.jl | 2 +- src/MAT_HDF5.jl | 55 +++++++++++++--- src/MAT_types.jl | 79 +++++++++++++++++++++-- src/MAT_v5.jl | 8 +-- test/read.jl | 19 +++++- test/types.jl | 116 +++++++++++++++++++--------------- test/v7.3/old_class_array.mat | Bin 0 -> 5048 bytes test/v7/old_class_array.mat | Bin 0 -> 239 bytes test/write.jl | 18 ++++-- 10 files changed, 220 insertions(+), 79 deletions(-) create mode 100644 test/v7.3/old_class_array.mat create mode 100644 test/v7/old_class_array.mat diff --git a/Project.toml b/Project.toml index fba705b..a500550 100644 --- a/Project.toml +++ b/Project.toml @@ -1,6 +1,6 @@ name = "MAT" uuid = "23992714-dd62-5051-b70f-ba57cb901cac" -version = "0.10.7" +version = "0.11.0" [deps] BufferedStreams = "e1450e63-4bb3-523b-b2a4-4ffa8c0fd77d" diff --git a/src/MAT.jl b/src/MAT.jl index 999afbb..b38126c 100644 --- a/src/MAT.jl +++ b/src/MAT.jl @@ -36,7 +36,7 @@ include("MAT_v4.jl") using .MAT_HDF5, .MAT_v5, .MAT_v4 export matopen, matread, matwrite, @read, @write -export MatlabStructArray +export MatlabStructArray, MatlabClassObject # Open a MATLAB file const HDF5_HEADER = UInt8[0x89, 0x48, 0x44, 0x46, 0x0d, 0x0a, 0x1a, 0x0a] diff --git a/src/MAT_HDF5.jl b/src/MAT_HDF5.jl index 0b6420a..6285532 100644 --- a/src/MAT_HDF5.jl +++ b/src/MAT_HDF5.jl @@ -32,7 +32,7 @@ using HDF5, SparseArrays import Base: names, read, write, close import HDF5: Reference -import ..MAT_types: MatlabStructArray, StructArrayField, convert_struct_array +import ..MAT_types: MatlabStructArray, StructArrayField, convert_struct_array, MatlabClassObject const HDF5Parent = Union{HDF5.File, HDF5.Group} const HDF5BitsOrBool = Union{HDF5.BitsType,Bool} @@ -119,6 +119,7 @@ const name_type_attr_matlab = "MATLAB_class" const empty_attr_matlab = "MATLAB_empty" const sparse_attr_matlab = "MATLAB_sparse" const int_decode_attr_matlab = "MATLAB_int_decode" +const object_decode_attr_matlab = "MATLAB_object_decode" ### Reading function read_complex(dtype::HDF5.Datatype, dset::HDF5.Dataset, ::Type{T}) where T @@ -252,19 +253,31 @@ end # reading a struct, struct array, or sparse matrix function m_read(g::HDF5.Group) mattype = read_attribute(g, name_type_attr_matlab) + is_object = false if mattype != "struct" + attr = attributes(g) # Check if this is a sparse matrix. - if haskey(attributes(g), sparse_attr_matlab) + if haskey(attr, sparse_attr_matlab) return read_sparse_matrix(g, mattype) elseif mattype == "function_handle" @warn "MATLAB $mattype values are currently not supported" return missing else - @warn "Unknown non-struct group of type $mattype detected; attempting to read as struct" + if haskey(attr, object_decode_attr_matlab) && read_attribute(g, object_decode_attr_matlab)==2 + # I think this means it's an old object class similar to mXOBJECT_CLASS in MAT_v5 + is_object = true + else + @warn "Unknown non-struct group of type $mattype detected; attempting to read as struct" + end end end + if is_object + class = mattype + else + class = "" + end s = read_struct_as_dict(g) - out = convert_struct_array(s) + out = convert_struct_array(s, class) return out end @@ -559,8 +572,13 @@ function m_write(mfile::MatlabHDF5File, parent::HDF5Parent, name::String, arr::M else g = create_group(parent, name) try - write_attribute(g, name_type_attr_matlab, "struct") - write_attribute(g, "MATLAB_fields", HDF5.VLen(arr.names)) + if isempty(arr.class) + write_attribute(g, name_type_attr_matlab, "struct") + write_attribute(g, "MATLAB_fields", HDF5.VLen(arr.names)) + else + write_attribute(g, name_type_attr_matlab, arr.class) + write_attribute(g, object_decode_attr_matlab, UInt32(2)) + end for (fieldname, field_values) in arr refs = _write_references!(mfile, parent, field_values) dset, dtype = create_dataset(g, fieldname, refs) @@ -590,14 +608,31 @@ function check_struct_keys(k::Vector) asckeys end +function m_write(mfile::MatlabHDF5File, parent::HDF5Parent, name::String, obj::MatlabClassObject) + g = create_group(parent, name) + try + write_attribute(g, name_type_attr_matlab, obj.class) + write_attribute(g, object_decode_attr_matlab, UInt32(2)) + for (ki, vi) in zip(keys(obj), values(obj)) + m_write(mfile, g, ki, vi) + end + finally + close(g) + end +end + # Write a struct from arrays of keys and values function m_write(mfile::MatlabHDF5File, parent::HDF5Parent, name::String, k::Vector{String}, v::Vector) g = create_group(parent, name) - write_attribute(g, name_type_attr_matlab, "struct") - for i = 1:length(k) - m_write(mfile, g, k[i], v[i]) + try + write_attribute(g, name_type_attr_matlab, "struct") + for i = 1:length(k) + m_write(mfile, g, k[i], v[i]) + end + write_attribute(g, "MATLAB_fields", HDF5.VLen(k)) + finally + close(g) end - write_attribute(g, "MATLAB_fields", HDF5.VLen(k)) end # Write Associative as a struct diff --git a/src/MAT_types.jl b/src/MAT_types.jl index a7bcbb9..b04bc81 100644 --- a/src/MAT_types.jl +++ b/src/MAT_types.jl @@ -29,8 +29,43 @@ module MAT_types export MatlabStructArray, StructArrayField, convert_struct_array + export MatlabClassObject # struct arrays are stored as columns per field name + """ + MatlabStructArray{N}( + names::Vector{String}, + values::Vector{Array{Any,N}}, + class::String = "", + ) + + Data structure to store matlab struct arrays, which stores the field names separate from the field values. + The field values are stored as columns of `Array{Any,N}` per Matlab field, which is how MAT files store these structures. + + These are distinct from cell arrays of structs, + which are handled as in MAT.jl as `Array{Any,N}` with `Dict{String,Any}` inside, + for example `Any[Dict("x"=>1), Dict("x"=>2)]`. + + Old class object arrays can be handled by providing a non-empty class name. + + # Example + + ```julia + using MAT + + s_arr = MatlabStructArray(["a", "b"], [[1, 2],["foo", 5]]) + + # write-read + matwrite("matfile.mat", Dict("struct_array" => s_arr)) + read_s_arr = matread("matfile.mat")["struct_array"] + + # convert to Dict Array + dict_array = Array{Dict{String,Any}}(s_arr) + + # convert to Dict (with arrays as fields) + dict = Dict{String,Any}(s_arr) + ``` + """ struct MatlabStructArray{N} names::Vector{String} values::Vector{Array{Any,N}} @@ -56,8 +91,8 @@ module MAT_types end end - function MatlabStructArray(names::AbstractVector{<:AbstractString}, values::AbstractArray{<:AbstractArray{T,N}}) where {T,N} - MatlabStructArray{N}(string.(names), Vector{Array{Any,N}}(values)) + function MatlabStructArray(names::AbstractVector{<:AbstractString}, values::AbstractArray{<:AbstractArray{T,N}}, class="") where {T,N} + MatlabStructArray{N}(string.(names), Vector{Array{Any,N}}(values), string(class)) end # empty array @@ -91,7 +126,7 @@ module MAT_types end function Base.:(==)(m1::MatlabStructArray{N},m2::MatlabStructArray{N}) where N - return isequal(m1.names, m2.names) && isequal(m1.values, m2.values) + return isequal(m1.names, m2.names) && isequal(m1.values, m2.values) && isequal(m1.class, m2.class) end function Base.isapprox(m1::MatlabStructArray,m2::MatlabStructArray; kwargs...) @@ -156,17 +191,47 @@ module MAT_types end dimension(::StructArrayField{N}) where N = N - function convert_struct_array(d::Dict{String, Any}) + """ + MatlabClassObject( + d::Dict{String, Any}, + class::String, + ) <: AbstractDict{String, Any} + + Type to store old class objects. Inside MATLAB a class named \"TestClassOld\" would be defined within `@TestClassOld` folders. + + If you want to write these objects you have to make sure the keys in the Dict match the class defined properties/fields. + """ + struct MatlabClassObject <: AbstractDict{String, Any} + d::Dict{String, Any} + class::String + end + + Base.eltype(::Type{MatlabClassObject}) = Pair{String, Any} + Base.length(m::MatlabClassObject) = length(m.d) + Base.keys(m::MatlabClassObject) = keys(m.d) + Base.values(m::MatlabClassObject) = values(m.d) + Base.getindex(m::MatlabClassObject, i) = getindex(m.d, i) + Base.setindex!(m::MatlabClassObject, v, k) = setindex!(m.d, v, k) + Base.iterate(m::MatlabClassObject, i) = iterate(m.d, i) + Base.iterate(m::MatlabClassObject) = iterate(m.d) + Base.haskey(m::MatlabClassObject, k) = haskey(m.d, k) + Base.get(m::MatlabClassObject, k, default) = get(m.d, k, default) + + function convert_struct_array(d::Dict{String, Any}, class::String="") # there is no possibility of having cell arrays mixed with struct arrays (afaik) field_values = first(values(d)) if field_values isa StructArrayField return MatlabStructArray{dimension(field_values)}( collect(keys(d)), [arr.values for arr in values(d)], + class, ) else - return d + if isempty(class) + return d + else + return MatlabClassObject(d, class) + end end - end - + end end \ No newline at end of file diff --git a/src/MAT_v5.jl b/src/MAT_v5.jl index b16ec57..0c12d01 100644 --- a/src/MAT_v5.jl +++ b/src/MAT_v5.jl @@ -28,7 +28,7 @@ module MAT_v5 using CodecZlib, BufferedStreams, HDF5, SparseArrays import Base: read, write, close -import ..MAT_types: MatlabStructArray +import ..MAT_types: MatlabStructArray, MatlabClassObject round_uint8(data) = round.(UInt8, data) complex_array(a, b) = complex.(a, b) @@ -192,12 +192,12 @@ function read_struct(f::IO, swap_bytes::Bool, dimensions::Vector{Int32}, is_obje # Read a single struct into a dict data = Dict{String, Any}() sizehint!(data, n_fields+1) - if is_object - data["class"] = class - end for field_name in field_name_strings data[field_name] = read_matrix(f, swap_bytes)[2] end + if is_object + data = MatlabClassObject(data, class) + end else # Read empty or multiple structs nfields = length(field_name_strings) diff --git a/test/read.jl b/test/read.jl index 0354b1b..410a10e 100644 --- a/test/read.jl +++ b/test/read.jl @@ -206,7 +206,7 @@ let objtestfile = "obj.mat" @test key in keys(vars) end # check if class name was read correctly - @test vars["A"]["class"] == "Assoc" + @test vars["A"].class == "Assoc" end # test reading of a Matlab figure @@ -240,4 +240,21 @@ let objtestfile = "old_class.mat" vars = matread(joinpath(dirname(@__FILE__), "v7.3", objtestfile)) @test "tc_old" in keys(vars) @test "foo" in keys(vars["tc_old"]) + @test vars["tc_old"].class == "TestClassOld" end + +let objtestfile = "old_class_array.mat" + vars = matread(joinpath(dirname(@__FILE__), "v7.3", objtestfile)) + c_arr = vars["class_arr"] + @test c_arr isa MatlabStructArray + @test c_arr.class == "TestClassOld" + @test c_arr["foo"] == Any[5.0 "test"] + + vars = matread(joinpath(dirname(@__FILE__), "v7", objtestfile)) + c_arr = vars["class_arr"] + @test c_arr isa MatlabStructArray + @test c_arr.class == "TestClassOld" + @test c_arr["foo"] == Any[5.0 "test"] +end + + diff --git a/test/types.jl b/test/types.jl index 4ca9c4e..c738002 100644 --- a/test/types.jl +++ b/test/types.jl @@ -1,52 +1,68 @@ using MAT, Test -# MatlabStructArray construction from dict array -d_arr = Dict{String, Any}[ - Dict("x"=>[1.0,2.0], SubString("y")=>[3.0,4.0]), - Dict("x"=>[5.0,6.0], "y"=>[]) -] -s_arr = MAT.MatlabStructArray(d_arr) -@test s_arr["y"][2] == d_arr[2]["y"] -@test s_arr["x"][1] == d_arr[1]["x"] - -# constructor errors to protect the user -@test_throws ErrorException MAT.MatlabStructArray(["a", "b"], [[]]) -@test_throws ErrorException MAT.MatlabStructArray(["a", "b"], [[],[0.1, 0.2]]) - -# equality checks -@test isequal(MAT.MatlabStructArray(["a"], [[0.1, 0.2]]), MAT.MatlabStructArray(["a"], [[0.1, 0.2]])) -@test !isequal(MAT.MatlabStructArray(["a"], [[0.1, 0.2]]), MAT.MatlabStructArray(["b"], [[0.1, 0.2]])) -@test isapprox(MAT.MatlabStructArray(["a"], [[0.1, 0.2]]), MAT.MatlabStructArray(["a"], [[0.1+eps(0.1), 0.2]])) -@test !isapprox(MAT.MatlabStructArray(["a"], [[0.1, 0.2]]), MAT.MatlabStructArray(["b"], [[0.1, 0.2]])) -@test !isapprox(MAT.MatlabStructArray(["a"], [[0.1, 0.2]]), MAT.MatlabStructArray(["a"], [[0.11, 0.2]])) - -# empty struct array constructor -s_arr = MAT.MatlabStructArray(["x", "y"], (0,1)) -@test s_arr["x"] == Matrix{Any}(undef, 0, 1) -@test s_arr["y"] == Matrix{Any}(undef, 0, 1) -@test MAT.MatlabStructArray(["a"]) == MAT.MatlabStructArray(["a"], (0,0)) - -# convert to Dict to support easy conversion to legacy read behavior -s_arr = MAT.MatlabStructArray(d_arr) -d = Dict(s_arr) -@test d isa Dict{String, Any} -@test collect(keys(d)) == s_arr.names -@test collect(values(d)) == s_arr.values -# iteration similar to Dict -@test length(s_arr) == 2 -@test collect(Dict(s_arr)) == collect(s_arr) - -# possibility to convert back to dict array via `Array` -s_arr = MAT.MatlabStructArray(d_arr) -@test Array(s_arr) == d_arr -d_symbol = Array{Dict{Symbol,Any}}(MAT.MatlabStructArray(d_arr)) -@test d_symbol[2][:x] == d_arr[2]["x"] -@test Array(MAT.MatlabStructArray(d_symbol)) == d_arr - -# test error of unequal structs -wrong_sarr = Dict{String, Any}[ - Dict("x"=>[1.0,2.0], "y"=>[3.0,4.0]), - Dict("x"=>[5.0,6.0]) -] -msg = "Cannot convert Dict array to MatlabStructArray. All elements must share identical field names" -@test_throws ErrorException(msg) MAT.MatlabStructArray(wrong_sarr) \ No newline at end of file +@testset "MatlabStructArray" begin + d_arr = Dict{String, Any}[ + Dict("x"=>[1.0,2.0], SubString("y")=>[3.0,4.0]), + Dict("x"=>[5.0,6.0], "y"=>[]) + ] + s_arr = MatlabStructArray(d_arr) + @test s_arr["y"][2] == d_arr[2]["y"] + @test s_arr["x"][1] == d_arr[1]["x"] + + # constructor errors to protect the user + @test_throws ErrorException MatlabStructArray(["a", "b"], [[]]) + @test_throws ErrorException MatlabStructArray(["a", "b"], [[],[0.1, 0.2]]) + + # equality checks + @test isequal(MatlabStructArray(["a"], [[0.1, 0.2]]), MatlabStructArray(["a"], [[0.1, 0.2]])) + @test !isequal(MatlabStructArray(["a"], [[0.1, 0.2]]), MatlabStructArray(["a"], [[0.1, 0.2]], "TestClass")) + @test !isequal(MatlabStructArray(["a"], [[0.1, 0.2]]), MatlabStructArray(["b"], [[0.1, 0.2]])) + @test isapprox(MatlabStructArray(["a"], [[0.1, 0.2]]), MatlabStructArray(["a"], [[0.1+eps(0.1), 0.2]])) + @test !isapprox(MatlabStructArray(["a"], [[0.1, 0.2]]), MatlabStructArray(["b"], [[0.1, 0.2]])) + @test !isapprox(MatlabStructArray(["a"], [[0.1, 0.2]]), MatlabStructArray(["a"], [[0.11, 0.2]])) + + # empty struct array constructor + s_arr = MatlabStructArray(["x", "y"], (0,1)) + @test s_arr["x"] == Matrix{Any}(undef, 0, 1) + @test s_arr["y"] == Matrix{Any}(undef, 0, 1) + @test MatlabStructArray(["a"]) == MatlabStructArray(["a"], (0,0)) + + # convert to Dict to support easy conversion to legacy read behavior + s_arr = MatlabStructArray(d_arr) + d = Dict(s_arr) + @test d isa Dict{String, Any} + @test collect(keys(d)) == s_arr.names + @test collect(values(d)) == s_arr.values + # iteration similar to Dict + @test length(s_arr) == 2 + @test collect(Dict(s_arr)) == collect(s_arr) + + # possibility to convert back to dict array via `Array` + s_arr = MatlabStructArray(d_arr) + @test Array(s_arr) == d_arr + d_symbol = Array{Dict{Symbol,Any}}(MatlabStructArray(d_arr)) + @test d_symbol[2][:x] == d_arr[2]["x"] + @test Array(MatlabStructArray(d_symbol)) == d_arr + + # test error of unequal structs + wrong_sarr = Dict{String, Any}[ + Dict("x"=>[1.0,2.0], "y"=>[3.0,4.0]), + Dict("x"=>[5.0,6.0]) + ] + msg = "Cannot convert Dict array to MatlabStructArray. All elements must share identical field names" + @test_throws ErrorException(msg) MatlabStructArray(wrong_sarr) +end + +@testset "MatlabClassObject" begin + d = Dict{String,Any}("a" => 5) + obj = MatlabClassObject(d, "TestClassOld") + @test keys(obj) == keys(d) + @test values(obj) == values(d) + @test collect(obj) == collect(d) + @test obj["a"] == d["a"] + @test haskey(obj, "a") + @test get(obj, "b", "default") == "default" + + obj["b"] = 7 + @test obj["b"] == 7 +end \ No newline at end of file diff --git a/test/v7.3/old_class_array.mat b/test/v7.3/old_class_array.mat new file mode 100644 index 0000000000000000000000000000000000000000..fbbde58a0e5d6d61b546c96257c6c73887bc14aa GIT binary patch literal 5048 zcmeHLPfrt35T9*9R%o^OClF6B;lcr-#a6HvTltfc2sGM6Z)MqT)0)yH?P}sTz>|Ii zj~+ex5j^@4Jo+v0&6^pz-J<+4!2o@P%$wPndGF2p&CG5$i#DTvCha#hfaw zuIE(OZFqZ*%FL(JYF-^TKt^n1Q>H`ueH*LmuDm_VdI6 z?C`{5dcgDx+gpHt0Mjg=WdBjU%8Ru%(3-NIP&R{~qcpQcFr?Qi0;RQL84vuLPpz1K zXPhw$Zp-O*t4^oGf|-t2@6I5yXe^83DXFD1B8=Y@<9B+J9DHU!C*k+UZBiWKmjiaq zNEE+ZW#NG9nuMe7`(`S5Ey;s((wAG?s}{tL>hJ6rITHV6=J#)sT=1#l{dy(l8`#3x8o!h3fIUG+mwtHZ0x{h1KSdx~2NZkz#Q7)A9iHqmb(u=up1qIRrjMoA zfyEWUvPd9pRA^lHIedb3ifTf^>|j)LD34h&V`#G<)IV2Pnsn}{o-*wu4yXiVDB}*K?PpXq~ zUKQY1J;GfdZf=<8Mh$BI+ufFjcoVxZV^_+R zixu6J5oZVy+5ZB)Xf8;;O4g7!)T6j8vq3a2BdjR?qI@_XDM#tte~U)|9&{lvp3f5x z+=J}LZ8+wYIspaa&(aYNnFH}aZCnW4(Jd)#Z zhVjT59)SZ~PmP%jg{M4YdGut?kw=ey99g8a!fJvAzd-vEvFR4gPyQ4KDg25&kY?a? x*6Z|n_SMgslbBV0-`H@2{V-3$Zpnj1FMdUAc#-UQNuA-zREE1O49p97+W{%EOlklC literal 0 HcmV?d00001 diff --git a/test/write.jl b/test/write.jl index 69f74fe..3cab705 100644 --- a/test/write.jl +++ b/test/write.jl @@ -155,14 +155,22 @@ sarr = Dict{String, Any}[ sarr = reshape(sarr, 1, 2) matwrite(tmpfile, Dict("s_array" => sarr)) read_sarr = matread(tmpfile)["s_array"] -@test read_sarr isa MAT.MatlabStructArray -@test read_sarr["y"][2] isa MAT.MatlabStructArray +@test read_sarr isa MatlabStructArray +@test read_sarr["y"][2] isa MatlabStructArray sarr = Dict{String, Any}[ Dict("x"=>[1.0,2.0], SubString("y")=>[3.0,4.0]), Dict("x"=>[5.0,6.0], "y"=>[]) ] -test_write(Dict("s_array" => MAT.MatlabStructArray(sarr))) +test_write(Dict("s_array" => MatlabStructArray(sarr))) -empty_sarr = MAT.MatlabStructArray(["a", "b", "c"]) -test_write(Dict("s_array" => empty_sarr)) \ No newline at end of file +empty_sarr = MatlabStructArray(["a", "b", "c"]) +test_write(Dict("s_array" => empty_sarr)) + +# old matlab class object array +carr = MatlabStructArray(["foo"], [[5, "bar"]], "TestClassOld") +test_write(Dict("class_array" => carr)) + +d = Dict{String,Any}("foo" => 5) +obj = MatlabClassObject(d, "TestClassOld") +test_write(Dict("tc_old" => obj)) \ No newline at end of file From 0a367dde96d7baad9705849d12cc228f64a36312 Mon Sep 17 00:00:00 2001 From: Matthijs Cox Date: Fri, 14 Nov 2025 15:04:12 +0100 Subject: [PATCH 14/19] Array{MatlabClassObject} support --- README.md | 76 ------------------------- docs/make.jl | 2 +- docs/src/index.md | 62 ++++++++++++++++++++ docs/src/object_arrays.md | 117 ++++++++++++++++++++++++++++++++++++++ src/MAT_HDF5.jl | 4 ++ src/MAT_types.jl | 18 ++++-- test/types.jl | 7 +++ test/write.jl | 7 ++- 8 files changed, 211 insertions(+), 82 deletions(-) create mode 100644 docs/src/object_arrays.md diff --git a/README.md b/README.md index e0df40c..632e012 100644 --- a/README.md +++ b/README.md @@ -86,82 +86,6 @@ end close(file) ``` -## Cell versus struct array writing - -Cell arrays are written for Any arrays `Array{Any}` or any other unsupported element type: - -```julia -sarr = Any[ - Dict("x"=>1.0, "y"=>2.0), - Dict("x"=>3.0, "y"=>4.0) -] -matwrite("matfile.mat", Dict("cell" => sarr)) - -``` - -Inside MATLAB you will find: - -```matlab ->> load('matfile.mat') ->> cell - -cell = - - 2×1 cell array - - {1×1 struct} - {1×1 struct} -``` - -Read and write behavior for struct arrays is different. For struct arrays we use the `MatlabStructArray` type. You can also write with MAT.jl using Dict arrays `AbstractArray{<:AbstractDict}` if all the Dicts have equal keys, which will automatically convert internally to `MatlabStructArray`. - -```julia -sarr = Dict{String, Any}[ - Dict("x"=>1.0, "y"=>2.0), - Dict("x"=>3.0, "y"=>4.0) -] -matwrite("matfile.mat", Dict("s" => sarr)) -# which is the same as: -matwrite("matfile.mat", Dict("s" => MatlabStructArray(sarr))) -# which is the same as: -matwrite("matfile.mat", Dict("s" => MatlabStructArray(["x", "y"], [[1.0, 3.0], [2.0, 4.0]]))) -``` - -Now you'll find the following inside MATLAB: - -```matlab ->> load('matfile.mat') ->> s - -s = - -[2x1 struct, 576 bytes] -x: 1 -y: 2 -``` - -Note that when you read the file again, you'll find the `MatlabStructArray`, which you can convert back to the Dict array with `Array`: - -```julia -julia> sarr = matread("matfile.mat")["struct_array"] -MatlabStructArray{1} with 2 columns: - "x": Any[1.0, 3.0] - "y": Any[2.0, 4.0] - -julia> sarr["x"] -2-element Vector{Any}: - 1.0 - 3.0 - -julia> Array(sarr) -2-element Vector{Dict{String, Any}}: - Dict("x" => 1.0, "y" => 2.0) - Dict("x" => 3.0, "y" => 4.0) - -``` - -Note that before v0.11 MAT.jl will read struct arrays as a Dict with concatenated arrays in the fields/keys, which is equal to `Dict(sarr)`. - ## Caveats * All files are written in MATLAB v7.3 format by default. diff --git a/docs/make.jl b/docs/make.jl index 669323e..4ec76f4 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -61,8 +61,8 @@ makedocs(; format, pages = [ "Home" => "index.md", + "Object Arrays" => "object_arrays.md", "Methods" => "methods.md", -# "Examples" => pages("examples") ], warnonly = [:missing_docs,], ) diff --git a/docs/src/index.md b/docs/src/index.md index 57355d7..d3169ca 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -10,3 +10,65 @@ This Julia package [(MAT.jl)](https://github.com/JuliaIO/MAT.jl) provides tools for reading and writing MATLAB format data files in Julia. + +## Basic Usage + +To read a single variable from a MAT file (compressed files are detected and handled automatically): + +```julia +using MAT +file = matopen("matfile.mat") +read(file, "varname") # note that this does NOT introduce a variable ``varname`` into scope +close(file) +``` + +To write a variable to a MAT file: + +```julia +file = matopen("matfile.mat", "w") +write(file, "varname", variable) +close(file) +``` + +To read all variables from a MAT file as a Dict: + +```julia +vars = matread("matfile.mat") +``` + +To write a Dict to a MAT file, using its keys as variable names. +The `compress` argument is optional, and compression is off by default: + +```julia +matwrite("matfile.mat", Dict( + "myvar1" => 0, + "myvar2" => 1 +); compress = true) +``` + +To write in MATLAB v4 format: + +```julia +matwrite("matfile.mat", Dict( + "myvar1" => 0, + "myvar2" => 1 +);version="v4") +``` + +To get a list of variable names in a MAT file: + +```julia +file = matopen("matfile.mat") +varnames = keys(file) +close(file) +``` + +To check for the presence of a variable name in a MAT file: + +```julia +file = matopen("matfile.mat") +if haskey(file, "variable") + # something +end +close(file) +``` diff --git a/docs/src/object_arrays.md b/docs/src/object_arrays.md new file mode 100644 index 0000000..df70188 --- /dev/null +++ b/docs/src/object_arrays.md @@ -0,0 +1,117 @@ +# Objects and struct arrays + +To better handle special cases we have these types since MAT 0.11: +* [`MatlabStructArray`](@ref MatlabStructArray) +* [`MatlabClassObject`](@ref MatlabClassObject) + +## Struct arrays vs Cell arrays + +Cell arrays are written for `Array{Any}` or any other unsupported element type: + +```julia +sarr = Any[ + Dict("x"=>1.0, "y"=>2.0), + Dict("x"=>3.0, "y"=>4.0) +] +matwrite("matfile.mat", Dict("cell" => sarr)) + +``` + +Inside MATLAB you will find: + +```matlab +>> load('matfile.mat') +>> cell + +cell = + + 2×1 cell array + + {1×1 struct} + {1×1 struct} +``` + +Read and write behavior for struct arrays is different. For struct arrays we use the `MatlabStructArray` type. You can also write with MAT.jl using Dict arrays `AbstractArray{<:AbstractDict}` if all the Dicts have equal keys, which will automatically convert internally to `MatlabStructArray`. + +```julia +sarr = Dict{String, Any}[ + Dict("x"=>1.0, "y"=>2.0), + Dict("x"=>3.0, "y"=>4.0) +] +matwrite("matfile.mat", Dict("s" => sarr)) +# which is the same as: +matwrite("matfile.mat", Dict("s" => MatlabStructArray(sarr))) +# which is the same as: +matwrite("matfile.mat", Dict("s" => MatlabStructArray(["x", "y"], [[1.0, 3.0], [2.0, 4.0]]))) +``` + +Now you'll find the following inside MATLAB: + +```matlab +>> load('matfile.mat') +>> s + +s = + +[2x1 struct, 576 bytes] +x: 1 +y: 2 +``` + +Note that when you read the file again, you'll find the `MatlabStructArray`, which you can convert back to the Dict array with `Array`: + +```julia +julia> sarr = matread("matfile.mat")["struct_array"] +MatlabStructArray{1} with 2 columns: + "x": Any[1.0, 3.0] + "y": Any[2.0, 4.0] + +julia> sarr["x"] +2-element Vector{Any}: + 1.0 + 3.0 + +julia> Array(sarr) +2-element Vector{Dict{String, Any}}: + Dict("x" => 1.0, "y" => 2.0) + Dict("x" => 3.0, "y" => 4.0) + +``` + +Note that before v0.11 MAT.jl will read struct arrays as a Dict with concatenated arrays in the fields/keys, which is equal to `Dict(sarr)`. + +## Object Arrays + +You can write an old class object with the `MatlabClassObject` and arrays of objects with `MatlabStructArray` by providing the class name. These are also the types you obtain when you read files. + +Write a single class object: +```julia +d = Dict("foo" => 5.0) +obj = MatlabClassObject(d, "TestClassOld") +matwrite("matfile.mat", Dict("tc_old" => obj)) +``` + +A class object array +```julia +class_array = MatlabStructArray(["foo"], [[5.0, "bar"]], "TestClassOld") +matwrite("matfile.mat", Dict("class_array" => class_array)) +``` + +Also a class object array, but will be converted to `MatlabStructArray` internally: +```julia +class_array = MatlabClassObject[ + MatlabClassObject(Dict("foo" => 5.0), "TestClassOld"), + MatlabClassObject(Dict("foo" => "bar"), "TestClassOld") +] +matwrite("matfile.mat", Dict("class_array" => class_array)) +``` + +A cell array: +```julia +cell_array = Any[ + MatlabClassObject(Dict("foo" => 5.0), "TestClassOld"), + MatlabClassObject(Dict("a" => "bar"), "AnotherClass") +] +matwrite("matfile.mat", Dict("cell_array" => cell_array)) +``` + diff --git a/src/MAT_HDF5.jl b/src/MAT_HDF5.jl index 6285532..abf8ad3 100644 --- a/src/MAT_HDF5.jl +++ b/src/MAT_HDF5.jl @@ -608,6 +608,10 @@ function check_struct_keys(k::Vector) asckeys end +function m_write(mfile::MatlabHDF5File, parent::HDF5Parent, name::String, arr::AbstractArray{MatlabClassObject}) + m_write(mfile, parent, name, MatlabStructArray(arr)) +end + function m_write(mfile::MatlabHDF5File, parent::HDF5Parent, name::String, obj::MatlabClassObject) g = create_group(parent, name) try diff --git a/src/MAT_types.jl b/src/MAT_types.jl index b04bc81..b51c1b2 100644 --- a/src/MAT_types.jl +++ b/src/MAT_types.jl @@ -147,11 +147,12 @@ module MAT_types end # convert Dict array to MatlabStructArray - function MatlabStructArray(arr::AbstractArray{<:AbstractDict, N}) where N - first_keys = keys(first(arr)) + function MatlabStructArray(arr::AbstractArray{<:AbstractDict, N}, class::String="") where N + first_dict, remaining_dicts = Iterators.peel(arr) + first_keys = keys(first_dict) field_names = string.(first_keys) # Ensure same field set for all elements - for d in arr + for d in remaining_dicts if !issetequal(keys(d), first_keys) error("Cannot convert Dict array to MatlabStructArray. All elements must share identical field names") end @@ -164,7 +165,7 @@ module MAT_types end field_values[idx] = this_field_values end - return MatlabStructArray{N}(field_names, field_values) + return MatlabStructArray{N}(field_names, field_values, class) end function Base.Dict(arr::MatlabStructArray) @@ -217,6 +218,15 @@ module MAT_types Base.haskey(m::MatlabClassObject, k) = haskey(m.d, k) Base.get(m::MatlabClassObject, k, default) = get(m.d, k, default) + function MatlabStructArray(arr::AbstractArray{MatlabClassObject}) + first_obj, remaining_obj = Iterators.peel(arr) + class = first_obj.class + if !all(x->isequal(class, x.class), remaining_obj) + error("to write a MatlabClassObject array all classes must be equal. Use `Array{Any}` to write a cell array") + end + return MatlabStructArray(arr, class) + end + function convert_struct_array(d::Dict{String, Any}, class::String="") # there is no possibility of having cell arrays mixed with struct arrays (afaik) field_values = first(values(d)) diff --git a/test/types.jl b/test/types.jl index c738002..266c226 100644 --- a/test/types.jl +++ b/test/types.jl @@ -65,4 +65,11 @@ end obj["b"] = 7 @test obj["b"] == 7 + + c_arr = [MatlabClassObject(d, "TestClassOld"), MatlabClassObject(d, "TestClassOld")] + s_arr = MatlabStructArray(c_arr) + @test s_arr.class == "TestClassOld" + + wrong_arr = [MatlabClassObject(d, "TestClassOld"), MatlabClassObject(d, "Bah")] + @test_throws ErrorException MatlabStructArray(wrong_arr) end \ No newline at end of file diff --git a/test/write.jl b/test/write.jl index 3cab705..b52a3d8 100644 --- a/test/write.jl +++ b/test/write.jl @@ -173,4 +173,9 @@ test_write(Dict("class_array" => carr)) d = Dict{String,Any}("foo" => 5) obj = MatlabClassObject(d, "TestClassOld") -test_write(Dict("tc_old" => obj)) \ No newline at end of file +test_write(Dict("tc_old" => obj)) + +carr = [MatlabClassObject(d, "TestClassOld"), MatlabClassObject(d, "TestClassOld")] +matwrite(tmpfile, Dict("class_array" => carr)) +carr_read = matread(tmpfile)["class_array"] +@test carr_read == MatlabStructArray(carr) \ No newline at end of file From e2050d0398c4b07d5863ac5d167dc776fc961b9d Mon Sep 17 00:00:00 2001 From: Matthijs Cox Date: Fri, 14 Nov 2025 15:10:49 +0100 Subject: [PATCH 15/19] include submodule in autodocs --- docs/src/methods.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/methods.md b/docs/src/methods.md index 0f6c160..078b32d 100644 --- a/docs/src/methods.md +++ b/docs/src/methods.md @@ -6,5 +6,5 @@ ## Methods usage ```@autodocs -Modules = [MAT] +Modules = [MAT, MAT.MAT_types] ``` From 54843a258fd842525feef7ca6ac8aaf2552408bd Mon Sep 17 00:00:00 2001 From: Matthijs Cox Date: Fri, 14 Nov 2025 15:36:46 +0100 Subject: [PATCH 16/19] additional Dict interfaces for MatlabStructArray --- src/MAT_HDF5.jl | 1 + src/MAT_types.jl | 12 ++++++++++++ test/types.jl | 10 +++++++--- 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/src/MAT_HDF5.jl b/src/MAT_HDF5.jl index abf8ad3..4760dd5 100644 --- a/src/MAT_HDF5.jl +++ b/src/MAT_HDF5.jl @@ -526,6 +526,7 @@ function _write_references!(mfile::MatlabHDF5File, parent::HDF5Parent, data::Abs else a = g["a"] if !haskey(attributes(a), "MATLAB_empty") + close(a) error("Must create the empty item, with name a, first") end close(a) diff --git a/src/MAT_types.jl b/src/MAT_types.jl index b51c1b2..c656f7c 100644 --- a/src/MAT_types.jl +++ b/src/MAT_types.jl @@ -104,6 +104,9 @@ module MAT_types Base.eltype(::Type{MatlabStructArray{N}}) where N = Pair{String, Array{Any,N}} Base.length(arr::MatlabStructArray) = length(arr.names) + Base.keys(arr::MatlabStructArray) = arr.names + Base.values(arr::MatlabStructArray) = arr.values + Base.haskey(arr::MatlabStructArray, k::AbstractString) = k in keys(arr) function Base.iterate(arr::T, i=next_state(arr)) where T<:MatlabStructArray if i == 0 @@ -146,6 +149,15 @@ module MAT_types return getindex(m.values, idx) end + function Base.get(m::MatlabStructArray, s::AbstractString, default) + idx = findfirst(isequal(s), m.names) + if isnothing(idx) + return default + else + return getindex(m.values, idx) + end + end + # convert Dict array to MatlabStructArray function MatlabStructArray(arr::AbstractArray{<:AbstractDict, N}, class::String="") where N first_dict, remaining_dicts = Iterators.peel(arr) diff --git a/test/types.jl b/test/types.jl index 266c226..c6c5b3c 100644 --- a/test/types.jl +++ b/test/types.jl @@ -31,11 +31,15 @@ using MAT, Test s_arr = MatlabStructArray(d_arr) d = Dict(s_arr) @test d isa Dict{String, Any} - @test collect(keys(d)) == s_arr.names - @test collect(values(d)) == s_arr.values - # iteration similar to Dict + @test collect(keys(d)) == keys(s_arr) + @test collect(values(d)) == values(s_arr) + # Dict like interfaces @test length(s_arr) == 2 @test collect(Dict(s_arr)) == collect(s_arr) + @test haskey(s_arr, "x") + @test get(s_arr, "x", nothing) == s_arr["x"] + @test !haskey(s_arr, "wrong") + @test get(s_arr, "wrong", nothing) === nothing # possibility to convert back to dict array via `Array` s_arr = MatlabStructArray(d_arr) From 4825ba9a28fcd5262ec16dd3d71c3d101ca183da Mon Sep 17 00:00:00 2001 From: Matthijs Cox Date: Fri, 14 Nov 2025 15:49:26 +0100 Subject: [PATCH 17/19] remove Literate dependency from docs --- docs/Project.toml | 3 +-- docs/make.jl | 43 +++---------------------------------------- test/runtests.jl | 11 +++++++---- 3 files changed, 11 insertions(+), 46 deletions(-) diff --git a/docs/Project.toml b/docs/Project.toml index 644fb3d..d03159b 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -1,8 +1,7 @@ [deps] Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" InteractiveUtils = "b77e0a4c-d291-57a0-90e8-8db25a27a240" -Literate = "98b081ad-f1c9-55d3-8b20-4c87d4299306" +MAT = "23992714-dd62-5051-b70f-ba57cb901cac" [compat] Documenter = "1" -Literate = "2" diff --git a/docs/make.jl b/docs/make.jl index 4ec76f4..e48e71c 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -1,50 +1,13 @@ execute = isempty(ARGS) || ARGS[1] == "run" -org, reps = :JuliaIO, :MAT -eval(:(using $reps)) +org, repo = :JuliaIO, :MAT +eval(:(using $repo)) using Documenter -using Literate # https://juliadocs.github.io/Documenter.jl/stable/man/syntax/#@example-block ENV["GKSwstype"] = "100" ENV["GKS_ENCODING"] = "utf-8" -# generate examples using Literate -lit = joinpath(@__DIR__, "lit") -src = joinpath(@__DIR__, "src") -gen = joinpath(@__DIR__, "src/generated") - -base = "$org/$reps.jl" -repo_root_url = - "https://github.com/$base/blob/main/docs/lit/examples" -nbviewer_root_url = - "https://nbviewer.org/github/$base/tree/gh-pages/dev/generated/examples" -binder_root_url = - "https://mybinder.org/v2/gh/$base/gh-pages?filepath=dev/generated/examples" - - -repo = eval(:($reps)) -DocMeta.setdocmeta!(repo, :DocTestSetup, :(using $reps); recursive=true) - -# can all Literate docs code be removed? because there is no folder docs/src/lit -if isdir(lit) - for (root, _, files) in walkdir(lit), file in files - splitext(file)[2] == ".jl" || continue # process .jl files only - ipath = joinpath(root, file) - opath = splitdir(replace(ipath, lit => gen))[1] - Literate.markdown(ipath, opath; documenter = execute, # run examples - repo_root_url, nbviewer_root_url, binder_root_url) - Literate.notebook(ipath, opath; execute = false, # no-run notebooks - repo_root_url, nbviewer_root_url, binder_root_url) - end -end - - -# Documentation structure -ismd(f) = splitext(f)[2] == ".md" -pages(folder) = - [joinpath("generated/", folder, f) for f in readdir(joinpath(gen, folder)) if ismd(f)] - isci = get(ENV, "CI", nothing) == "true" format = Documenter.HTML(; @@ -55,7 +18,7 @@ format = Documenter.HTML(; ) makedocs(; - modules = [repo], + modules = [MAT], authors = "Contributors", sitename = "$repo.jl", format, diff --git a/test/runtests.jl b/test/runtests.jl index 5ba9e68..6789b43 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,6 +1,9 @@ using SparseArrays, LinearAlgebra +using Test, MAT -include("types.jl") -include("read.jl") -include("readwrite4.jl") -include("write.jl") +@testset "MAT" begin + include("types.jl") + include("read.jl") + include("readwrite4.jl") + include("write.jl") +end From 9969785e4fdcbf36163f09c9eb85c8aab3bbd350 Mon Sep 17 00:00:00 2001 From: Matthijs Cox Date: Fri, 14 Nov 2025 16:16:51 +0100 Subject: [PATCH 18/19] allow Char writing via String --- src/MAT_HDF5.jl | 5 +++++ src/MAT_v4.jl | 4 ++++ test/write.jl | 6 ++++++ 3 files changed, 15 insertions(+) diff --git a/src/MAT_HDF5.jl b/src/MAT_HDF5.jl index 4760dd5..023b7fe 100644 --- a/src/MAT_HDF5.jl +++ b/src/MAT_HDF5.jl @@ -485,6 +485,11 @@ function m_write(mfile::MatlabHDF5File, parent::HDF5Parent, name::String, str::A end end +# Char +function m_write(mfile::MatlabHDF5File, parent::HDF5Parent, name::String, c::AbstractChar) + m_write(mfile, parent, name, string(c)) +end + # Write cell arrays function m_write(mfile::MatlabHDF5File, parent::HDF5Parent, name::String, data::AbstractArray{T}) where T data = _normalize_arr(data) diff --git a/src/MAT_v4.jl b/src/MAT_v4.jl index 07fa547..c3bb69c 100644 --- a/src/MAT_v4.jl +++ b/src/MAT_v4.jl @@ -227,6 +227,10 @@ function colvals(A::AbstractSparseMatrix) cols end +function write(parent::Matlabv4File, name::String, s::AbstractChar) + write(parent, name, string(s)) +end + function write(parent::Matlabv4File, name::String, s) M = Int(parent.swap_bytes) O = 0 diff --git a/test/write.jl b/test/write.jl index b52a3d8..e131814 100644 --- a/test/write.jl +++ b/test/write.jl @@ -83,6 +83,12 @@ test_write(Dict( "string" => "string" )) +# cannot distinguish char from single element string +test_write(Dict("char" => 'a')) +# inconsistent behavior in v4 +matwrite(tmpfile, Dict("char" => 'a'), version="v4") +@test matread(tmpfile)["char"] == "a" + test_write(Dict( "cell" => Any[1 2.01 "string" Any["string1" "string2"]] )) From ea82e8a7a0e0108ce077a7832de31515c4cc777f Mon Sep 17 00:00:00 2001 From: Matthijs Cox Date: Mon, 17 Nov 2025 09:42:43 +0100 Subject: [PATCH 19/19] Array(::MatlabStructArray) converts for class object array --- src/MAT_types.jl | 36 ++++++++++++++++++++------ test/types.jl | 11 +++++++- test/write.jl | 66 +++++++++++++++++++++++++----------------------- 3 files changed, 72 insertions(+), 41 deletions(-) diff --git a/src/MAT_types.jl b/src/MAT_types.jl index c656f7c..1ca8e08 100644 --- a/src/MAT_types.jl +++ b/src/MAT_types.jl @@ -70,9 +70,8 @@ module MAT_types names::Vector{String} values::Vector{Array{Any,N}} class::String - function MatlabStructArray(names::Vector{String}, values::Vector{Array{Any,N}}, class::String="") where N - # call MatlabStructArray{N}() to avoid the check - check_struct_array(names, values) + function MatlabStructArray(names::Vector{String}, values::Vector{Array{Any,N}}, class::String=""; check::Bool=true) where N + check && check_struct_array(names, values) return new{N}(names, values, class) end function MatlabStructArray{N}(names::Vector{String}, values::Vector{Array{Any,N}}, class::String="") where N @@ -91,8 +90,11 @@ module MAT_types end end - function MatlabStructArray(names::AbstractVector{<:AbstractString}, values::AbstractArray{<:AbstractArray{T,N}}, class="") where {T,N} - MatlabStructArray{N}(string.(names), Vector{Array{Any,N}}(values), string(class)) + function MatlabStructArray(names::AbstractVector{<:AbstractString}, values::AbstractArray{A}, class=""; check::Bool=true) where {N, A<:AbstractArray{T, N} where {T}} + MatlabStructArray(string.(names), Vector{Array{Any,N}}(values), string(class); check=check) + end + function MatlabStructArray(names::Vector{String}, values::AbstractArray{A}, class=""; check::Bool=true) where {N, A<:AbstractArray{T, N} where {T}} + MatlabStructArray(names, Vector{Array{Any,N}}(values), string(class); check=check) end # empty array @@ -187,18 +189,23 @@ module MAT_types Base.Dict{String, Any}(arr.names .=> arr.values) end - Base.Array(arr::MatlabStructArray) = Array{Dict{String,Any}}(arr) - function Base.Array{D}(arr::MatlabStructArray{N}) where {T,D<:AbstractDict{T},N} + Base.Array{D}(arr::MatlabStructArray{N}) where {D<:AbstractDict,N} = Array{D,N}(arr) + + function Base.Array{D, N}(arr::MatlabStructArray{N}) where {D<:AbstractDict,N} first_field = first(arr.values) sz = size(first_field) result = Array{D, N}(undef, sz) for idx in eachindex(first_field) element_values = (v[idx] for v in arr.values) - result[idx] = D(T.(arr.names) .=> element_values) + result[idx] = create_struct(D, arr.names, element_values, arr.class) end return result end + function create_struct(::Type{D}, keys, values, class::String) where {T, D<:AbstractDict{T}} + return D(T.(keys) .=> values) + end + struct StructArrayField{N} values::Array{Any,N} end @@ -256,4 +263,17 @@ module MAT_types end end end + + function Base.Array(arr::MatlabStructArray{N}) where N + if isempty(arr.class) + return Array{Dict{String,Any}, N}(arr) + else + return Array{MatlabClassObject, N}(arr) + end + end + + function create_struct(::Type{D}, keys, values, class::String) where D<:MatlabClassObject + d = Dict{String, Any}(string.(keys) .=> values) + return MatlabClassObject(d, class) + end end \ No newline at end of file diff --git a/test/types.jl b/test/types.jl index c6c5b3c..aaa87da 100644 --- a/test/types.jl +++ b/test/types.jl @@ -2,7 +2,7 @@ using MAT, Test @testset "MatlabStructArray" begin d_arr = Dict{String, Any}[ - Dict("x"=>[1.0,2.0], SubString("y")=>[3.0,4.0]), + Dict("x"=>[1.0,2.0], SubString("y")=>3.0), Dict("x"=>[5.0,6.0], "y"=>[]) ] s_arr = MatlabStructArray(d_arr) @@ -44,10 +44,19 @@ using MAT, Test # possibility to convert back to dict array via `Array` s_arr = MatlabStructArray(d_arr) @test Array(s_arr) == d_arr + d_arr_reshape = reshape(d_arr, 1, 2) + @test Array(MatlabStructArray(d_arr_reshape)) == d_arr_reshape d_symbol = Array{Dict{Symbol,Any}}(MatlabStructArray(d_arr)) @test d_symbol[2][:x] == d_arr[2]["x"] @test Array(MatlabStructArray(d_symbol)) == d_arr + # class object array conversion + s_arr = MatlabStructArray(d_arr, "TestClass") + c_arr = Array(s_arr) + @test c_arr isa Array{MatlabClassObject} + @test all(c->c.class=="TestClass", c_arr) + @test MatlabStructArray(c_arr) == s_arr + # test error of unequal structs wrong_sarr = Dict{String, Any}[ Dict("x"=>[1.0,2.0], "y"=>[3.0,4.0]), diff --git a/test/write.jl b/test/write.jl index e131814..02acc62 100644 --- a/test/write.jl +++ b/test/write.jl @@ -153,35 +153,37 @@ test_write(Dict("adjoint_arr"=>Any[1 2 3;4 5 6;7 8 9]')) test_write(Dict("reshape_arr"=>reshape(Any[1 2 3;4 5 6;7 8 9]',1,9))) # test nested struct array - interface via Dict array -sarr = Dict{String, Any}[ - Dict("x"=>[1.0,2.0], SubString("y")=>[3.0,4.0]), - Dict("x"=>[5.0,6.0], "y"=>[Dict("a"=>7), Dict("a"=>8)]) -] -# we have to test Array size is maintained inside mat files -sarr = reshape(sarr, 1, 2) -matwrite(tmpfile, Dict("s_array" => sarr)) -read_sarr = matread(tmpfile)["s_array"] -@test read_sarr isa MatlabStructArray -@test read_sarr["y"][2] isa MatlabStructArray - -sarr = Dict{String, Any}[ - Dict("x"=>[1.0,2.0], SubString("y")=>[3.0,4.0]), - Dict("x"=>[5.0,6.0], "y"=>[]) -] -test_write(Dict("s_array" => MatlabStructArray(sarr))) - -empty_sarr = MatlabStructArray(["a", "b", "c"]) -test_write(Dict("s_array" => empty_sarr)) - -# old matlab class object array -carr = MatlabStructArray(["foo"], [[5, "bar"]], "TestClassOld") -test_write(Dict("class_array" => carr)) - -d = Dict{String,Any}("foo" => 5) -obj = MatlabClassObject(d, "TestClassOld") -test_write(Dict("tc_old" => obj)) - -carr = [MatlabClassObject(d, "TestClassOld"), MatlabClassObject(d, "TestClassOld")] -matwrite(tmpfile, Dict("class_array" => carr)) -carr_read = matread(tmpfile)["class_array"] -@test carr_read == MatlabStructArray(carr) \ No newline at end of file +@testset "MatlabStructArray writing" begin + sarr = Dict{String, Any}[ + Dict("x"=>[1.0,2.0], SubString("y")=>3.0), + Dict("x"=>[5.0,6.0], "y"=>[Dict("a"=>7), Dict("a"=>8)]) + ] + # we have to test Array size is maintained inside mat files + sarr = reshape(sarr, 1, 2) + matwrite(tmpfile, Dict("s_array" => sarr)) + read_sarr = matread(tmpfile)["s_array"] + @test read_sarr isa MatlabStructArray + @test read_sarr["y"][2] isa MatlabStructArray + + sarr = Dict{String, Any}[ + Dict("x"=>[1.0,2.0], SubString("y")=>3.0), + Dict("x"=>[5.0,6.0], "y"=>[]) + ] + test_write(Dict("s_array" => MatlabStructArray(sarr))) + + empty_sarr = MatlabStructArray(["a", "b", "c"]) + test_write(Dict("s_array" => empty_sarr)) + + # old matlab class object array + carr = MatlabStructArray(["foo"], [[5, "bar"]], "TestClassOld") + test_write(Dict("class_array" => carr)) + + d = Dict{String,Any}("foo" => 5) + obj = MatlabClassObject(d, "TestClassOld") + test_write(Dict("tc_old" => obj)) + + carr = [MatlabClassObject(d, "TestClassOld"), MatlabClassObject(d, "TestClassOld")] + matwrite(tmpfile, Dict("class_array" => carr)) + carr_read = matread(tmpfile)["class_array"] + @test carr_read == MatlabStructArray(carr) +end \ No newline at end of file