Skip to content

Commit 5c9d9b7

Browse files
authored
DateTime and Millisecond writing (#218)
* DateTime and Millisecond writing * option to avoid automatic MatlabOpaque conversions for troubleshooting * better isapprox support
1 parent 36ae2bd commit 5c9d9b7

File tree

8 files changed

+195
-59
lines changed

8 files changed

+195
-59
lines changed

src/MAT.jl

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -41,11 +41,11 @@ export MatlabStructArray, MatlabClassObject, MatlabOpaque, MatlabTable
4141

4242
# Open a MATLAB file
4343
const HDF5_HEADER = UInt8[0x89, 0x48, 0x44, 0x46, 0x0d, 0x0a, 0x1a, 0x0a]
44-
function matopen(filename::AbstractString, rd::Bool, wr::Bool, cr::Bool, tr::Bool, ff::Bool, compress::Bool; table::Type=MatlabTable)
44+
function matopen(filename::AbstractString, rd::Bool, wr::Bool, cr::Bool, tr::Bool, ff::Bool, compress::Bool; table::Type=MatlabTable, convert_opaque::Bool=true)
4545
# When creating new files, create as HDF5 by default
4646
fs = filesize(filename)
4747
if cr && (tr || fs == 0)
48-
return MAT_HDF5.matopen(filename, rd, wr, cr, tr, ff, compress, Base.ENDIAN_BOM == 0x04030201; table=table)
48+
return MAT_HDF5.matopen(filename, rd, wr, cr, tr, ff, compress, Base.ENDIAN_BOM == 0x04030201; table=table, convert_opaque=convert_opaque)
4949
elseif fs == 0
5050
error("File \"$filename\" does not exist and create was not specified")
5151
end
@@ -73,26 +73,26 @@ function matopen(filename::AbstractString, rd::Bool, wr::Bool, cr::Bool, tr::Boo
7373
if wr || cr || tr || ff
7474
error("creating or appending to MATLAB v5 files is not supported")
7575
end
76-
return MAT_v5.matopen(rawfid, endian_indicator; table=table)
76+
return MAT_v5.matopen(rawfid, endian_indicator; table=table, convert_opaque=convert_opaque)
7777
end
7878

7979
# Check for HDF5 file
8080
for offset = 512:512:fs-8
8181
seek(rawfid, offset)
8282
if read!(rawfid, Vector{UInt8}(undef, 8)) == HDF5_HEADER
8383
close(rawfid)
84-
return MAT_HDF5.matopen(filename, rd, wr, cr, tr, ff, compress, endian_indicator == 0x494D; table=table)
84+
return MAT_HDF5.matopen(filename, rd, wr, cr, tr, ff, compress, endian_indicator == 0x494D; table=table, convert_opaque=convert_opaque)
8585
end
8686
end
8787

8888
close(rawfid)
8989
error("\"$filename\" is not a MAT file")
9090
end
9191

92-
function matopen(fname::AbstractString, mode::AbstractString; compress::Bool = false, table::Type = MatlabTable)
93-
mode == "r" ? matopen(fname, true , false, false, false, false, false; table=table) :
94-
mode == "r+" ? matopen(fname, true , true , false, false, false, compress; table=table) :
95-
mode == "w" ? matopen(fname, false, true , true , true , false, compress; table=table) :
92+
function matopen(fname::AbstractString, mode::AbstractString; compress::Bool = false, kwargs...)
93+
mode == "r" ? matopen(fname, true , false, false, false, false, false; kwargs...) :
94+
mode == "r+" ? matopen(fname, true , true , false, false, false, compress; kwargs...) :
95+
mode == "w" ? matopen(fname, false, true , true , true , false, compress; kwargs...) :
9696
# mode == "w+" ? matopen(fname, true , true , true , true , false, compress) :
9797
# mode == "a" ? matopen(fname, false, true , true , false, true, compress) :
9898
# mode == "a+" ? matopen(fname, true , true , true , false, true, compress) :
@@ -185,8 +185,8 @@ vars["s"]["testTable"]
185185
3 │ 3489.0 Smith 2016-12-22T00:00:00 Fair Late, but only by half an hour. …
186186
```
187187
"""
188-
function matread(filename::AbstractString; table::Type=MatlabTable)
189-
file = matopen(filename; table=table)
188+
function matread(filename::AbstractString; table::Type=MatlabTable, convert_opaque::Bool=true)
189+
file = matopen(filename; table=table, convert_opaque=convert_opaque)
190190
local vars
191191
try
192192
vars = read(file)

src/MAT_HDF5.jl

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,16 @@ import HDF5: Reference
3636
import Dates
3737
import Tables
3838
import PooledArrays: PooledArray
39-
import ..MAT_types: MatlabStructArray, StructArrayField, convert_struct_array, MatlabClassObject, MatlabOpaque, MatlabTable, EmptyStruct
39+
40+
import ..MAT_types:
41+
convert_struct_array,
42+
EmptyStruct,
43+
MatlabClassObject,
44+
MatlabOpaque,
45+
MatlabStructArray,
46+
MatlabTable,
47+
ScalarOrArray,
48+
StructArrayField
4049

4150
const HDF5Parent = Union{HDF5.File, HDF5.Group}
4251
const HDF5BitsOrBool = Union{HDF5.BitsType,Bool}
@@ -100,7 +109,7 @@ function close(f::MatlabHDF5File)
100109
nothing
101110
end
102111

103-
function matopen(filename::AbstractString, rd::Bool, wr::Bool, cr::Bool, tr::Bool, ff::Bool, compress::Bool, endian_indicator::Bool; table::Type=MatlabTable)
112+
function matopen(filename::AbstractString, rd::Bool, wr::Bool, cr::Bool, tr::Bool, ff::Bool, compress::Bool, endian_indicator::Bool; table::Type=MatlabTable, convert_opaque::Bool=true)
104113
local f
105114
if ff && !wr
106115
error("Cannot append to a read-only file")
@@ -132,6 +141,7 @@ function matopen(filename::AbstractString, rd::Bool, wr::Bool, cr::Bool, tr::Boo
132141
subsys_refs = "#subsystem#"
133142
if rd && haskey(fid.plain, subsys_refs)
134143
fid.subsystem.table_type = table
144+
fid.subsystem.convert_opaque = convert_opaque
135145
subsys_data = m_read(fid.plain[subsys_refs], fid.subsystem)
136146
MAT_subsys.load_subsys!(fid.subsystem, subsys_data, endian_indicator)
137147
elseif wr
@@ -747,8 +757,12 @@ function m_write(mfile::MatlabHDF5File, parent::HDF5Parent, name::String, s)
747757
m_write(mfile, parent, name, check_struct_keys([string(x) for x in fieldnames(T)]), [getfield(s, x) for x in fieldnames(T)])
748758
end
749759

750-
function m_write(mfile::MatlabHDF5File, parent::HDF5Parent, name::String, dat::Dates.AbstractTime)
751-
error("writing of Dates types is not yet supported")
760+
function m_write(mfile::MatlabHDF5File, parent::HDF5Parent, name::String, dat::ScalarOrArray{T}) where T<:Dates.AbstractTime
761+
error("writing of type $T is not yet supported")
762+
end
763+
764+
function m_write(mfile::MatlabHDF5File, parent::HDF5Parent, name::String, dat::ScalarOrArray{T}) where T<:Union{Dates.DateTime, Dates.Millisecond}
765+
m_write(mfile, parent, name, MatlabOpaque(dat))
752766
end
753767

754768
function m_write(mfile::MatlabHDF5File, parent::HDF5Parent, name::String, obj::MatlabOpaque)

src/MAT_subsys.jl

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,10 @@ mutable struct Subsystem
5757
prop_vals_defaults::Any
5858
handle_data::Any
5959
java_data::Any
60-
table_type::Type # Julia type to convert Matlab tables into
60+
61+
# automatic MatlabOpaque conversion
62+
convert_opaque::Bool
63+
table_type::Type
6164

6265
# Counters for saving
6366
saveobj_counter::UInt32
@@ -84,6 +87,7 @@ mutable struct Subsystem
8487
nothing,
8588
nothing,
8689
nothing,
90+
true,
8791
Nothing,
8892
UInt32(0),
8993
UInt32(0),
@@ -420,7 +424,11 @@ function load_mcos_object(metadata::Array{UInt32}, type_name::String, subsys::Su
420424
if nobjects == 1
421425
oid = object_ids[1]
422426
obj = get_object!(subsys, oid, classname)
423-
return convert_opaque(obj; table=subsys.table_type)
427+
if subsys.convert_opaque
428+
return convert_opaque(obj; table=subsys.table_type)
429+
else
430+
return obj
431+
end
424432
else
425433
# no need to convert_opaque, matlab wraps object arrays in a single class normally
426434
object_arr = Array{MatlabOpaque}(undef, convert(Vector{Int}, dims)...)

src/MAT_types.jl

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ module MAT_types
3030

3131
using StringEncodings: StringEncodings
3232
import StringEncodings: Encoding
33+
import Dates
3334
import Dates: DateTime, Second, Millisecond
3435
import PooledArrays: PooledArray, RefArray
3536
using Tables: Tables
@@ -38,6 +39,9 @@ export MatlabStructArray, StructArrayField, convert_struct_array
3839
export MatlabClassObject
3940
export MatlabOpaque, convert_opaque
4041
export MatlabTable
42+
export ScalarOrArray
43+
44+
const ScalarOrArray{T} = Union{T, AbstractArray{T}}
4145

4246
# struct arrays are stored as columns per field name
4347
"""
@@ -169,7 +173,7 @@ function Base.:(==)(m1::MatlabStructArray{N}, m2::MatlabStructArray{N}) where {N
169173
end
170174

171175
function Base.isapprox(m1::MatlabStructArray, m2::MatlabStructArray; kwargs...)
172-
return isequal(m1.names, m2.names) && isapprox(m1.values, m2.values; kwargs...)
176+
return isequal(m1.class, m2.class) && isequal(m1.names, m2.names) && isapprox(m1.values, m2.values; kwargs...)
173177
end
174178

175179
function find_index(m::MatlabStructArray, s::AbstractString)
@@ -308,6 +312,14 @@ Base.iterate(m::MatlabClassObject) = iterate(m.d)
308312
Base.haskey(m::MatlabClassObject, k) = haskey(m.d, k)
309313
Base.get(m::MatlabClassObject, k, default) = get(m.d, k, default)
310314

315+
function Base.:(==)(m1::MatlabClassObject, m2::MatlabClassObject)
316+
return m1.class == m2.class && m1.d == m2.d
317+
end
318+
319+
function Base.isapprox(m1::MatlabClassObject, m2::MatlabClassObject; kwargs...)
320+
return m1.class == m2.class && dict_isapprox(m1.d, m2.d; kwargs...)
321+
end
322+
311323
function MatlabStructArray(arr::AbstractArray{MatlabClassObject})
312324
first_obj, remaining_obj = Iterators.peel(arr)
313325
class = first_obj.class
@@ -374,6 +386,28 @@ Base.iterate(m::MatlabOpaque) = iterate(m.d)
374386
Base.haskey(m::MatlabOpaque, k) = haskey(m.d, k)
375387
Base.get(m::MatlabOpaque, k, default) = get(m.d, k, default)
376388

389+
function Base.:(==)(m1::MatlabOpaque, m2::MatlabOpaque)
390+
return m1.class == m2.class && m1.d == m2.d
391+
end
392+
393+
function Base.isapprox(m1::MatlabOpaque, m2::MatlabOpaque; kwargs...)
394+
return m1.class == m2.class && dict_isapprox(m1.d, m2.d; kwargs...)
395+
end
396+
397+
function dict_isapprox(d1::AbstractDict{T}, d2::AbstractDict{T}; kwargs...) where T
398+
keys(d1) == keys(d2) || return false
399+
for k in keys(d1)
400+
v1, v2 = d1[k], d2[k]
401+
value_isapprox(v1, v2) || return false
402+
end
403+
return true
404+
end
405+
dict_isapprox(d1::AbstractDict{T1}, d2::AbstractDict{T2}; kwargs...) where {T1,T2} = false
406+
407+
value_isapprox(x1::AbstractString, x2::AbstractString; kwargs...) = isequal(x1, x2)
408+
value_isapprox(x1, x2; kwargs...) = isapprox(x1, x2; kwargs...)
409+
value_isapprox(d1::AbstractDict, d2::AbstractDict; kwargs...) = dict_isapprox(x1, x2; kwargs...)
410+
377411
function convert_opaque(obj::MatlabOpaque; table::Type=Nothing)
378412
if obj.class == "string"
379413
return from_string(obj)
@@ -445,6 +479,28 @@ function ms_to_datetime(ms::Real)
445479
return DateTime(1970, 1, 1) + Second(s) + Millisecond(ms_rem)
446480
end
447481

482+
function to_matlab_data(d::DateTime)
483+
ms = Dates.value(d)
484+
485+
# matlab counts w.r.t. 1970
486+
matlab_offset = Dates.value(DateTime(1970, 1, 1))
487+
488+
# note: can be negative, that's fine
489+
matlab_ms = ms - matlab_offset
490+
return Complex(Float64(matlab_ms))
491+
end
492+
493+
function MatlabOpaque(d::ScalarOrArray{DateTime})
494+
return MatlabOpaque(
495+
Dict(
496+
"tz" => "",
497+
"data" => map_or_not(to_matlab_data, d),
498+
"fmt" => "",
499+
),
500+
"datetime"
501+
)
502+
end
503+
448504
function from_duration(obj::MatlabOpaque)
449505
dat = obj["millis"]
450506
#fmt = obj["fmt"] # TODO: format, e.g. 'd' to Day
@@ -454,6 +510,17 @@ function from_duration(obj::MatlabOpaque)
454510
return map_or_not(Millisecond, dat)
455511
end
456512

513+
to_matlab_millis(d::Millisecond) = Float64(Dates.value(d))
514+
515+
function MatlabOpaque(d::ScalarOrArray{Millisecond})
516+
return MatlabOpaque(
517+
Dict(
518+
"millis" => map_or_not(to_matlab_millis, d),
519+
),
520+
"duration"
521+
)
522+
end
523+
457524
function from_categorical(obj::MatlabOpaque)
458525
category_names = obj["categoryNames"]
459526
codes = obj["codes"]

src/MAT_v5.jl

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -394,7 +394,7 @@ function read_matrix(f::IO, swap_bytes::Bool, subsys::Subsystem)
394394
end
395395

396396
# Open MAT file for reading
397-
function matopen(ios::IOStream, endian_indicator::UInt16; table::Type=MatlabTable)
397+
function matopen(ios::IOStream, endian_indicator::UInt16; table::Type=MatlabTable, convert_opaque::Bool=true)
398398
matfile = Matlabv5File(ios, endian_indicator == 0x494D)
399399

400400
seek(matfile.ios, 116)
@@ -405,6 +405,7 @@ function matopen(ios::IOStream, endian_indicator::UInt16; table::Type=MatlabTabl
405405
if subsys_offset != 0
406406
matfile.subsystem_position = subsys_offset
407407
matfile.subsystem.table_type = table
408+
matfile.subsystem.convert_opaque = convert_opaque
408409
read_subsystem!(matfile)
409410
end
410411

test/read.jl

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using MAT, Test
22
using Dates
3+
using SparseArrays, LinearAlgebra
34

45
function check(filename, result)
56
matfile = matopen(filename)
@@ -271,6 +272,12 @@ for format in ["v7", "v7.3"]
271272
dt = vars["testDatetime"]
272273
@test dt isa DateTime
273274
@test dt - DateTime(2019, 12, 2, 16, 42, 49) < Second(1)
275+
276+
# test no conversion at all
277+
vars = matread(filepath; convert_opaque=false)["s"]
278+
t = vars["testTable"]
279+
@test t isa MatlabOpaque
280+
@test t["data"][3] isa MatlabOpaque
274281
end
275282
end
276283

0 commit comments

Comments
 (0)