diff --git a/dataset.go b/dataset.go index bcfc663d..6009f5e0 100644 --- a/dataset.go +++ b/dataset.go @@ -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 { } } diff --git a/pkg/dcmtime/date.go b/pkg/dcmtime/date.go index d1b7181a..1b9d43de 100644 --- a/pkg/dcmtime/date.go +++ b/pkg/dcmtime/date.go @@ -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. // @@ -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() } @@ -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() } @@ -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() } diff --git a/pkg/dcmtime/date_test.go b/pkg/dcmtime/date_test.go index 594f39f4..ef515b04 100644 --- a/pkg/dcmtime/date_test.go +++ b/pkg/dcmtime/date_test.go @@ -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, + }, }, } @@ -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, ) @@ -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 { @@ -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 @@ -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) + } +} diff --git a/pkg/dcmtime/datetime.go b/pkg/dcmtime/datetime.go index 904640fb..48d3c47b 100644 --- a/pkg/dcmtime/datetime.go +++ b/pkg/dcmtime/datetime.go @@ -12,11 +12,78 @@ type Datetime struct { // Precision with which this value was stored. For instance, a DT value with a // precision of PrecisionYear ONLY stored the year. Precision PrecisionLevel - // NoOffset: if true, offset information was not specifically included in the + // NoOffset: if false, offset information was not specifically included in the // original DT string, and will not be rendered with DCM() + // + // We use the negated version here for safer defaults - by default, without setting + // this field explicitly, the timezone will be included. NoOffset bool } +// GetTime returns the Time field value for the Datetime. Included to support common +// interfaces with other dcmtime types. +func (dt Datetime) GetTime() time.Time { + return dt.Time +} + +// GetPrecision returns the Precision field value for the Datetime. Included to support +// common interfaces with other dcmtime types. +func (dt Datetime) GetPrecision() PrecisionLevel { + return dt.Precision +} + +// HasPrecision returns whether this da value has a precision of AT LEAST 'check'. +func (dt Datetime) HasPrecision(check PrecisionLevel) bool { + return hasPrecision(check, dt.Precision) +} + +// Year returns the underlying Time.Year(). Since a DICOM DT value must contain a year, +// presence is not reported. +func (dt Datetime) Year() int { + return dt.Time.Year() +} + +// Month returns the underlying Time.Month(), and a boolean indicating whether the +// original DICOM value included the month. +func (dt Datetime) Month() (month time.Month, ok bool) { + return dt.Time.Month(), hasPrecision(PrecisionMonth, dt.Precision) +} + +// Day returns the underlying time.Month, and a boolean indicating whether the +func (dt Datetime) Day() (month int, ok bool) { + return dt.Time.Day(), hasPrecision(PrecisionDay, dt.Precision) +} + +// Hour returns the underlying Time.Hour(). Since a DICOM TM value must contain an hour, +// presence is not reported. +func (dt Datetime) Hour() (hour int, ok bool) { + return dt.Time.Hour(), hasPrecision(PrecisionHours, dt.Precision) +} + +// Minute returns the underlying Time.Minute(), and a boolean indicating whether the +// original DICOM value included minutes. +func (dt Datetime) Minute() (minute int, ok bool) { + return dt.Time.Minute(), hasPrecision(PrecisionMinutes, dt.Precision) +} + +// Second returns the underlying Time.Second(), and a boolean indicating whether the +// original DICOM value included seconds. +func (dt Datetime) Second() (second int, ok bool) { + return dt.Time.Second(), hasPrecision(PrecisionSeconds, dt.Precision) +} + +// Nanosecond returns the underlying Time.Nanosecond(), and a boolean indicating whether +// the original DICOM value included any fractal seconds. +func (dt Datetime) Nanosecond() (second int, ok bool) { + return dt.Time.Nanosecond(), hasPrecision(PrecisionMS1, dt.Precision) +} + +// Location returns the underlying Time.Location(), and a boolean indicating whether +// the original DICOM value included any timezone offset. +func (dt Datetime) Location() (location *time.Location, ok bool) { + return dt.Time.Location(), !dt.NoOffset +} + // DCM converts time.Time value to dicom DT string. Values are truncated to the // DT.Precision value. // @@ -29,7 +96,7 @@ func (dt Datetime) DCM() string { builder.WriteString(Date{Time: dt.Time, Precision: dt.Precision}.DCM()) // Check that at lead - if isIncluded(PrecisionHours, dt.Precision) { + if hasPrecision(PrecisionHours, dt.Precision) { builder.WriteString(Time{Time: dt.Time, Precision: dt.Precision}.DCM()) } @@ -53,7 +120,7 @@ func (dt Datetime) String() string { builder.WriteString(Date{Time: dt.Time, Precision: dt.Precision}.String()) // Check that at lead - if isIncluded(PrecisionHours, dt.Precision) { + if hasPrecision(PrecisionHours, dt.Precision) { builder.WriteRune(' ') builder.WriteString(Time{Time: dt.Time, Precision: dt.Precision}.String()) } @@ -130,6 +197,6 @@ func ParseDatetime(dtString string) (Datetime, error) { return Datetime{ Time: parsed, Precision: precision, - NoOffset: hasOffset, + NoOffset: !hasOffset, }, nil } diff --git a/pkg/dcmtime/datetime_test.go b/pkg/dcmtime/datetime_test.go index 15e9b569..c6b5626e 100644 --- a/pkg/dcmtime/datetime_test.go +++ b/pkg/dcmtime/datetime_test.go @@ -7,297 +7,819 @@ import ( "time" ) -func TestParseDatetime(t *testing.T) { +// dtPrecisionOmits describes the precision range not valid for datetime. Since datetime +// has no omits, the range is outside our enumerated values. +var dtPrecisionOmits = precisionRange{ + Min: dcmtime.PrecisionYear + 1, + Max: dcmtime.PrecisionYear + 1, +} + +func TestDatetime(t *testing.T) { testCases := []struct { - Name string - DTValue string - Expected time.Time + // Name is the name of the sub-test + Name string + // DTValue is the RAW DICOM DT value we are going to parse. + DTValue string + // ExpectedTime is the time.Time value we expect to be returned by the parse. + ExpectedTime time.Time + // ExpectedPrecision is the PrecisionLevel we expect to be returned by the + // parse. ExpectedPrecision dcmtime.PrecisionLevel - HasOffset bool + // ExpectedString is the expected result from the value's String() method. + ExpectedString string + // ExpectedNoOffset is the ExpectedNoOffset value we expect to get from the parse + ExpectedNoOffset bool + // 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 + // HasHour is whether the parsed value's Hour() method should return ok=true. + HasHour bool + // HasMinute is whether the parsed value's Minute() method should return ok=true. + HasMinute bool + // HasSecond is whether the parsed value's Second() method should return ok=true. + HasSecond bool + // HasNanosecond is whether the parsed value's HasNanosecond() method should + // return ok=true. + HasNanosecond bool + // HasPrecisionRange is the range of Precision Values we expect the + // HasPrecision() method to return true for. + HasPrecisionRange precisionRange }{ { Name: "PrecisionFull-PositiveOffset", DTValue: "10100203040506.456789+0102", - Expected: time.Date(1010, 2, 3, 4, 5, 6, 456789000, time.FixedZone("", 3720)), + ExpectedTime: time.Date(1010, 2, 3, 4, 5, 6, 456789000, time.FixedZone("", 3720)), ExpectedPrecision: dcmtime.PrecisionFull, - HasOffset: true, + ExpectedString: "1010-02-03 04:05:06.456789 +01:02", + ExpectedNoOffset: false, + HasMonth: true, + HasDay: true, + HasHour: true, + HasMinute: true, + HasSecond: true, + HasNanosecond: true, + HasPrecisionRange: precisionRange{ + Min: dcmtime.PrecisionYear, + Max: dcmtime.PrecisionFull, + }, }, { Name: "PrecisionMS5-PositiveOffset", DTValue: "10100203040506.45678+0102", - Expected: time.Date(1010, 2, 3, 4, 5, 6, 456780000, time.FixedZone("", 3720)), + ExpectedTime: time.Date(1010, 2, 3, 4, 5, 6, 456780000, time.FixedZone("", 3720)), ExpectedPrecision: dcmtime.PrecisionMS5, - HasOffset: true, + ExpectedString: "1010-02-03 04:05:06.45678 +01:02", + ExpectedNoOffset: false, + HasMonth: true, + HasDay: true, + HasHour: true, + HasMinute: true, + HasSecond: true, + HasNanosecond: true, + HasPrecisionRange: precisionRange{ + Min: dcmtime.PrecisionYear, + Max: dcmtime.PrecisionMS5, + }, }, { Name: "PrecisionMS4-PositiveOffset", DTValue: "10100203040506.4567+0102", - Expected: time.Date(1010, 2, 3, 4, 5, 6, 456700000, time.FixedZone("", 3720)), + ExpectedTime: time.Date(1010, 2, 3, 4, 5, 6, 456700000, time.FixedZone("", 3720)), ExpectedPrecision: dcmtime.PrecisionMS4, - HasOffset: true, + ExpectedString: "1010-02-03 04:05:06.4567 +01:02", + ExpectedNoOffset: false, + HasMonth: true, + HasDay: true, + HasHour: true, + HasMinute: true, + HasSecond: true, + HasNanosecond: true, + HasPrecisionRange: precisionRange{ + Min: dcmtime.PrecisionYear, + Max: dcmtime.PrecisionMS4, + }, }, { Name: "PrecisionMS3-PositiveOffset", DTValue: "10100203040506.456+0102", - Expected: time.Date(1010, 2, 3, 4, 5, 6, 456000000, time.FixedZone("", 3720)), + ExpectedTime: time.Date(1010, 2, 3, 4, 5, 6, 456000000, time.FixedZone("", 3720)), ExpectedPrecision: dcmtime.PrecisionMS3, - HasOffset: true, + ExpectedString: "1010-02-03 04:05:06.456 +01:02", + ExpectedNoOffset: false, + HasMonth: true, + HasDay: true, + HasHour: true, + HasMinute: true, + HasSecond: true, + HasNanosecond: true, + HasPrecisionRange: precisionRange{ + Min: dcmtime.PrecisionYear, + Max: dcmtime.PrecisionMS3, + }, }, { Name: "PrecisionMS2-PositiveOffset", DTValue: "10100203040506.45+0102", - Expected: time.Date(1010, 2, 3, 4, 5, 6, 450000000, time.FixedZone("", 3720)), + ExpectedTime: time.Date(1010, 2, 3, 4, 5, 6, 450000000, time.FixedZone("", 3720)), ExpectedPrecision: dcmtime.PrecisionMS2, - HasOffset: true, + ExpectedString: "1010-02-03 04:05:06.45 +01:02", + ExpectedNoOffset: false, + HasMonth: true, + HasDay: true, + HasHour: true, + HasMinute: true, + HasSecond: true, + HasNanosecond: true, + HasPrecisionRange: precisionRange{ + Min: dcmtime.PrecisionYear, + Max: dcmtime.PrecisionMS2, + }, }, { Name: "PrecisionMS1-PositiveOffset", DTValue: "10100203040506.4+0102", - Expected: time.Date(1010, 2, 3, 4, 5, 6, 400000000, time.FixedZone("", 3720)), + ExpectedTime: time.Date(1010, 2, 3, 4, 5, 6, 400000000, time.FixedZone("", 3720)), ExpectedPrecision: dcmtime.PrecisionMS1, - HasOffset: true, + ExpectedString: "1010-02-03 04:05:06.4 +01:02", + ExpectedNoOffset: false, + HasMonth: true, + HasDay: true, + HasHour: true, + HasMinute: true, + HasSecond: true, + HasNanosecond: true, + HasPrecisionRange: precisionRange{ + Min: dcmtime.PrecisionYear, + Max: dcmtime.PrecisionMS1, + }, }, { Name: "PrecisionSeconds-PositiveOffset", DTValue: "10100203040506+0102", - Expected: time.Date(1010, 2, 3, 4, 5, 6, 0, time.FixedZone("", 3720)), + ExpectedTime: time.Date(1010, 2, 3, 4, 5, 6, 0, time.FixedZone("", 3720)), ExpectedPrecision: dcmtime.PrecisionSeconds, - HasOffset: true, + ExpectedString: "1010-02-03 04:05:06 +01:02", + ExpectedNoOffset: false, + HasMonth: true, + HasDay: true, + HasHour: true, + HasMinute: true, + HasSecond: true, + HasNanosecond: false, + HasPrecisionRange: precisionRange{ + Min: dcmtime.PrecisionYear, + Max: dcmtime.PrecisionSeconds, + }, }, { Name: "PrecisionMinutes-PositiveOffset", DTValue: "101002030405+0102", - Expected: time.Date(1010, 2, 3, 4, 5, 0, 0, time.FixedZone("", 3720)), + ExpectedTime: time.Date(1010, 2, 3, 4, 5, 0, 0, time.FixedZone("", 3720)), ExpectedPrecision: dcmtime.PrecisionMinutes, - HasOffset: true, + ExpectedString: "1010-02-03 04:05 +01:02", + ExpectedNoOffset: false, + HasMonth: true, + HasDay: true, + HasHour: true, + HasMinute: true, + HasSecond: false, + HasNanosecond: false, + HasPrecisionRange: precisionRange{ + Min: dcmtime.PrecisionYear, + Max: dcmtime.PrecisionMinutes, + }, }, { Name: "PrecisionHours-PositiveOffset", DTValue: "1010020304+0102", - Expected: time.Date(1010, 2, 3, 4, 0, 0, 0, time.FixedZone("", 3720)), + ExpectedTime: time.Date(1010, 2, 3, 4, 0, 0, 0, time.FixedZone("", 3720)), ExpectedPrecision: dcmtime.PrecisionHours, - HasOffset: true, + ExpectedString: "1010-02-03 04 +01:02", + ExpectedNoOffset: false, + HasMonth: true, + HasDay: true, + HasHour: true, + HasMinute: false, + HasSecond: false, + HasNanosecond: false, + HasPrecisionRange: precisionRange{ + Min: dcmtime.PrecisionYear, + Max: dcmtime.PrecisionHours, + }, }, { Name: "PrecisionDay-PositiveOffset", DTValue: "10100203+0102", - Expected: time.Date(1010, 2, 3, 0, 0, 0, 0, time.FixedZone("", 3720)), + ExpectedTime: time.Date(1010, 2, 3, 0, 0, 0, 0, time.FixedZone("", 3720)), ExpectedPrecision: dcmtime.PrecisionDay, - HasOffset: true, + ExpectedString: "1010-02-03 +01:02", + ExpectedNoOffset: false, + HasMonth: true, + HasDay: true, + HasHour: false, + HasMinute: false, + HasSecond: false, + HasNanosecond: false, + HasPrecisionRange: precisionRange{ + Min: dcmtime.PrecisionYear, + Max: dcmtime.PrecisionDay, + }, }, { Name: "PrecisionMonth-PositiveOffset", DTValue: "101002+0102", - Expected: time.Date(1010, 2, 1, 0, 0, 0, 0, time.FixedZone("", 3720)), + ExpectedTime: time.Date(1010, 2, 1, 0, 0, 0, 0, time.FixedZone("", 3720)), ExpectedPrecision: dcmtime.PrecisionMonth, - HasOffset: true, + ExpectedString: "1010-02 +01:02", + ExpectedNoOffset: false, + HasMonth: true, + HasDay: false, + HasHour: false, + HasMinute: false, + HasSecond: false, + HasNanosecond: false, + HasPrecisionRange: precisionRange{ + Min: dcmtime.PrecisionYear, + Max: dcmtime.PrecisionMonth, + }, }, { Name: "PrecisionYear-PositiveOffset", DTValue: "1010+0102", - Expected: time.Date(1010, 1, 1, 0, 0, 0, 0, time.FixedZone("", 3720)), + ExpectedTime: time.Date(1010, 1, 1, 0, 0, 0, 0, time.FixedZone("", 3720)), ExpectedPrecision: dcmtime.PrecisionYear, - HasOffset: true, + ExpectedString: "1010 +01:02", + ExpectedNoOffset: false, + HasMonth: false, + HasDay: false, + HasHour: false, + HasMinute: false, + HasSecond: false, + HasNanosecond: false, + HasPrecisionRange: precisionRange{ + Min: dcmtime.PrecisionYear, + Max: dcmtime.PrecisionYear, + }, }, { Name: "PrecisionFull-NegativeOffset", DTValue: "10100203040506.456789-0102", - Expected: time.Date(1010, 2, 3, 4, 5, 6, 456789000, time.FixedZone("", -3720)), + ExpectedTime: time.Date(1010, 2, 3, 4, 5, 6, 456789000, time.FixedZone("", -3720)), ExpectedPrecision: dcmtime.PrecisionFull, - HasOffset: true, + ExpectedString: "1010-02-03 04:05:06.456789 -01:02", + ExpectedNoOffset: false, + HasMonth: true, + HasDay: true, + HasHour: true, + HasMinute: true, + HasSecond: true, + HasNanosecond: true, + HasPrecisionRange: precisionRange{ + Min: dcmtime.PrecisionYear, + Max: dcmtime.PrecisionFull, + }, }, { Name: "PrecisionMS5-NegativeOffset", DTValue: "10100203040506.45678-0102", - Expected: time.Date(1010, 2, 3, 4, 5, 6, 456780000, time.FixedZone("", -3720)), + ExpectedTime: time.Date(1010, 2, 3, 4, 5, 6, 456780000, time.FixedZone("", -3720)), ExpectedPrecision: dcmtime.PrecisionMS5, - HasOffset: true, + ExpectedString: "1010-02-03 04:05:06.45678 -01:02", + ExpectedNoOffset: false, + HasMonth: true, + HasDay: true, + HasHour: true, + HasMinute: true, + HasSecond: true, + HasNanosecond: true, + HasPrecisionRange: precisionRange{ + Min: dcmtime.PrecisionYear, + Max: dcmtime.PrecisionMS5, + }, }, { Name: "PrecisionMS4-NegativeOffset", DTValue: "10100203040506.4567-0102", - Expected: time.Date(1010, 2, 3, 4, 5, 6, 456700000, time.FixedZone("", -3720)), + ExpectedTime: time.Date(1010, 2, 3, 4, 5, 6, 456700000, time.FixedZone("", -3720)), ExpectedPrecision: dcmtime.PrecisionMS4, - HasOffset: true, + ExpectedString: "1010-02-03 04:05:06.4567 -01:02", + ExpectedNoOffset: false, + HasMonth: true, + HasDay: true, + HasHour: true, + HasMinute: true, + HasSecond: true, + HasNanosecond: true, + HasPrecisionRange: precisionRange{ + Min: dcmtime.PrecisionYear, + Max: dcmtime.PrecisionMS4, + }, }, { Name: "PrecisionMS3-NegativeOffset", DTValue: "10100203040506.456-0102", - Expected: time.Date(1010, 2, 3, 4, 5, 6, 456000000, time.FixedZone("", -3720)), + ExpectedTime: time.Date(1010, 2, 3, 4, 5, 6, 456000000, time.FixedZone("", -3720)), ExpectedPrecision: dcmtime.PrecisionMS3, - HasOffset: true, + ExpectedString: "1010-02-03 04:05:06.456 -01:02", + ExpectedNoOffset: false, + HasMonth: true, + HasDay: true, + HasHour: true, + HasMinute: true, + HasSecond: true, + HasNanosecond: true, + HasPrecisionRange: precisionRange{ + Min: dcmtime.PrecisionYear, + Max: dcmtime.PrecisionMS3, + }, }, { Name: "PrecisionMS2-NegativeOffset", DTValue: "10100203040506.45-0102", - Expected: time.Date(1010, 2, 3, 4, 5, 6, 450000000, time.FixedZone("", -3720)), + ExpectedTime: time.Date(1010, 2, 3, 4, 5, 6, 450000000, time.FixedZone("", -3720)), ExpectedPrecision: dcmtime.PrecisionMS2, - HasOffset: true, + ExpectedString: "1010-02-03 04:05:06.45 -01:02", + ExpectedNoOffset: false, + HasMonth: true, + HasDay: true, + HasHour: true, + HasMinute: true, + HasSecond: true, + HasNanosecond: true, + HasPrecisionRange: precisionRange{ + Min: dcmtime.PrecisionYear, + Max: dcmtime.PrecisionMS2, + }, }, { Name: "PrecisionMS1-NegativeOffset", DTValue: "10100203040506.4-0102", - Expected: time.Date(1010, 2, 3, 4, 5, 6, 400000000, time.FixedZone("", -3720)), + ExpectedTime: time.Date(1010, 2, 3, 4, 5, 6, 400000000, time.FixedZone("", -3720)), ExpectedPrecision: dcmtime.PrecisionMS1, - HasOffset: true, + ExpectedString: "1010-02-03 04:05:06.4 -01:02", + ExpectedNoOffset: false, + HasMonth: true, + HasDay: true, + HasHour: true, + HasMinute: true, + HasSecond: true, + HasNanosecond: true, + HasPrecisionRange: precisionRange{ + Min: dcmtime.PrecisionYear, + Max: dcmtime.PrecisionMS1, + }, }, { Name: "PrecisionSeconds-NegativeOffset", DTValue: "10100203040506-0102", - Expected: time.Date(1010, 2, 3, 4, 5, 6, 000000000, time.FixedZone("", -3720)), + ExpectedTime: time.Date(1010, 2, 3, 4, 5, 6, 000000000, time.FixedZone("", -3720)), ExpectedPrecision: dcmtime.PrecisionSeconds, - HasOffset: true, + ExpectedString: "1010-02-03 04:05:06 -01:02", + ExpectedNoOffset: false, + HasMonth: true, + HasDay: true, + HasHour: true, + HasMinute: true, + HasSecond: true, + HasNanosecond: false, + HasPrecisionRange: precisionRange{ + Min: dcmtime.PrecisionYear, + Max: dcmtime.PrecisionSeconds, + }, }, { Name: "PrecisionMinutes-NegativeOffset", DTValue: "101002030405-0102", - Expected: time.Date(1010, 2, 3, 4, 5, 0, 000000000, time.FixedZone("", -3720)), + ExpectedTime: time.Date(1010, 2, 3, 4, 5, 0, 000000000, time.FixedZone("", -3720)), ExpectedPrecision: dcmtime.PrecisionMinutes, - HasOffset: true, + ExpectedString: "1010-02-03 04:05 -01:02", + ExpectedNoOffset: false, + HasMonth: true, + HasDay: true, + HasHour: true, + HasMinute: true, + HasSecond: false, + HasNanosecond: false, + HasPrecisionRange: precisionRange{ + Min: dcmtime.PrecisionYear, + Max: dcmtime.PrecisionMinutes, + }, }, { Name: "PrecisionHours-NegativeOffset", DTValue: "1010020304-0102", - Expected: time.Date(1010, 2, 3, 4, 0, 0, 000000000, time.FixedZone("", -3720)), + ExpectedTime: time.Date(1010, 2, 3, 4, 0, 0, 000000000, time.FixedZone("", -3720)), ExpectedPrecision: dcmtime.PrecisionHours, - HasOffset: true, + ExpectedString: "1010-02-03 04 -01:02", + ExpectedNoOffset: false, + HasMonth: true, + HasDay: true, + HasHour: true, + HasMinute: false, + HasSecond: false, + HasNanosecond: false, + HasPrecisionRange: precisionRange{ + Min: dcmtime.PrecisionYear, + Max: dcmtime.PrecisionHours, + }, }, { Name: "PrecisionDay-NegativeOffset", DTValue: "10100203-0102", - Expected: time.Date(1010, 2, 3, 0, 0, 0, 000000000, time.FixedZone("", -3720)), + ExpectedTime: time.Date(1010, 2, 3, 0, 0, 0, 000000000, time.FixedZone("", -3720)), ExpectedPrecision: dcmtime.PrecisionDay, - HasOffset: true, + ExpectedString: "1010-02-03 -01:02", + ExpectedNoOffset: false, + HasMonth: true, + HasDay: true, + HasHour: false, + HasMinute: false, + HasSecond: false, + HasNanosecond: false, + HasPrecisionRange: precisionRange{ + Min: dcmtime.PrecisionYear, + Max: dcmtime.PrecisionDay, + }, }, { Name: "PrecisionMonth-NegativeOffset", DTValue: "101002-0102", - Expected: time.Date(1010, 2, 1, 0, 0, 0, 000000000, time.FixedZone("", -3720)), + ExpectedTime: time.Date(1010, 2, 1, 0, 0, 0, 000000000, time.FixedZone("", -3720)), ExpectedPrecision: dcmtime.PrecisionMonth, - HasOffset: true, + ExpectedString: "1010-02 -01:02", + ExpectedNoOffset: false, + HasMonth: true, + HasDay: false, + HasHour: false, + HasMinute: false, + HasSecond: false, + HasNanosecond: false, + HasPrecisionRange: precisionRange{ + Min: dcmtime.PrecisionYear, + Max: dcmtime.PrecisionMonth, + }, }, { Name: "PrecisionYear-NegativeOffset", DTValue: "1010-0102", - Expected: time.Date(1010, 1, 1, 0, 0, 0, 000000000, time.FixedZone("", -3720)), + ExpectedTime: time.Date(1010, 1, 1, 0, 0, 0, 000000000, time.FixedZone("", -3720)), ExpectedPrecision: dcmtime.PrecisionYear, - HasOffset: true, + ExpectedString: "1010 -01:02", + ExpectedNoOffset: false, + HasMonth: false, + HasDay: false, + HasHour: false, + HasMinute: false, + HasSecond: false, + HasNanosecond: false, + HasPrecisionRange: precisionRange{ + Min: dcmtime.PrecisionYear, + Max: dcmtime.PrecisionYear, + }, }, { Name: "PrecisionFull-NoOffset", DTValue: "10100203040506.456789", - Expected: time.Date(1010, 2, 3, 4, 5, 6, 456789000, time.UTC), + ExpectedTime: time.Date(1010, 2, 3, 4, 5, 6, 456789000, time.UTC), ExpectedPrecision: dcmtime.PrecisionFull, - HasOffset: false, + ExpectedString: "1010-02-03 04:05:06.456789", + ExpectedNoOffset: true, + HasMonth: true, + HasDay: true, + HasHour: true, + HasMinute: true, + HasSecond: true, + HasNanosecond: true, + HasPrecisionRange: precisionRange{ + Min: dcmtime.PrecisionYear, + Max: dcmtime.PrecisionFull, + }, }, { Name: "PrecisionMS5-NoOffset", DTValue: "10100203040506.45678", - Expected: time.Date(1010, 2, 3, 4, 5, 6, 456780000, time.UTC), + ExpectedTime: time.Date(1010, 2, 3, 4, 5, 6, 456780000, time.UTC), ExpectedPrecision: dcmtime.PrecisionMS5, - HasOffset: false, + ExpectedString: "1010-02-03 04:05:06.45678", + ExpectedNoOffset: true, + HasMonth: true, + HasDay: true, + HasHour: true, + HasMinute: true, + HasSecond: true, + HasNanosecond: true, + HasPrecisionRange: precisionRange{ + Min: dcmtime.PrecisionYear, + Max: dcmtime.PrecisionMS5, + }, }, { Name: "PrecisionMS4-NoOffset", DTValue: "10100203040506.4567", - Expected: time.Date(1010, 2, 3, 4, 5, 6, 456700000, time.UTC), + ExpectedTime: time.Date(1010, 2, 3, 4, 5, 6, 456700000, time.UTC), ExpectedPrecision: dcmtime.PrecisionMS4, - HasOffset: false, + ExpectedString: "1010-02-03 04:05:06.4567", + ExpectedNoOffset: true, + HasMonth: true, + HasDay: true, + HasHour: true, + HasMinute: true, + HasSecond: true, + HasNanosecond: true, + HasPrecisionRange: precisionRange{ + Min: dcmtime.PrecisionYear, + Max: dcmtime.PrecisionMS4, + }, }, { Name: "PrecisionMS3-NoOffset", DTValue: "10100203040506.456", - Expected: time.Date(1010, 2, 3, 4, 5, 6, 456000000, time.UTC), + ExpectedTime: time.Date(1010, 2, 3, 4, 5, 6, 456000000, time.UTC), ExpectedPrecision: dcmtime.PrecisionMS3, - HasOffset: false, + ExpectedString: "1010-02-03 04:05:06.456", + ExpectedNoOffset: true, + HasMonth: true, + HasDay: true, + HasHour: true, + HasMinute: true, + HasSecond: true, + HasNanosecond: true, + HasPrecisionRange: precisionRange{ + Min: dcmtime.PrecisionYear, + Max: dcmtime.PrecisionMS3, + }, }, { Name: "PrecisionMS2-NoOffset", DTValue: "10100203040506.45", - Expected: time.Date(1010, 2, 3, 4, 5, 6, 450000000, time.UTC), + ExpectedTime: time.Date(1010, 2, 3, 4, 5, 6, 450000000, time.UTC), ExpectedPrecision: dcmtime.PrecisionMS2, - HasOffset: false, + ExpectedString: "1010-02-03 04:05:06.45", + ExpectedNoOffset: true, + HasMonth: true, + HasDay: true, + HasHour: true, + HasMinute: true, + HasSecond: true, + HasNanosecond: true, + HasPrecisionRange: precisionRange{ + Min: dcmtime.PrecisionYear, + Max: dcmtime.PrecisionMS2, + }, }, { Name: "PrecisionMS1-NoOffset", DTValue: "10100203040506.4", - Expected: time.Date(1010, 2, 3, 4, 5, 6, 400000000, time.UTC), + ExpectedTime: time.Date(1010, 2, 3, 4, 5, 6, 400000000, time.UTC), ExpectedPrecision: dcmtime.PrecisionMS1, - HasOffset: false, + ExpectedString: "1010-02-03 04:05:06.4", + ExpectedNoOffset: true, + HasMonth: true, + HasDay: true, + HasHour: true, + HasMinute: true, + HasSecond: true, + HasNanosecond: true, + HasPrecisionRange: precisionRange{ + Min: dcmtime.PrecisionYear, + Max: dcmtime.PrecisionMS1, + }, }, { Name: "PrecisionSeconds-NoOffset", DTValue: "10100203040506", - Expected: time.Date(1010, 2, 3, 4, 5, 6, 0, time.UTC), + ExpectedTime: time.Date(1010, 2, 3, 4, 5, 6, 0, time.UTC), ExpectedPrecision: dcmtime.PrecisionSeconds, - HasOffset: false, + ExpectedString: "1010-02-03 04:05:06", + ExpectedNoOffset: true, + HasMonth: true, + HasDay: true, + HasHour: true, + HasMinute: true, + HasSecond: true, + HasNanosecond: false, + HasPrecisionRange: precisionRange{ + Min: dcmtime.PrecisionYear, + Max: dcmtime.PrecisionSeconds, + }, }, { Name: "PrecisionMinutes-NoOffset", DTValue: "101002030405", - Expected: time.Date(1010, 2, 3, 4, 5, 0, 0, time.UTC), + ExpectedTime: time.Date(1010, 2, 3, 4, 5, 0, 0, time.UTC), ExpectedPrecision: dcmtime.PrecisionMinutes, - HasOffset: false, + ExpectedString: "1010-02-03 04:05", + ExpectedNoOffset: true, + HasMonth: true, + HasDay: true, + HasHour: true, + HasMinute: true, + HasSecond: false, + HasNanosecond: false, + HasPrecisionRange: precisionRange{ + Min: dcmtime.PrecisionYear, + Max: dcmtime.PrecisionMinutes, + }, }, { Name: "PrecisionHours-NoOffset", DTValue: "1010020304", - Expected: time.Date(1010, 2, 3, 4, 0, 0, 0, time.UTC), + ExpectedTime: time.Date(1010, 2, 3, 4, 0, 0, 0, time.UTC), ExpectedPrecision: dcmtime.PrecisionHours, - HasOffset: false, + ExpectedString: "1010-02-03 04", + ExpectedNoOffset: true, + HasMonth: true, + HasDay: true, + HasHour: true, + HasMinute: false, + HasSecond: false, + HasNanosecond: false, + HasPrecisionRange: precisionRange{ + Min: dcmtime.PrecisionYear, + Max: dcmtime.PrecisionHours, + }, }, // Full value, no offset, no hours { Name: "PrecisionDay-NoOffset", DTValue: "10100203", - Expected: time.Date(1010, 2, 3, 0, 0, 0, 0, time.UTC), + ExpectedTime: time.Date(1010, 2, 3, 0, 0, 0, 0, time.UTC), ExpectedPrecision: dcmtime.PrecisionDay, - HasOffset: false, + ExpectedString: "1010-02-03", + ExpectedNoOffset: true, + HasMonth: true, + HasDay: true, + HasHour: false, + HasMinute: false, + HasSecond: false, + HasNanosecond: false, + HasPrecisionRange: precisionRange{ + Min: dcmtime.PrecisionYear, + Max: dcmtime.PrecisionDay, + }, }, // Full value, no offset, no days { Name: "PrecisionMonth-NoOffset", DTValue: "101002", - Expected: time.Date(1010, 2, 1, 0, 0, 0, 0, time.UTC), + ExpectedTime: time.Date(1010, 2, 1, 0, 0, 0, 0, time.UTC), ExpectedPrecision: dcmtime.PrecisionMonth, - HasOffset: false, + ExpectedString: "1010-02", + ExpectedNoOffset: true, + HasMonth: true, + HasDay: false, + HasHour: false, + HasMinute: false, + HasSecond: false, + HasNanosecond: false, + HasPrecisionRange: precisionRange{ + Min: dcmtime.PrecisionYear, + Max: dcmtime.PrecisionMonth, + }, }, // Full value, no offset, no month { Name: "PrecisionYear-NoOffset", DTValue: "1010", - Expected: time.Date(1010, 1, 1, 0, 0, 0, 0, time.UTC), + ExpectedTime: time.Date(1010, 1, 1, 0, 0, 0, 0, time.UTC), ExpectedPrecision: dcmtime.PrecisionYear, - HasOffset: false, + ExpectedString: "1010", + ExpectedNoOffset: true, + HasMonth: false, + HasDay: false, + HasHour: false, + HasMinute: false, + HasSecond: false, + HasNanosecond: false, + HasPrecisionRange: precisionRange{ + Min: dcmtime.PrecisionYear, + Max: dcmtime.PrecisionYear, + }, }, } for _, tc := range testCases { t.Run(tc.Name, func(t *testing.T) { - parsed, err := dcmtime.ParseDatetime(tc.DTValue) - if err != nil { - t.Fatal("parse err:", err) - } - if !tc.Expected.Equal(parsed.Time) { - t.Errorf( - "parsed time (%v) != expected (%v)", - parsed.Time, - tc.Expected, - ) + var parsed dcmtime.Datetime - } + t.Run("ParseDatetime()", func(t *testing.T) { + var err error + + parsed, err = dcmtime.ParseDatetime(tc.DTValue) + if err != nil { + t.Fatal("parse err:", err) + } + + if !tc.ExpectedTime.Equal(parsed.Time) { + t.Errorf( + "Datetime.Time: expected %v, got %v", + tc.ExpectedTime, + parsed.Time, + ) + + } + + if parsed.Precision != tc.ExpectedPrecision { + t.Errorf( + "Datetime.Precision: expected %v, got %v", + tc.ExpectedPrecision.String(), + parsed.Precision.String(), + ) + } + + if parsed.NoOffset != tc.ExpectedNoOffset { + t.Errorf( + "Datetime.NoOffset: expected %v, got %v", + tc.ExpectedNoOffset, + parsed.NoOffset, + ) + } + }) + + 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 { + t.Fatalf( + "got String() value '%v', expected '%v'", + stringVal, + tc.ExpectedString, + ) + } + }) + + t.Run("DCM()", func(t *testing.T) { + dcmVal := parsed.DCM() + if dcmVal != tc.DTValue { + t.Fatalf( + "got DCM() value '%v', expected '%v'", dcmVal, tc.DTValue, + ) + } + }) + + 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("Hour()", func(t *testing.T) { + hour, ok := parsed.Hour() + checkDateHelperOutput(t, "Hour()", parsed.Time.Hour(), hour, tc.HasHour, ok) + }) + + t.Run("Minute()", func(t *testing.T) { + minute, ok := parsed.Minute() + checkDateHelperOutput(t, "Minute()", parsed.Time.Minute(), minute, tc.HasMinute, ok) + }) + + t.Run("Second()", func(t *testing.T) { + minute, ok := parsed.Second() + checkDateHelperOutput(t, "Second()", parsed.Time.Second(), minute, tc.HasSecond, ok) + }) + + t.Run("Nanosecond()", func(t *testing.T) { + nanos, ok := parsed.Nanosecond() + checkDateHelperOutput(t, "Nanosecond()", parsed.Time.Nanosecond(), nanos, tc.HasNanosecond, ok) + }) + + t.Run("HasPrecision()", func(t *testing.T) { + checkHasPrecision(t, parsed, tc.HasPrecisionRange, dtPrecisionOmits) + }) - if parsed.Precision != tc.ExpectedPrecision { - t.Errorf( - "precision: expected %v, got %v", - tc.ExpectedPrecision.String(), - parsed.Precision.String(), - ) - } }) } } @@ -467,7 +989,7 @@ func TestParseDatetimeErr(t *testing.T) { } } -func TestDatetime_Methods(t *testing.T) { +func TestDatetime_PrecisionTrimming(t *testing.T) { testCases := []struct { Name string TimeVal time.Time @@ -715,3 +1237,155 @@ func TestDatetime_Methods(t *testing.T) { }) } } + +// TestDatetime_SaneDefaults tests that instantiating a new Datetime object with just +// the Time field specified yields a reasonable result. +func TestDatetime_SaneDefaults(t *testing.T) { + newValue := dcmtime.Datetime{ + Time: time.Date(2021, 03, 16, 13, 45, 32, 123456000, time.FixedZone("", 60)), + } + + dcmVal := newValue.DCM() + expexted := "20210316134532.123456+0001" + if dcmVal != expexted { + t.Errorf("DCM(): expected '%v', but got '%v'", expexted, dcmVal) + } +} + +func TestCombineDateAndTime(t *testing.T) { + testCases := []struct { + Name string + DCMDate string + DCMTime string + Location *time.Location + ExpectedDCMDatetime string + ExpectedPrecision dcmtime.PrecisionLevel + }{ + { + Name: "PrecisionFull_WithOffset", + DCMDate: "20210317", + DCMTime: "102345.123456", + Location: time.FixedZone("", 120), + ExpectedDCMDatetime: "20210317102345.123456+0002", + ExpectedPrecision: dcmtime.PrecisionFull, + }, + { + Name: "PrecisionHours_WithOffset", + DCMDate: "20210317", + DCMTime: "10", + Location: time.FixedZone("", 120), + ExpectedDCMDatetime: "2021031710+0002", + ExpectedPrecision: dcmtime.PrecisionHours, + }, + { + Name: "PrecisionFull_NoOffset", + DCMDate: "20210317", + DCMTime: "102345.123456", + Location: nil, + ExpectedDCMDatetime: "20210317102345.123456", + ExpectedPrecision: dcmtime.PrecisionFull, + }, + } + + for _, tc := range testCases { + t.Run(tc.Name, func(t *testing.T) { + dateValue, err := dcmtime.ParseDate(tc.DCMDate) + if err != nil { + t.Fatalf("error parrsing date value '%v': %v", tc.DCMDate, err) + } + + timeValue, err := dcmtime.ParseTime(tc.DCMTime) + if err != nil { + t.Fatalf("error parrsing time value '%v': %v", tc.DCMTime, err) + } + + t.Run("Date.Combine()", func(t *testing.T) { + datetime, err := dateValue.Combine(timeValue, tc.Location) + if err != nil { + t.Fatalf("Date.Combine(%v, %v) error: %v", timeValue, tc.Location, err) + } + + dcmValue := datetime.DCM() + if dcmValue != tc.ExpectedDCMDatetime { + t.Errorf("expected combined datetime of '%v', got '%v'", tc.ExpectedDCMDatetime, dcmValue) + } + + if datetime.Precision != tc.ExpectedPrecision { + t.Errorf("expected combined precision of '%v', got '%v'", tc.ExpectedPrecision, datetime.Precision) + } + }) + + t.Run("Time.Combine()", func(t *testing.T) { + datetime, err := timeValue.Combine(dateValue, tc.Location) + if err != nil { + t.Fatalf("Date.Combine(%v, %v) error: %v", timeValue, tc.Location, err) + } + + dcmValue := datetime.DCM() + if dcmValue != tc.ExpectedDCMDatetime { + t.Errorf("expected combined datetime of '%v', got '%v'", tc.ExpectedDCMDatetime, dcmValue) + } + + if datetime.Precision != tc.ExpectedPrecision { + t.Errorf("expected combined precision of '%v', got '%v'", tc.ExpectedPrecision, datetime.Precision) + } + }) + }) + } +} + +func TestErrCombineDateAndTime_DateLimitedPrecision(t *testing.T) { + testCases := []struct { + Name string + DCMDate string + DCMTime string + ExpectedErr string + }{ + { + Name: "PrecisionMonth", + DCMDate: "202103", + DCMTime: "120000", + ExpectedErr: "DA value must have full precision, got 'MONTH'", + }, + { + Name: "PrecisionYear", + DCMDate: "2021", + DCMTime: "120000", + ExpectedErr: "DA value must have full precision, got 'YEAR'", + }, + } + + for _, tc := range testCases { + dateValue, err := dcmtime.ParseDate(tc.DCMDate) + if err != nil { + t.Fatalf("error parrsing date value '%v': %v", tc.DCMDate, err) + } + + timeValue, err := dcmtime.ParseTime(tc.DCMTime) + if err != nil { + t.Fatalf("error parrsing time value '%v': %v", tc.DCMTime, err) + } + + t.Run("Date.Combine()", func(t *testing.T) { + _, err = dateValue.Combine(timeValue, nil) + if err == nil { + t.Fatalf("Date.Combine(): expected error, got nil") + } + + if err.Error() != tc.ExpectedErr { + t.Errorf("unexpected error text: %v", err) + } + }) + + t.Run("Time.Combine()", func(t *testing.T) { + _, err = timeValue.Combine(dateValue, nil) + if err == nil { + t.Fatalf("Date.Combine(): expected error, got nil") + } + + if err.Error() != tc.ExpectedErr { + t.Errorf("unexpected error text: %v", err) + } + }) + } +} diff --git a/pkg/dcmtime/example_test.go b/pkg/dcmtime/example_test.go index bb69da36..38378de6 100644 --- a/pkg/dcmtime/example_test.go +++ b/pkg/dcmtime/example_test.go @@ -10,7 +10,6 @@ func ExampleParseDate() { // This is a DA value like we would expect daString := "20201210" - // We are parsing the date string without allowing nema da, err := ParseDate(daString) if err != nil { panic(err) @@ -29,26 +28,46 @@ func ExampleParseDate_lessPrecision() { // This is a DA value like we would expect, but it is missing the day value. daString := "202012" - // We are parsing the date string without allowing NEMA-300 formatted dates. da, err := ParseDate(daString) if err != nil { panic(err) } // The resulting da value contains a native time.Time value. - fmt.Println("TIME MONTH:", da.Time.Month()) + fmt.Println("TIME MONTH :", da.Time.Month()) // It also reports the precision, of the value. This value is Precision.Month, // so we know that even though da.Time.Day() will equal 1, we should disregard it. - fmt.Println("PRECISION :", da.Precision) + fmt.Println("PRECISION :", da.Precision) // This date is not a NEMA-300 date. - fmt.Println("IS NEMA :", da.IsNEMA) + fmt.Println("IS NEMA :", da.IsNEMA) + + // Our Date value has some methods similar to time.Time's methods, but also + // returns presence information since not all DICOM dates contain all date + // components. + // + // Try to get the Month value. Our value included a month, so 'ok' will be true. + if month, ok := da.Month(); ok { + fmt.Println("MONTH :", month) + } + + // Try to get the Day value. Because minutes are not included, 'ok' will be false + // and this will not print. + if minute, ok := da.Day(); ok { + fmt.Println("DAY:", minute) + } + + // We can also easily check if the value contains a certain precision: + hasMonth := da.HasPrecision(PrecisionMonth) + fmt.Println("HAS MONTH :", hasMonth) // Output: - // TIME MONTH: December - // PRECISION : MONTH - // IS NEMA : false + // TIME MONTH : December + // PRECISION : MONTH + // IS NEMA : false + // MONTH : December + // HAS MONTH : true } // Parse a NEMA date string. @@ -82,7 +101,7 @@ func ExampleDate_create() { panic(err) } - // Create a nw DA object like so: + // Create a nw Date object like so: da := Date{ Time: date, Precision: PrecisionFull, @@ -110,7 +129,7 @@ func ExampleDate_createNEMA300() { panic(err) } - // Create a nw DA object like so: + // Create a nw Date object like so: da := Date{ Time: date, Precision: PrecisionFull, @@ -135,7 +154,7 @@ func ExampleDate_precisionYear() { panic(err) } - // Create a nw DA object that only represent the year like so: + // Create a nw Date object that only represent the year and month like so: da := Date{ Time: date, Precision: PrecisionMonth, @@ -153,6 +172,33 @@ func ExampleDate_precisionYear() { // STRING: 2006-01 } +func ExampleDate_Combine() { + 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 +} + func ExampleParseTime() { // This is a TM value like we would expect for 12:30:01 and 400 microseconds tmString := "123001.000431" @@ -187,21 +233,42 @@ func ExampleParseTime_precisionMS() { // PRECISION : MS3 } -func ExampleParseTime_precisionHour() { - // This is a TM value like we would expect for 12:30:01 and 400 microseconds - tmString := "12" +func ExampleParseTime_precisionMinute() { + // This is a TM value like we would expect for 12:35 + tmString := "1235" tm, err := ParseTime(tmString) if err != nil { panic(err) } - fmt.Println("TIME VALUE:", tm.Time) - fmt.Println("PRECISION :", tm.Precision) + fmt.Println("TIME VALUE :", tm.Time) + fmt.Println("PRECISION :", tm.Precision) + + // Our Time value has some methods similar to time.Time's methods, but also + // returns presence information since not all DICOM times contain all time + // components. + // + // Try to get the Minute value. Our value included a day, so 'ok' will be true. + if day, ok := tm.Minute(); ok { + fmt.Println("MINUTE VALUE :", day) + } + + // Try to get the Second value. Because minutes are not included, 'ok' will be false + // and this will not print. + if minute, ok := tm.Second(); ok { + fmt.Println("SECOND VALUE :", minute) + } + + // We can also easily check if the value contains a certain precision: + hasSeconds := tm.HasPrecision(PrecisionSeconds) + fmt.Println("HAS SECONDS :", hasSeconds) // Output: - // TIME VALUE: 0001-01-01 12:00:00 +0000 +0000 - // PRECISION : HOURS + // TIME VALUE : 0001-01-01 12:35:00 +0000 +0000 + // PRECISION : MINUTES + // MINUTE VALUE : 35 + // HAS SECONDS : false } func ExampleTime_create() { @@ -214,7 +281,7 @@ func ExampleTime_create() { panic(err) } - // Create a nw TM object like so: + // Create a nw Time object like so: tm := Time{ Time: timeVal, Precision: PrecisionFull, @@ -241,7 +308,7 @@ func ExampleTime_precision3MS() { panic(err) } - // Create a nw TM object like so: + // Create a nw Time object like so: tm := Time{ Time: timeVal, Precision: PrecisionMS3, @@ -268,7 +335,7 @@ func ExampleTime_precisionMinutes() { panic(err) } - // Create a nw TM object like so: + // Create a nw Time object like so: tm := Time{ Time: timeVal, Precision: PrecisionMinutes, @@ -285,67 +352,112 @@ func ExampleTime_precisionMinutes() { // STRING: 15:04 } +func ExampleTime_Combine() { + 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 := tmParsed.Combine(daParsed, 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 +} + // Parse a datetime string. func ExampleParseDatetime() { // This is a DT value like we would expect - daString := "20201210123001.000431+0100" + dtString := "20201210123001.000431+0100" - // We are parsing the date string without allowing nema - dt, err := ParseDatetime(daString) + dt, err := ParseDatetime(dtString) if err != nil { panic(err) } fmt.Println("TIME VALUE:", dt.Time) fmt.Println("PRECISION :", dt.Precision) - fmt.Println("HAS OFFSET:", dt.NoOffset) + fmt.Println("NO OFFSET :", dt.NoOffset) // Output: // TIME VALUE: 2020-12-10 12:30:01.000431 +0100 +0100 // PRECISION : FULL - // HAS OFFSET: true + // NO OFFSET : false } // Parse a datetime string with no timezone. func ExampleParseDatetime_noTimezone() { // This is a DT value like we would expect - daString := "20201210123001.000431" + dtString := "20201210123001.000431" - // We are parsing the date string without allowing nema - dt, err := ParseDatetime(daString) + dt, err := ParseDatetime(dtString) if err != nil { panic(err) } fmt.Println("TIME VALUE:", dt.Time) fmt.Println("PRECISION :", dt.Precision) - fmt.Println("HAS OFFSET:", dt.NoOffset) + fmt.Println("NO OFFSET :", dt.NoOffset) // Output: // TIME VALUE: 2020-12-10 12:30:01.000431 +0000 +0000 // PRECISION : FULL - // HAS OFFSET: false + // NO OFFSET : true } // Parse a datetime string with no timezone. func ExampleParseDatetime_precisionHour() { // This is a DT value like we would expect - daString := "2020121012" + dtString := "2020121012" - // We are parsing the date string without allowing nema - dt, err := ParseDatetime(daString) + dt, err := ParseDatetime(dtString) if err != nil { panic(err) } - fmt.Println("TIME VALUE:", dt.Time) - fmt.Println("PRECISION :", dt.Precision) - fmt.Println("HAS OFFSET:", dt.NoOffset) + fmt.Println("TIME VALUE :", dt.Time) + fmt.Println("PRECISION :", dt.Precision) + fmt.Println("NO OFFSET :", dt.NoOffset) + + // 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 - // HAS OFFSET: false + // TIME VALUE : 2020-12-10 12:00:00 +0000 +0000 + // PRECISION : HOURS + // NO OFFSET : true + // DAY VALUE : 10 + // HAS MINUTES : false } func ExampleDatetime_create() { @@ -358,7 +470,7 @@ func ExampleDatetime_create() { panic(err) } - // Create a nw TM object like so: + // Create a nw Datetime object like so: dt := Datetime{ Time: timeVal, Precision: PrecisionFull, @@ -386,7 +498,7 @@ func ExampleDatetime_createNoOffset() { panic(err) } - // Create a nw TM object like so: + // Create a nw Datetime object like so: dt := Datetime{ Time: timeVal, Precision: PrecisionFull, @@ -410,7 +522,7 @@ func ExampleDatetime_precisionMinute() { panic(err) } - // Create a nw TM object like so: + // Create a nw Datetime object like so: dt := Datetime{ Time: timeVal, Precision: PrecisionMinutes, @@ -434,7 +546,7 @@ func ExampleDatetime_precisionMinuteWithOffset() { panic(err) } - // Create a nw TM object like so: + // Create a nw Datetime object like so: dt := Datetime{ Time: timeVal, Precision: PrecisionMinutes, diff --git a/pkg/dcmtime/common.go b/pkg/dcmtime/helpers.go similarity index 51% rename from pkg/dcmtime/common.go rename to pkg/dcmtime/helpers.go index 47a9d47f..4400f9e0 100644 --- a/pkg/dcmtime/common.go +++ b/pkg/dcmtime/helpers.go @@ -8,19 +8,40 @@ import ( "time" ) -// isIncluded returns whether `check` is included in `limit`. +// hasPrecision returns whether `check` is included in `limit`. // // Example: to test whether seconds should be included, you would: -// isIncluded(PrecisionSeconds, [caller-passed-limit]) -func isIncluded(check PrecisionLevel, precision PrecisionLevel) bool { - return check <= precision +// hasPrecision(PrecisionSeconds, [caller-passed-limit]) +func hasPrecision(check PrecisionLevel, precision PrecisionLevel) bool { + return check >= precision +} + +// hasPrecisionOmits is the underlying call made on [type].HasPrecision() call. +// +// check is the value passed in by the caller to check. +// +// valuePrecision is the precision of the value we are checking about. +// +// omits are a set of Precision levels the value cannot have. For instance. Date can +// have a precision of PrecisionYear, but not PrecisionSeconds +func hasPrecisionOmits(check PrecisionLevel, valuePrecision PrecisionLevel, omits precisionRange) bool { + if check < valuePrecision { + return false + } + + // If this value falls within the omit range, it is false. + if omits.Contains(check) { + return false + } + + return true } // truncateMilliseconds truncate nanosecond time.Time value to arbitrary precision. func truncateMilliseconds(nanoSeconds int, precision PrecisionLevel) (millis string) { milliseconds := nanoSeconds / 1000 millis = fmt.Sprintf("%06d", milliseconds) - millis = millis[:6-(PrecisionFull-precision)] + millis = millis[:6-(PrecisionFull+precision)] return millis } @@ -69,7 +90,7 @@ func extractDurationInfo(subMatches []string, index int, isFractal bool) (durati // get our nano-seconds. missingPlaces := 9 - len(valueStr) valueStr = valueStr + strings.Repeat("0", missingPlaces) - info.FractalPrecision = PrecisionFull - PrecisionLevel(missingPlaces-3) + info.FractalPrecision = PrecisionFull + PrecisionLevel(missingPlaces-3) } // If our info is present, parse the value into an int. @@ -101,3 +122,69 @@ func updatePrecision(info durationInfo, current, infoLevel PrecisionLevel, level } return infoLevel } + +// precisionRange defines the (inclusive) minimum and maximum precision for omits. +type precisionRange struct { + // Min is the the minimum precision in this range (inclusive). + Min PrecisionLevel + // Max is the maximum precision in this range (inclusive). + Max PrecisionLevel +} + +// Contains returns true if a value falls within the given range (inclusive). +func (pRange precisionRange) Contains(val PrecisionLevel) bool { + // Because the precision iota is reversed to make PrecisionFull the default, we + // need to invert the normal range logic here. + if val > pRange.Min { + return false + } + + if val < pRange.Max { + return false + } + + // If this value falls within the omit range, it is false. + return true +} + +// combineDAAndTM is the backing function for both DA.Combine and TM.Combine +// +// If no location is given time.FixedZone("", 0) will be used, and NoOffset will be +// set to 'true'. +func combineDateAndTime(da Date, tm Time, location *time.Location) (Datetime, error) { + // User-created values might use PrecisionDay in place of PrecisionFull for + // full-precision dates. + // + // We need a full precision date, because Datetime values can only elide fom the + // end. Since combining a Date and Time value implies the Time has at least an + // Hour value, no Date values can be missing. + if !da.HasPrecision(PrecisionFull) && !da.HasPrecision(PrecisionDay) { + return Datetime{}, fmt.Errorf( + "DA value must have full precision, got '%v'", da.Precision, + ) + } + + noOffset := false + if location == nil { + location = time.FixedZone("", 0) + noOffset = true + } + + return Datetime{ + Time: time.Date( + // Date values. + da.Time.Year(), + da.Time.Month(), + da.Time.Day(), + // Time values. + tm.Time.Hour(), + tm.Time.Minute(), + tm.Time.Second(), + tm.Time.Nanosecond(), + location, + ), + // We'll inherit our Precision from the Time value. + Precision: tm.Precision, + NoOffset: noOffset, + }, nil +} diff --git a/pkg/dcmtime/helpers_test.go b/pkg/dcmtime/helpers_test.go new file mode 100644 index 00000000..5ac51900 --- /dev/null +++ b/pkg/dcmtime/helpers_test.go @@ -0,0 +1,51 @@ +package dcmtime_test + +import ( + "github.com/suyashkumar/dicom/pkg/dcmtime" + "testing" +) + +// precisionRange defines the (inclusive) minimum and maximum precision to be expected. +type precisionRange struct { + Min dcmtime.PrecisionLevel + Max dcmtime.PrecisionLevel +} + +// Contains returns true if a value falls within the given range (inclusive). +func (pRange precisionRange) Contains(val dcmtime.PrecisionLevel) bool { + if val > pRange.Min { + return false + } + + if val < pRange.Max { + return false + } + // If this value falls within the omit range, it is false. + return true +} + +// precisionChecker defines an interface for types that can check their precision. +type precisionChecker interface { + HasPrecision(check dcmtime.PrecisionLevel) bool +} + +// checkHasPrecision checks that we get the expected results from a type with +// hasPrecisionOmits +func checkHasPrecision(t *testing.T, value precisionChecker, expectedRange precisionRange, omits precisionRange) { + for p := dcmtime.PrecisionFull; p <= dcmtime.PrecisionYear; p++ { + expected := expectedRange.Contains(p) && !omits.Contains(p) + result := value.HasPrecision(p) + + t.Logf("Has Precision %v: %v", p, result) + + if result != expected { + t.Errorf( + "expected value '%v' HasPrecision() to be '%v' for precision '%v', got '%v'", + value, + expected, + p, + result, + ) + } + } +} diff --git a/pkg/dcmtime/interfaces_test.go b/pkg/dcmtime/interfaces_test.go new file mode 100644 index 00000000..48f4fdc1 --- /dev/null +++ b/pkg/dcmtime/interfaces_test.go @@ -0,0 +1,241 @@ +package dcmtime_test + +import ( + "fmt" + "github.com/suyashkumar/dicom/pkg/dcmtime" + "reflect" + "testing" + "time" +) + +/* +This file is here to ensure that certain dcm types implement certain interfaces. +*/ + +type hasTime interface { + GetTime() time.Time +} + +type hasPrecision interface { + GetPrecision() dcmtime.PrecisionLevel +} + +type hasYear interface { + Year() int +} + +type hasMonth interface { + Month() (time.Month, bool) +} + +type hasDay interface { + Day() (int, bool) +} + +type hasHour interface { + Hour() (int, bool) +} + +type hasMinute interface { + Minute() (int, bool) +} + +type hasSecond interface { + Second() (int, bool) +} + +type hasNanosecond interface { + Nanosecond() (int, bool) +} + +type dcmVal interface { + DCM() string +} + +type isPrecisionChecker interface { + HasPrecision(level dcmtime.PrecisionLevel) bool +} + +// TestCommonInterfaces ensures that certain types continue to share certain common +// interfaces. +func TestCommonInterfaces(t *testing.T) { + t.Run("hasTime", func(t *testing.T) { + testCases := []interface{}{ + dcmtime.Time{}, + dcmtime.Date{}, + dcmtime.Datetime{}, + } + + for _, tc := range testCases { + t.Run(reflect.TypeOf(tc).String(), func(t *testing.T) { + _, ok := tc.(hasTime) + if !ok { + t.Errorf("%v does not implement hasTime", reflect.TypeOf(tc)) + } + }) + } + }) + + t.Run("hasPrecision", func(t *testing.T) { + testCases := []interface{}{ + dcmtime.Time{}, + dcmtime.Date{}, + dcmtime.Datetime{}, + } + + for _, tc := range testCases { + t.Run(reflect.TypeOf(tc).String(), func(t *testing.T) { + _, ok := tc.(hasPrecision) + if !ok { + t.Errorf("%v does not implement hasPrecision", reflect.TypeOf(tc)) + } + }) + } + }) + + t.Run("hasYear", func(t *testing.T) { + testCases := []interface{}{ + dcmtime.Date{}, + dcmtime.Datetime{}, + } + + for _, tc := range testCases { + t.Run(reflect.TypeOf(tc).String(), func(t *testing.T) { + _, ok := tc.(hasYear) + if !ok { + t.Errorf("%v does not implement hasYear", reflect.TypeOf(tc)) + } + }) + } + }) + + t.Run("hasMonth", func(t *testing.T) { + testCases := []interface{}{ + dcmtime.Date{}, + dcmtime.Datetime{}, + } + + for _, tc := range testCases { + t.Run(reflect.TypeOf(tc).String(), func(t *testing.T) { + _, ok := tc.(hasMonth) + if !ok { + t.Errorf("%v does not implement hasMonth", reflect.TypeOf(tc)) + } + }) + } + }) + + t.Run("hasDay", func(t *testing.T) { + testCases := []interface{}{ + dcmtime.Date{}, + dcmtime.Datetime{}, + } + + for _, tc := range testCases { + t.Run(reflect.TypeOf(tc).String(), func(t *testing.T) { + _, ok := tc.(hasDay) + if !ok { + t.Errorf("%v does not implement hasDay", reflect.TypeOf(tc)) + } + }) + } + }) + + t.Run("hasHour", func(t *testing.T) { + testCases := []interface{}{ + dcmtime.Time{}, + dcmtime.Datetime{}, + } + + for _, tc := range testCases { + t.Run(reflect.TypeOf(tc).String(), func(t *testing.T) { + _, ok := tc.(hasHour) + if !ok { + t.Errorf("%v does not implement hasHour", reflect.TypeOf(tc)) + } + }) + } + }) + + t.Run("hasMinute", func(t *testing.T) { + testCases := []interface{}{ + dcmtime.Time{}, + dcmtime.Datetime{}, + } + + for _, tc := range testCases { + t.Run(reflect.TypeOf(tc).String(), func(t *testing.T) { + _, ok := tc.(hasMinute) + if !ok { + t.Errorf("%v does not implement hasMinute", reflect.TypeOf(tc)) + } + }) + } + }) + + t.Run("hasSecond", func(t *testing.T) { + testCases := []interface{}{ + dcmtime.Time{}, + dcmtime.Datetime{}, + } + + for _, tc := range testCases { + t.Run(reflect.TypeOf(tc).String(), func(t *testing.T) { + _, ok := tc.(hasSecond) + if !ok { + t.Errorf("%v does not implement hasSecond", reflect.TypeOf(tc)) + } + }) + } + }) + + t.Run("hasNanosecond", func(t *testing.T) { + testCases := []interface{}{ + dcmtime.Time{}, + dcmtime.Datetime{}, + } + + for _, tc := range testCases { + t.Run(reflect.TypeOf(tc).String(), func(t *testing.T) { + _, ok := tc.(hasNanosecond) + if !ok { + t.Errorf("%v does not implement hasNanosecond", reflect.TypeOf(tc)) + } + }) + } + }) + + t.Run("dcmVal", func(t *testing.T) { + testCases := []interface{}{ + dcmtime.Date{}, + dcmtime.Time{}, + dcmtime.Datetime{}, + } + + for _, tc := range testCases { + t.Run(reflect.TypeOf(tc).String(), func(t *testing.T) { + _, ok := tc.(dcmVal) + if !ok { + t.Errorf("%v does not implement dcmVal", reflect.TypeOf(tc)) + } + }) + } + }) + + t.Run("fmt.Stringer", func(t *testing.T) { + testCases := []interface{}{ + dcmtime.Date{}, + dcmtime.Time{}, + dcmtime.Datetime{}, + } + + for _, tc := range testCases { + t.Run(reflect.TypeOf(tc).String(), func(t *testing.T) { + _, ok := tc.(fmt.Stringer) + if !ok { + t.Errorf("%v does not implement fmt.Stringer", reflect.TypeOf(tc)) + } + }) + } + }) +} diff --git a/pkg/dcmtime/precision.go b/pkg/dcmtime/precision.go index b6a55dad..36735870 100644 --- a/pkg/dcmtime/precision.go +++ b/pkg/dcmtime/precision.go @@ -45,37 +45,37 @@ func (level PrecisionLevel) String() string { } const ( - // PrecisionYear indicated that a given dcm time value is only precise to the year. - PrecisionYear PrecisionLevel = iota - // PrecisionMonth indicated that a given dcm time value is only precise to the - // month. - PrecisionMonth - // PrecisionDay indicated that a given dcm time value is only precise to the day. - PrecisionDay - // PrecisionHours indicated that a given dcm time value is only precise to the hour. - PrecisionHours - // PrecisionMinutes indicated that a given dcm time value is only precise to the - // minute. - PrecisionMinutes - // PrecisionSeconds indicated that a given dcm time value is only precise to the - // second. - PrecisionSeconds - // PrecisionMS1 indicated that a given dcm time value is only precise to 1 - // millisecond place (1/10 of a second). - PrecisionMS1 - // PrecisionMS2 indicated that a given dcm time value is only precise to 2 - // millisecond place (1/100 of a second). - PrecisionMS2 - // PrecisionMS3 indicated that a given dcm time value is only precise to 3 - // millisecond place (1/1000 of a second). - PrecisionMS3 - // PrecisionMS4 indicated that a given dcm time value is only precise to 4 - // millisecond place (1/10000 of a second). - PrecisionMS4 + // PrecisionFull indicates that a given dcm time value is precise to the full extent + // it is able to be. + PrecisionFull PrecisionLevel = iota // PrecisionMS5 indicated that a given dcm time value is only precise to 4 // millisecond place (1/100000 of a second). PrecisionMS5 - // PrecisionFull indicates that a given dcm time value is precise to the full extent - // it is able to be. - PrecisionFull + // PrecisionMS4 indicated that a given dcm time value is only precise to 4 + // millisecond place (1/10000 of a second). + PrecisionMS4 + // PrecisionMS3 indicated that a given dcm time value is only precise to 3 + // millisecond place (1/1000 of a second). + PrecisionMS3 + // PrecisionMS2 indicated that a given dcm time value is only precise to 2 + // millisecond place (1/100 of a second). + PrecisionMS2 + // PrecisionMS1 indicated that a given dcm time value is only precise to 1 + // millisecond place (1/10 of a second). + PrecisionMS1 + // PrecisionSeconds indicated that a given dcm time value is only precise to the + // second. + PrecisionSeconds + // PrecisionMinutes indicated that a given dcm time value is only precise to the + // minute. + PrecisionMinutes + // PrecisionHours indicated that a given dcm time value is only precise to the hour. + PrecisionHours + // PrecisionDay indicated that a given dcm time value is only precise to the day. + PrecisionDay + // PrecisionMonth indicated that a given dcm time value is only precise to the + // month. + PrecisionMonth + // PrecisionYear indicated that a given dcm time value is only precise to the year. + PrecisionYear ) diff --git a/pkg/dcmtime/time.go b/pkg/dcmtime/time.go index 0c050191..23504214 100644 --- a/pkg/dcmtime/time.go +++ b/pkg/dcmtime/time.go @@ -15,6 +15,67 @@ type Time struct { Precision PrecisionLevel } +// GetTime returns the Time field value for the Time. Included to support common +// interfaces with other dcmtime types. +func (tm Time) GetTime() time.Time { + return tm.Time +} + +// GetPrecision returns the Precision field value for the Time. Included to support +// common interfaces with other dcmtime types. +func (tm Time) GetPrecision() PrecisionLevel { + return tm.Precision +} + +// tmPrecisionOmits is the range of precision values not relevant to Time. +var tmPrecisionOmits = precisionRange{ + Min: PrecisionYear, + Max: PrecisionDay, +} + +// HasPrecision returns whether this da value has a precision of AT LEAST 'check'. +// +// Will always Return false for PrecisionYear, PrecisionMonth, and PrecisionDay. +// +// Will return true for PrecisionFull if all possible values are present. +func (tm Time) HasPrecision(check PrecisionLevel) bool { + return hasPrecisionOmits(check, tm.Precision, tmPrecisionOmits) +} + +// Hour returns the underlying Time.Hour(). Since a DICOM TM value must contain an hour, +// 'ok' will always be true. 'ok' is included to form a common interface with Datetime. +func (tm Time) Hour() (hour int, ok bool) { + return tm.Time.Hour(), true +} + +// Minute returns the underlying Time.Minute(), and a boolean indicating whether the +// original DICOM value included minutes. +func (tm Time) Minute() (minute int, ok bool) { + return tm.Time.Minute(), hasPrecision(PrecisionMinutes, tm.Precision) +} + +// Second returns the underlying Time.Second(), and a boolean indicating whether the +// original DICOM value included seconds. +func (tm Time) Second() (second int, ok bool) { + return tm.Time.Second(), hasPrecision(PrecisionSeconds, tm.Precision) +} + +// Nanosecond returns the underlying Time.Nanosecond(), and a boolean indicating whether +// the original DICOM value included any fractal seconds. +func (tm Time) Nanosecond() (second int, ok bool) { + return tm.Time.Nanosecond(), hasPrecision(PrecisionMS1, tm.Precision) +} + +// Combine combines the Time with a Date 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 (tm Time) Combine(da Date, location *time.Location) (Datetime, error) { + return combineDateAndTime(da, tm, location) +} + // DCM converts internal time.Time value to dicom TM string, truncating the output // to the DA value's Precision. // @@ -24,17 +85,17 @@ func (tm Time) DCM() string { builder := strings.Builder{} builder.WriteString(fmt.Sprintf("%02d", tm.Time.Hour())) - if !isIncluded(PrecisionMinutes, tm.Precision) { + if !hasPrecision(PrecisionMinutes, tm.Precision) { return builder.String() } builder.WriteString(fmt.Sprintf("%02d", tm.Time.Minute())) - if !isIncluded(PrecisionSeconds, tm.Precision) { + if !hasPrecision(PrecisionSeconds, tm.Precision) { return builder.String() } builder.WriteString(fmt.Sprintf("%02d", tm.Time.Second())) - if !isIncluded(PrecisionMS1, tm.Precision) { + if !hasPrecision(PrecisionMS1, tm.Precision) { return builder.String() } @@ -49,17 +110,17 @@ func (tm Time) String() string { builder := strings.Builder{} builder.WriteString(fmt.Sprintf("%02d", tm.Time.Hour())) - if !isIncluded(PrecisionMinutes, tm.Precision) { + if !hasPrecision(PrecisionMinutes, tm.Precision) { return builder.String() } builder.WriteString(fmt.Sprintf(":%02d", tm.Time.Minute())) - if !isIncluded(PrecisionSeconds, tm.Precision) { + if !hasPrecision(PrecisionSeconds, tm.Precision) { return builder.String() } builder.WriteString(fmt.Sprintf(":%02d", tm.Time.Second())) - if !isIncluded(PrecisionMS1, tm.Precision) { + if !hasPrecision(PrecisionMS1, tm.Precision) { return builder.String() } diff --git a/pkg/dcmtime/time_test.go b/pkg/dcmtime/time_test.go index 77e621d8..b519dd41 100644 --- a/pkg/dcmtime/time_test.go +++ b/pkg/dcmtime/time_test.go @@ -7,12 +7,34 @@ import ( "time" ) -func TestParseTime(t *testing.T) { +// daPrecisionOmits is the range of precision values not relevant to Date. +var tmPrecisionOmits = precisionRange{ + Min: dcmtime.PrecisionYear, + Max: dcmtime.PrecisionDay, +} + +func TestTime(t *testing.T) { testCases := []struct { - Name string - TMValue string - ExpectedTime time.Time + // Name is the name for the sub-test. + Name string + // TMValue is the raw DICOM TM string we are parsing. + TMValue string + // ExpectedString is the expected value of the String() method. + ExpectedTime time.Time + // ExpectedPrecision is the expected precision value of the parsed value. ExpectedPrecision dcmtime.PrecisionLevel + // ExpectedString is the expected result of the String() value. + ExpectedString string + // HasMinute is whether the parsed value's Minute() method should return ok=true. + HasMinute bool + // HasSecond is whether the parsed value's Second() method should return ok=true. + HasSecond bool + // HasNanosecond is whether the parsed value's HasNanosecond() method should + // return ok=true. + HasNanosecond bool + // HasPrecisionRange is the range of Precision Values we expect the + // HasPrecision() method to return true for. + HasPrecisionRange precisionRange }{ // Full value, leading zeros { @@ -20,6 +42,14 @@ func TestParseTime(t *testing.T) { TMValue: "010203.456789", ExpectedTime: time.Date(1, 1, 1, 1, 2, 3, 456789000, time.UTC), ExpectedPrecision: dcmtime.PrecisionFull, + ExpectedString: "01:02:03.456789", + HasMinute: true, + HasSecond: true, + HasNanosecond: true, + HasPrecisionRange: precisionRange{ + Min: dcmtime.PrecisionHours, + Max: dcmtime.PrecisionFull, + }, }, // Remove one millisecond @@ -28,6 +58,14 @@ func TestParseTime(t *testing.T) { TMValue: "010203.45678", ExpectedTime: time.Date(1, 1, 1, 1, 2, 3, 456780000, time.UTC), ExpectedPrecision: dcmtime.PrecisionMS5, + ExpectedString: "01:02:03.45678", + HasMinute: true, + HasSecond: true, + HasNanosecond: true, + HasPrecisionRange: precisionRange{ + Min: dcmtime.PrecisionHours, + Max: dcmtime.PrecisionMS5, + }, }, // Remove two millisecond @@ -36,6 +74,14 @@ func TestParseTime(t *testing.T) { TMValue: "010203.4567", ExpectedTime: time.Date(1, 1, 1, 1, 2, 3, 456700000, time.UTC), ExpectedPrecision: dcmtime.PrecisionMS4, + ExpectedString: "01:02:03.4567", + HasMinute: true, + HasSecond: true, + HasNanosecond: true, + HasPrecisionRange: precisionRange{ + Min: dcmtime.PrecisionHours, + Max: dcmtime.PrecisionMS4, + }, }, // Remove three millisecond @@ -44,6 +90,14 @@ func TestParseTime(t *testing.T) { TMValue: "010203.456", ExpectedTime: time.Date(1, 1, 1, 1, 2, 3, 456000000, time.UTC), ExpectedPrecision: dcmtime.PrecisionMS3, + ExpectedString: "01:02:03.456", + HasMinute: true, + HasSecond: true, + HasNanosecond: true, + HasPrecisionRange: precisionRange{ + Min: dcmtime.PrecisionHours, + Max: dcmtime.PrecisionMS3, + }, }, // Remove four millisecond @@ -52,6 +106,14 @@ func TestParseTime(t *testing.T) { TMValue: "010203.45", ExpectedTime: time.Date(1, 1, 1, 1, 2, 3, 450000000, time.UTC), ExpectedPrecision: dcmtime.PrecisionMS2, + ExpectedString: "01:02:03.45", + HasMinute: true, + HasSecond: true, + HasNanosecond: true, + HasPrecisionRange: precisionRange{ + Min: dcmtime.PrecisionHours, + Max: dcmtime.PrecisionMS2, + }, }, // Remove five millisecond @@ -60,6 +122,14 @@ func TestParseTime(t *testing.T) { TMValue: "010203.4", ExpectedTime: time.Date(1, 1, 1, 1, 2, 3, 400000000, time.UTC), ExpectedPrecision: dcmtime.PrecisionMS1, + ExpectedString: "01:02:03.4", + HasMinute: true, + HasSecond: true, + HasNanosecond: true, + HasPrecisionRange: precisionRange{ + Min: dcmtime.PrecisionHours, + Max: dcmtime.PrecisionMS1, + }, }, // No milliseconds @@ -68,6 +138,14 @@ func TestParseTime(t *testing.T) { TMValue: "010203", ExpectedTime: time.Date(1, 1, 1, 1, 2, 3, 0, time.UTC), ExpectedPrecision: dcmtime.PrecisionSeconds, + ExpectedString: "01:02:03", + HasMinute: true, + HasSecond: true, + HasNanosecond: false, + HasPrecisionRange: precisionRange{ + Min: dcmtime.PrecisionHours, + Max: dcmtime.PrecisionSeconds, + }, }, // No seconds @@ -76,6 +154,14 @@ func TestParseTime(t *testing.T) { TMValue: "0102", ExpectedTime: time.Date(1, 1, 1, 1, 2, 0, 0, time.UTC), ExpectedPrecision: dcmtime.PrecisionMinutes, + ExpectedString: "01:02", + HasMinute: true, + HasSecond: false, + HasNanosecond: false, + HasPrecisionRange: precisionRange{ + Min: dcmtime.PrecisionHours, + Max: dcmtime.PrecisionMinutes, + }, }, // No minutes @@ -84,6 +170,14 @@ func TestParseTime(t *testing.T) { TMValue: "01", ExpectedTime: time.Date(1, 1, 1, 1, 0, 0, 0, time.UTC), ExpectedPrecision: dcmtime.PrecisionHours, + ExpectedString: "01", + HasMinute: false, + HasSecond: false, + HasNanosecond: false, + HasPrecisionRange: precisionRange{ + Min: dcmtime.PrecisionHours, + Max: dcmtime.PrecisionHours, + }, }, // No leading zeroes @@ -92,30 +186,108 @@ func TestParseTime(t *testing.T) { TMValue: "102030.456789", ExpectedTime: time.Date(1, 1, 1, 10, 20, 30, 456789000, time.UTC), ExpectedPrecision: dcmtime.PrecisionFull, + ExpectedString: "10:20:30.456789", + HasMinute: true, + HasSecond: true, + HasNanosecond: true, + HasPrecisionRange: precisionRange{ + Min: dcmtime.PrecisionHours, + Max: dcmtime.PrecisionFull, + }, }, } for _, tc := range testCases { - t.Run(tc.TMValue, func(t *testing.T) { + t.Run(tc.Name, func(t *testing.T) { - parsed, err := dcmtime.ParseTime(tc.TMValue) - if err != nil { - t.Fatal("parse error:", err) - } + // We'll store the parsed object here for subsequent subtests + var parsed dcmtime.Time - if !tc.ExpectedTime.Equal(parsed.Time) { - t.Errorf( - "parsed Time (%v) != expected (%v)", parsed, tc.ExpectedTime, - ) - } + t.Run("ParseTime()", func(t *testing.T) { + var err error + parsed, err = dcmtime.ParseTime(tc.TMValue) + if err != nil { + t.Fatal("ParseTime() error:", err) + } - if parsed.Precision != tc.ExpectedPrecision { - t.Errorf( - "Time.Precision: expected %v, got %v", - tc.ExpectedPrecision.String(), - parsed.Precision.String(), - ) - } + if !tc.ExpectedTime.Equal(parsed.Time) { + t.Errorf( + "parsed Time (%v) != expected (%v)", parsed, tc.ExpectedTime, + ) + } + + if parsed.Precision != tc.ExpectedPrecision { + t.Errorf( + "Time.Precision: expected %v, got %v", + tc.ExpectedPrecision.String(), + parsed.Precision.String(), + ) + } + }) + + 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("DCM()", func(t *testing.T) { + dcmVal := parsed.DCM() + if dcmVal != tc.TMValue { + t.Errorf( + "DCM(): expected '%v', got '%v'", tc.TMValue, dcmVal, + ) + } + }) + + t.Run("String()", func(t *testing.T) { + strVal := parsed.String() + if strVal != tc.ExpectedString { + t.Errorf( + "String(): expected '%v', got '%v'", + tc.ExpectedString, + strVal, + ) + } + }) + + t.Run("Hour()", func(t *testing.T) { + hour, ok := parsed.Hour() + checkDateHelperOutput(t, "Hour()", parsed.Time.Hour(), hour, true, ok) + }) + + t.Run("Minute()", func(t *testing.T) { + minute, ok := parsed.Minute() + checkDateHelperOutput(t, "Minute()", parsed.Time.Minute(), minute, tc.HasMinute, ok) + }) + + t.Run("Second()", func(t *testing.T) { + minute, ok := parsed.Second() + checkDateHelperOutput(t, "Second()", parsed.Time.Second(), minute, tc.HasSecond, ok) + }) + + t.Run("Nanosecond()", func(t *testing.T) { + nanos, ok := parsed.Nanosecond() + checkDateHelperOutput(t, "Nanosecond()", parsed.Time.Nanosecond(), nanos, tc.HasNanosecond, ok) + }) + + t.Run("HasPrecision()", func(t *testing.T) { + checkHasPrecision(t, parsed, tc.HasPrecisionRange, tmPrecisionOmits) + }) }) } } @@ -201,7 +373,7 @@ func TestParseTimeErr(t *testing.T) { } } -func TestTime_Methods(t *testing.T) { +func TestTime_PrecisionTrimming(t *testing.T) { testCases := []struct { Name string Time time.Time @@ -340,3 +512,17 @@ func TestTime_Methods(t *testing.T) { }) } } + +// TestTime_SaneDefaults tests that instantiating a new Time object with just the Time +// field specified yields a reasonable result. +func TestTime_SaneDefaults(t *testing.T) { + newValue := dcmtime.Time{ + Time: time.Date(1, 1, 1, 12, 7, 56, 123456000, time.FixedZone("", 0)), + } + + dcmVal := newValue.DCM() + expected := "120756.123456" + if dcmVal != expected { + t.Errorf("DCM(): expected '%v', but got '%v'", expected, dcmVal) + } +}