Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/date time helper methods #189

Draft
wants to merge 15 commits into
base: main
Choose a base branch
from
2 changes: 1 addition & 1 deletion dataset.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ func (d *Dataset) FlatIterator() <-chan *Element {
// Or, if you don't need the channel interface, simply use
// Dataset.FlatStatefulIterator.
func ExhaustElementChannel(c <-chan *Element) {
for _ = range c {
for range c {
}
}

Expand Down
63 changes: 59 additions & 4 deletions pkg/dcmtime/date.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,61 @@ type Date struct {
IsNEMA bool
}

// GetTime returns the Time field value for the Date. Included to support common
// interfaces with other dcmtime types.
func (da Date) GetTime() time.Time {
return da.Time
}

// GetPrecision returns the Precision field value for the Date. Included to support
// common interfaces with other dcmtime types.
func (da Date) GetPrecision() PrecisionLevel {
return da.Precision
}

// daPrecisionOmits is the range of precision values not relevant to Date.
var daPrecisionOmits = precisionRange{
Min: PrecisionHours,
Max: PrecisionMS5,
}

// HasPrecision returns whether this da value has a precision of AT LEAST 'check'.
//
// Will always Return false for PrecisionHours, PrecisionMinutes PrecisionSeconds, and
// PrecisionMS*.
//
// Will return true for PrecisionFull if all possible values are present.
func (da Date) HasPrecision(check PrecisionLevel) bool {
return hasPrecisionOmits(check, da.Precision, daPrecisionOmits)
}

// Year returns the underlying Time.Year(). Since a DICOM DA value must contain a year,
// presence is not reported.
func (da Date) Year() int {
return da.Time.Year()
}

// Month returns the underlying Time.Month(), and a boolean indicating whether the
// original DICOM value included the month.
func (da Date) Month() (month time.Month, ok bool) {
return da.Time.Month(), hasPrecision(PrecisionMonth, da.Precision)
}

// Day returns the underlying time.Month, and a boolean indicating whether the
func (da Date) Day() (month int, ok bool) {
return da.Time.Day(), hasPrecision(PrecisionDay, da.Precision)
}

// Combine combines the Date with a Time value into a single Datetime value.
//
// The Date value must have a PrecisionLevel of PrecisionFull or the method will fail.
//
// If no location is given, time.FixedZone("", 0) will be used and NoOffset will be
// set to 'true'.
func (da Date) Combine(tm Time, location *time.Location) (Datetime, error) {
return combineDateAndTime(da, tm, location)
}

// DCM converts time.Time value to dicom DA string. Values are truncated to the
// Date.Precision value.
//
Expand All @@ -29,7 +84,7 @@ func (da Date) DCM() string {
builder := strings.Builder{}

builder.WriteString(fmt.Sprintf("%04d", year))
if !isIncluded(PrecisionMonth, da.Precision) {
if !hasPrecision(PrecisionMonth, da.Precision) {
return builder.String()
}

Expand All @@ -39,7 +94,7 @@ func (da Date) DCM() string {
}

builder.WriteString(fmt.Sprintf("%02d", month))
if !isIncluded(PrecisionDay, da.Precision) {
if !hasPrecision(PrecisionDay, da.Precision) {
return builder.String()
}

Expand All @@ -55,12 +110,12 @@ func (da Date) DCM() string {
func (da Date) String() string {
builder := strings.Builder{}
_, _ = builder.WriteString(fmt.Sprintf("%04d", da.Time.Year()))
if !isIncluded(PrecisionMonth, da.Precision) {
if !hasPrecision(PrecisionMonth, da.Precision) {
return builder.String()
}

_, _ = builder.WriteString(fmt.Sprintf("-%02d", da.Time.Month()))
if !isIncluded(PrecisionDay, da.Precision) {
if !hasPrecision(PrecisionDay, da.Precision) {
return builder.String()
}

Expand Down
144 changes: 131 additions & 13 deletions pkg/dcmtime/date_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,55 +7,109 @@ import (
"time"
)

func TestParseDate(t *testing.T) {
// daPrecisionOmits is the range of precision values not relevant to Date.
var daPrecisionOmits = precisionRange{
Min: dcmtime.PrecisionHours,
Max: dcmtime.PrecisionMS5,
}

func TestDate(t *testing.T) {
testCases := []struct {
Name string
DAValue string
ExpectedString string
Expected time.Time
// Name is the name of the test case.
Name string
// DAValue is the DICOM string value we are going to parse.
DAValue string
// ExpectedString is the expected value of the String() method.
ExpectedString string
// ExpectedTime is the expected time.Time value of the parsed value.
ExpectedTime time.Time
// ExpectedPrecision is the expected precision value of the parsed value.
ExpectedPrecision dcmtime.PrecisionLevel
// HasMonth is whether the parsed value's Month() method should return ok=true
HasMonth bool
// HasDay is whether the parsed value's Day() method should return ok=true
HasDay bool
// HasPrecisionRange is the range of Precision Values we expect the
// HasPrecision() method to return true for.
HasPrecisionRange precisionRange
}{
{
Name: "PrecisionFull",
DAValue: "20200304",
ExpectedString: "2020-03-04",
Expected: time.Date(2020, 3, 4, 0, 0, 0, 0, time.UTC),
ExpectedTime: time.Date(2020, 3, 4, 0, 0, 0, 0, time.UTC),
ExpectedPrecision: dcmtime.PrecisionFull,
HasMonth: true,
HasDay: true,
HasPrecisionRange: precisionRange{
Min: dcmtime.PrecisionYear,
Max: dcmtime.PrecisionFull,
},
},
{
Name: "PrecisionMonth",
DAValue: "202003",
ExpectedString: "2020-03",
Expected: time.Date(2020, 3, 1, 0, 0, 0, 0, time.UTC),
ExpectedTime: time.Date(2020, 3, 1, 0, 0, 0, 0, time.UTC),
ExpectedPrecision: dcmtime.PrecisionMonth,
HasMonth: true,
HasDay: false,
HasPrecisionRange: precisionRange{
Min: dcmtime.PrecisionYear,
Max: dcmtime.PrecisionMonth,
},
},
{
Name: "PrecisionYear",
DAValue: "2020",
ExpectedString: "2020",
Expected: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC),
ExpectedTime: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC),
ExpectedPrecision: dcmtime.PrecisionYear,
HasMonth: false,
HasDay: false,
HasPrecisionRange: precisionRange{
Min: dcmtime.PrecisionYear,
Max: dcmtime.PrecisionYear,
},
},
{
Name: "PrecisionFullNEMA",
DAValue: "2020.03.04",
ExpectedString: "2020-03-04",
Expected: time.Date(2020, 3, 4, 0, 0, 0, 0, time.UTC),
ExpectedTime: time.Date(2020, 3, 4, 0, 0, 0, 0, time.UTC),
ExpectedPrecision: dcmtime.PrecisionFull,
HasMonth: true,
HasDay: true,
HasPrecisionRange: precisionRange{
Min: dcmtime.PrecisionYear,
Max: dcmtime.PrecisionFull,
},
},
{
Name: "PrecisionMonthNEMA",
DAValue: "2020.03",
ExpectedString: "2020-03",
Expected: time.Date(2020, 3, 1, 0, 0, 0, 0, time.UTC),
ExpectedTime: time.Date(2020, 3, 1, 0, 0, 0, 0, time.UTC),
ExpectedPrecision: dcmtime.PrecisionMonth,
HasMonth: true,
HasDay: false,
HasPrecisionRange: precisionRange{
Min: dcmtime.PrecisionYear,
Max: dcmtime.PrecisionMonth,
},
},
{
Name: "PrecisionYearNEMA",
DAValue: "2020",
ExpectedString: "2020",
Expected: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC),
ExpectedTime: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC),
ExpectedPrecision: dcmtime.PrecisionYear,
HasMonth: false,
HasDay: false,
HasPrecisionRange: precisionRange{
Min: dcmtime.PrecisionYear,
Max: dcmtime.PrecisionYear,
},
},
}

Expand All @@ -73,11 +127,11 @@ func TestParseDate(t *testing.T) {
t.Fatal("parse err:", err)
}

if !tc.Expected.Equal(parsed.Time) {
if !tc.ExpectedTime.Equal(parsed.Time) {
t.Errorf(
"parsed time (%v) != expected (%v) from source DA '%v'",
parsed.Time,
tc.Expected,
tc.ExpectedTime,
tc.DAValue,
)

Expand All @@ -93,6 +147,26 @@ func TestParseDate(t *testing.T) {
}
})

t.Run("GetTime()", func(t *testing.T) {
if !tc.ExpectedTime.Equal(parsed.GetTime()) {
t.Errorf(
"Datetime.GetTime(): expected %v, got %v",
tc.ExpectedTime,
parsed.Time,
)
}
})

t.Run("GetPrecision()", func(t *testing.T) {
if parsed.GetPrecision() != tc.ExpectedPrecision {
t.Errorf(
"Datetime.GetPrecision(): expected %v, got %v",
tc.ExpectedPrecision.String(),
parsed.Precision.String(),
)
}
})

t.Run("String()", func(t *testing.T) {
stringVal := parsed.String()
if stringVal != tc.ExpectedString {
Expand All @@ -112,11 +186,41 @@ func TestParseDate(t *testing.T) {
)
}
})

t.Run("Year()", func(t *testing.T) {
year := parsed.Year()
checkDateHelperOutput(t, "Year()", parsed.Time.Year(), year, true, true)
})

t.Run("Month()", func(t *testing.T) {
month, ok := parsed.Month()
checkDateHelperOutput(t, "Month()", int(parsed.Time.Month()), int(month), tc.HasMonth, ok)
})

t.Run("Day()", func(t *testing.T) {
day, ok := parsed.Day()
checkDateHelperOutput(t, "Day()", parsed.Time.Day(), day, tc.HasDay, ok)
})

t.Run("HasPrecision()", func(t *testing.T) {
checkHasPrecision(t, parsed, tc.HasPrecisionRange, daPrecisionOmits)
})
})

}
}

// checkDateHelperOutput check the output of a helper value getter like Date.Month()
func checkDateHelperOutput(t *testing.T, methodName string, expectedValue int, value int, expectedOK bool, ok bool) {
if expectedValue != value {
t.Errorf("got %v int value of '%v', expected '%v'", methodName, value, expectedValue)
}

if expectedOK != ok {
t.Errorf("got %v ok value of '%v', expected '%v'", methodName, ok, expectedOK)
}
}

func TestParseDateErr(t *testing.T) {
testCases := []struct {
Name string
Expand Down Expand Up @@ -200,3 +304,17 @@ func TestDate_DCMTrimming(t *testing.T) {
})
}
}

// TestDate_SaneDefaults tests that instantiating a new Date object with just the Time
// field specified yields a reasonable result.
func TestDate_SaneDefaults(t *testing.T) {
newValue := dcmtime.Date{
Time: time.Date(2021, 03, 16, 0, 0, 0, 0, time.FixedZone("", 0)),
}

dcmVal := newValue.DCM()
expexted := "20210316"
if dcmVal != expexted {
t.Errorf("DCM(): expected '%v', but got '%v'", expexted, dcmVal)
}
}
Loading