Skip to content

Commit

Permalink
restrict numeric values to fit in 8 ASCII characters per EDF spec (#30)
Browse files Browse the repository at this point in the history
  • Loading branch information
OTDE authored May 5, 2020
1 parent c61ddad commit c2f706b
Show file tree
Hide file tree
Showing 4 changed files with 59 additions and 21 deletions.
3 changes: 2 additions & 1 deletion Project.toml
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
name = "EDF"
uuid = "ccffbfc1-f56e-50fb-a33b-53d1781b2825"
authors = ["Beacon Biosignals, Inc."]
version = "0.4.0"
version = "0.4.1"

[deps]
Dates = "ade2ca70-3891-5945-98fb-dc099432e06a"
Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7"

[compat]
julia = "1.4"
Expand Down
2 changes: 1 addition & 1 deletion src/EDF.jl
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module EDF

using Dates
using Dates, Printf

include("types.jl")
include("read.jl")
Expand Down
67 changes: 48 additions & 19 deletions src/write.jl
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,54 @@
##### utilities
#####

_edf_write(io::IO, value::Union{String,Char}) = Base.write(io, value)
_edf_write(io::IO, date::Date) = Base.write(io, uppercase(Dates.format(date, dateformat"dd-u-yyyy")))
_edf_write(io::IO, date::DateTime) = Base.write(io, Dates.format(date, dateformat"dd\.mm\.yyHH\.MM\.SS"))
_edf_write(io::IO, x::Integer) = Base.write(io, string(x))
_edf_write(io::IO, x::AbstractFloat) = Base.write(io, string(isinteger(x) ? trunc(Int, x) : x))
_edf_repr(value::Union{String,Char}) = value
_edf_repr(date::Date) = uppercase(Dates.format(date, dateformat"dd-u-yyyy"))
_edf_repr(date::DateTime) = Dates.format(date, dateformat"dd\.mm\.yyHH\.MM\.SS")

function _edf_write(io::IO, metadata::T) where T<:Union{PatientID,RecordingID}
bytes_written = 0
for name in fieldnames(T)
field = getfield(metadata, name)
if T <: RecordingID && name === :startdate
bytes_written += _edf_write(io, "Startdate ")
# XXX this is really really hacky and doesn't support use of scientific notation
# where appropriate; keep in mind if you do improve this to support scientific
# notation, that scientific is NOT allowed in EDF annotation onset/duration fields
function _edf_repr(x::Real)
result = missing
if isinteger(x)
str = string(trunc(Int, x))
if length(str) <= 8
result = str
end
else
fpart, ipart = modf(x)
ipart_str = string('-'^signbit(x), Int(abs(ipart))) # handles `-0.0` case
fpart_str = @sprintf "%.7f" abs(fpart)
fpart_str = fpart_str[3:end] # remove leading `0.`
if length(ipart_str) < 7
result = ipart_str * '.' * fpart_str[1:(7 - length(ipart_str))]
elseif length(ipart_str) <= 8
result = ipart_str
end
bytes_written += field isa Missing ? Base.write(io, 'X') : _edf_write(io, field)
bytes_written += Base.write(io, ' ')
end
return bytes_written
if !ismissing(result)
if all(c -> c in ('0', '.', '-'), result)
x == 0 && return result
else
return result
end
end
error("failed to fit number into EDF's 8 ASCII character limit: $x")
end

_edf_metadata_repr(::Missing) = 'X'
_edf_metadata_repr(x) = _edf_repr(x)

function _edf_repr(metadata::T) where T<:Union{PatientID,RecordingID}
header = T <: RecordingID ? String["Startdate"] : String[]
return join([header; [_edf_metadata_repr(getfield(metadata, name)) for name in fieldnames(T)]], ' ')
end

function edf_write(io::IO, value, byte_limit::Integer)
bytes_written = _edf_write(io, value)
bytes_written <= byte_limit || error("Written value $value contains more bytes than limit $byte_limit")
edf_value = _edf_repr(value)
@assert isascii(edf_value)
size = length(edf_value)
bytes_written = Base.write(io, edf_value)
while bytes_written < byte_limit
bytes_written += Base.write(io, UInt8(' '))
end
Expand All @@ -35,6 +61,7 @@ end
#####

function write_header(io::IO, file::File)
length(file.signals) <= 9999 || error("EDF does not allow files with more than 9999 signals")
bytes_written = 0
bytes_written += edf_write(io, file.header.version, 8)
bytes_written += edf_write(io, file.header.patient, 80)
Expand Down Expand Up @@ -81,11 +108,13 @@ end
function write_signal_record(io::IO, signal::AnnotationsSignal, record_index::Int)
bytes_written = 0
for tal in signal.records[record_index]
bytes_written += Base.write(io, signbit(tal.onset_in_seconds) ? '-' : '+')
bytes_written += _edf_write(io, tal.onset_in_seconds)
if !signbit(tal.onset_in_seconds) # otherwise, the `-` will already be in number string
bytes_written += Base.write(io, '+')
end
bytes_written += Base.write(io, _edf_repr(tal.onset_in_seconds))
if tal.duration_in_seconds !== nothing
bytes_written += Base.write(io, 0x15)
bytes_written += _edf_write(io, tal.duration_in_seconds)
bytes_written += Base.write(io, _edf_repr(tal.duration_in_seconds))
end
bytes_written += Base.write(io, 0x14)
for annotation in tal.annotations
Expand Down
8 changes: 8 additions & 0 deletions test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,14 @@ const DATADIR = joinpath(@__DIR__, "data")
@test eof(io)
end

@test EDF._edf_repr(34577777) == "34577777"
@test EDF._edf_repr(0.0345) == "0.034500"
@test EDF._edf_repr(-0.02) == "-0.02000"
@test EDF._edf_repr(-187.74445) == "-187.744"
@test_throws ErrorException EDF._edf_repr(123456789)
@test_throws ErrorException EDF._edf_repr(-12345678)
@test_throws ErrorException EDF._edf_repr(0.00000000024)

uneven = EDF.read(joinpath(DATADIR, "test_uneven_samp.edf"))
@test sprint(show, uneven) == "EDF.File with 2 signals"
@test uneven.header.version == "0"
Expand Down

2 comments on commit c2f706b

@jrevels
Copy link
Member

@jrevels jrevels commented on c2f706b May 5, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@JuliaRegistrator
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Registration pull request created: JuliaRegistries/General/14222

After the above pull request is merged, it is recommended that a tag is created on this repository for the registered package version.

This will be done automatically if the Julia TagBot GitHub Action is installed, or can be done manually through the github interface, or via:

git tag -a v0.4.1 -m "<description of version>" c2f706b0854319f4d464aa4dfa4d46bfaddad3c9
git push origin v0.4.1

Please sign in to comment.