diff --git a/src/MAT_types.jl b/src/MAT_types.jl index eff6f87..9b54c01 100644 --- a/src/MAT_types.jl +++ b/src/MAT_types.jl @@ -526,9 +526,11 @@ function from_datetime(obj::MatlabOpaque) return map_or_not(ms_to_datetime, dat) end -# is the complex part the submilliseconds? +# The imaginary part is the low half of a double-double: smaller than half an ulp of the +# real part, so real + imag == real in Float64 and nothing is lost at DateTime resolution. ms_to_datetime(ms::Complex) = ms_to_datetime(real(ms)) function ms_to_datetime(ms::Real) + isfinite(ms) || return missing # NaT is stored as NaN; DateTime has no ±Inf either s, ms_rem = fldmod(ms, 1_000) # whole seconds and remainder milliseconds return DateTime(1970, 1, 1) + Second(s) + Millisecond(round(Int, ms_rem)) end @@ -561,9 +563,13 @@ function from_duration(obj::MatlabOpaque) if isnothing(dat) || isempty(dat) return Millisecond[] end - return map_or_not(Millisecond, dat) + return map_or_not(ms_to_millisecond, dat) end +# milliseconds are stored as Float64: rounded to whole ones, as datetimes are, and a +# missing (NaN) or infinite duration is missing +ms_to_millisecond(ms::Real) = isfinite(ms) ? Millisecond(round(Int, ms)) : missing + to_matlab_millis(d::Millisecond) = Float64(Dates.value(d)) function MatlabOpaque(d::ScalarOrArray{Millisecond}) diff --git a/test/missing_gen.m b/test/missing_gen.m new file mode 100644 index 0000000..c368575 --- /dev/null +++ b/test/missing_gen.m @@ -0,0 +1,16 @@ +% Generates test/v7/missing_times.mat and test/v7.3/missing_times.mat: datetime and +% duration arrays holding missing (NaT, NaN), infinite, or fractional-millisecond values. +% Run from the test directory: matlab -batch "missing_gen" + +dt_nat = [datetime(2022, 7, 20, 12, 0, 0), NaT]; +dt_nat_scalar = NaT; +dt_inf = [datetime(2022, 7, 20, 12, 0, 0), datetime(Inf, 'ConvertFrom', 'posixtime'), ... + datetime(-Inf, 'ConvertFrom', 'posixtime')]; +dt_nat_zoned = [datetime(2022, 7, 20, 12, 0, 0, 'TimeZone', 'Europe/London'), NaT('TimeZone', 'Europe/London')]; +dur_nan = [seconds(1.5), seconds(NaN)]; +dur_nan_scalar = seconds(NaN); +dur_frac = milliseconds([1.5; 2.25; 2.75]); + +vars = {'dt_nat', 'dt_nat_scalar', 'dt_inf', 'dt_nat_zoned', 'dur_nan', 'dur_nan_scalar', 'dur_frac'}; +save(fullfile('v7', 'missing_times.mat'), vars{:}, '-v7'); +save(fullfile('v7.3', 'missing_times.mat'), vars{:}, '-v7.3'); diff --git a/test/read.jl b/test/read.jl index 950480a..9b70045 100644 --- a/test/read.jl +++ b/test/read.jl @@ -275,6 +275,30 @@ for format in ["v7", "v7.3"] end end + # missing_times.mat is written by missing_gen.m: datetime and duration arrays holding + # missing (NaT, NaN), infinite or fractional-millisecond values + @testset "missing and fractional times $format" begin + filepath = joinpath(dirname(@__FILE__), format, "missing_times.mat") + if isfile(filepath) + # dt_nat_zoned has a time zone, which is dropped with a warning + vars = @test_logs (:warn, r"timezone") match_mode=:any matread(filepath) + noon = DateTime(2022, 7, 20, 12) + + @test isequal(vars["dt_nat"], [noon missing]) # NaT is missing + @test eltype(vars["dt_nat"]) == Union{Missing,DateTime} + @test ismissing(vars["dt_nat_scalar"]) + @test isequal(vars["dt_inf"], [noon missing missing]) # so are ±Inf + @test isequal(vars["dt_nat_zoned"], [noon - Hour(1) missing]) # the UTC instant + + @test isequal(vars["dur_nan"], [Millisecond(1500) missing]) + @test ismissing(vars["dur_nan_scalar"]) + @test vec(vars["dur_frac"]) == Millisecond.([2, 2, 3]) # 1.5, 2.25, 2.75 ms + else + # generated in MATLAB by test/missing_gen.m; skipped until it is committed + @test_skip isfile(filepath) + end + end + @testset "user defined classdef $format" begin let objtestfile = "user_defined_classdefs.mat" filepath = joinpath(dirname(@__FILE__), format, objtestfile)