suyashkumar / dicom

⚡High Performance DICOM Medical Image Parser in Go.
MIT License
950 stars 138 forks source link

Feature/date time helper methods #189

Open bpeake-illuscio opened 3 years ago

bpeake-illuscio commented 3 years ago

Opening this as a draft, but if you like the API, I think it's ready to be a full PR.

This PR adds several helper methods to the Date, Time, and Datetime types. The work I do involves manipulating DICOM dates and times pretty frequently, so I thought it might be nice to add some quality-of-life features to follow up on the groundwork of #171

Since not all values on a given [Type].Time field are valid due to low precision of the source values, I've added methods for the relevant time values that also report their presence.

Each type has also received a general method to easily check whether a value has at least some precision value.

An example using Datetime:

// This is a DT value like we would expect
dtString := "2020121012"

dt, err := ParseDatetime(dtString)
if err != nil {
    panic(err)
}

// Our Datetime value has some methods similar to time.Time's methods, but also
// returns presence information since not all DICOM datetimes contain all datetime
// components.
//
// Try to get the Day value. Our value included a day, so 'ok' will be true
if day, ok := dt.Day(); ok {
    fmt.Println("DAY VALUE   :", day)
}

// Try to get the Minute value. Because minutes are not included, 'ok' will be false
// and this will not print.
if minute, ok := dt.Minute(); ok {
    fmt.Println("MINUTE VALUE :", minute)
}

// We can also easily check if the value contains a certain precision:
hasMinutes := dt.HasPrecision(PrecisionMinutes)
fmt.Println("HAS MINUTES :", hasMinutes)

// Output:
// TIME VALUE  : 2020-12-10 12:00:00 +0000 +0000
// PRECISION   : HOURS
// NO OFFSET   : true
// DAY VALUE   : 10
// HAS MINUTES : false

A method has also been added to both the Date and Time types to combine a Date value and a Time value into a single Datetime value:

daString := "20200316"
tmString := "105434.123456"

daParsed, err := ParseDate(daString)
if err != nil {
    panic(err)
}

tmParsed, err := ParseTime(tmString)
if err != nil {
    panic(err)
}

datetime, err := daParsed.Combine(tmParsed, time.UTC)
if err != nil {
    panic(err)
}

fmt.Println("DCM    :", datetime.DCM())
fmt.Println("STRING :", datetime.String())

// Output:
// DCM    : 20200316105434.123456+0000
// STRING : 2020-03-16 10:54:34.123456 +00:00

Both #186 and #188 have been merged into this branch already.

bpeake-illuscio commented 3 years ago

I've just pushed an update to this followup that tweaks and adds a couple methods so that you can form a few common interfaces between the dcmtime types. For instance, you can now write a helper function like this:

// DCMTime is a common interface for dcmtime.Date, dcmtime.Time, and dcmtime.Datetime.
type DCMTime interface {
    GetTime() time.Time
    GetPrecision() dcmtime.PrecisionLevel
    DCM() string
}

func InspectDICOMTimeVal(value DCMTime) error {
    // Do something with this value
    return nil
}

The main addition here is adding a GetTime() and GetPrecision() method to each of the dcmtime types. I've been writing some code where a single code path could be used with an interface like this, so I decided it was worth adding in. Callers could do this though wrappers on their own, but having it right out of the box is kind of nice, IMO.

I realize that I am kind of bloating this PR, so let me know if you would like me to try and break this up into smaller PRs. Thanks!