Skip to content

Commit 518fbc6

Browse files
committed
Refactor date/time arithmetic and add operator support
Simplified `Subtract` methods by delegating to `Add` with negated quantities, reducing duplication. Added `+` and `-` operator overloads for `CqlDate`, `CqlDateTime`, and `CqlTime` to enable intuitive arithmetic operations. Enhanced `CqlQuantity` with negation support via `Negate` method and `-` operator. Improved precision handling in `CqlTime` with explicit handling for `DateTimePrecision.Unknown`. Fixed arithmetic logic for year (`"a"`) and month (`"mo"`) units to account for value signs. Overrode `Equals` and `GetHashCode` for proper equality and hashing in `CqlDate`, `CqlDateTime`, and `CqlTime`. Refactored `CqlTime` constructor for compactness and added `MinValue`/`MaxValue` static properties. Improved boundary calculation methods for readability. Updated test case in `PrimitiveTests.cs` to reflect corrected subtraction logic. Performed general code cleanup to improve readability and maintainability.
1 parent 4254920 commit 518fbc6

File tree

5 files changed

+177
-148
lines changed

5 files changed

+177
-148
lines changed

Cql/CoreTests/PrimitiveTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ public void CqlDate_Subtract_Months_From_Year()
3333
{
3434
Assert.IsTrue(CqlDateTime.TryParse("2014", out var baseDate));
3535
var result = baseDate.Subtract(new CqlQuantity(25m, "month"));
36-
Assert.AreEqual(2016, result.Value.Year);
36+
Assert.AreEqual(2011, result.Value.Year);
3737
Assert.AreEqual(DateTimePrecision.Year, result.Precision);
3838
}
3939

Cql/Cql.Abstractions/Primitives/CqlDate.cs

Lines changed: 28 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,11 @@ namespace Hl7.Cql.Primitives
1717
/// </summary>
1818
/// <see href="https://cql.hl7.org/09-b-cqlreference.html#date"/>
1919
[CqlPrimitiveType(CqlPrimitiveType.Date)]
20-
public class CqlDate : ICqlComparable<CqlDate>, IEquivalentable<CqlDate>
20+
public class CqlDate :
21+
ICqlComparable<CqlDate>,
22+
IEquivalentable<CqlDate>,
23+
IAdditionOperators<CqlDate?, CqlQuantity?, CqlDate?>,
24+
ISubtractionOperators<CqlDate?, CqlQuantity?, CqlDate?>
2125
{
2226
/// <summary>
2327
/// Defines the minimum value for System dates (@0001-01-01).
@@ -98,9 +102,9 @@ public static bool TryParse(string s, out CqlDate? cqlDate)
98102
var dto = Value.DateTimeOffset;
99103
dto = unit switch
100104
{
101-
"a" => dto.AddDays(UCUMUnits.DaysPerYearDouble),
105+
"a" => dto.AddDays(Math.Sign(value) * UCUMUnits.DaysPerYearDouble),
102106
"year" or "years" => dto.AddYears((int)value),
103-
"mo" => dto.AddDays(UCUMUnits.DaysPerMonthDouble),
107+
"mo" => dto.AddDays(Math.Sign(value) * UCUMUnits.DaysPerMonthDouble),
104108
"month" or "months" => dto.AddMonths((int)value),
105109
"wk" or "week" or "weeks" => dto.AddDays((int)(value! * CqlDateTimeMath.DaysPerWeek)),
106110
"d" or "day" or "days" => dto.AddDays((int)value!),
@@ -122,30 +126,7 @@ public static bool TryParse(string s, out CqlDate? cqlDate)
122126
/// <param name="quantity">The quantity to subtract.</param>
123127
/// <returns>A new date with <paramref name="quantity"/> subtracted from it.</returns>
124128
/// <exception cref="ArgumentException">If the quantity is not expressed in supported units, or an overflow occurs.</exception>
125-
public CqlDate? Subtract(CqlQuantity? quantity)
126-
{
127-
if (quantity is not { value: { } value, unit: { } unit })
128-
return null;
129-
130-
var dto = Value.DateTimeOffset;
131-
dto = unit switch
132-
{
133-
"a" => dto.AddDays(-1 * UCUMUnits.DaysPerYearDouble),
134-
"year" or "years" => dto.AddYears((int)value),
135-
"mo" => dto.AddDays(-1 * UCUMUnits.DaysPerMonthDouble),
136-
"month" or "months" => dto.AddMonths((int)value),
137-
"wk" or "week" or "weeks" => dto.AddDays((int)(value! * CqlDateTimeMath.DaysPerWeek)),
138-
"d" or "day" or "days" => dto.AddDays((int)value!),
139-
"hour" or "hours" => dto.AddHours(Math.Truncate((double)value)),
140-
"min" or "minute" or "minutes" => dto.AddMinutes(Math.Truncate((double)value)),
141-
"s" or "second" or "seconds" => dto.AddSeconds(Math.Truncate((double)value)),
142-
"ms" or "millisecond" or "milliseconds" => dto.AddMilliseconds(Math.Truncate((double)value)),
143-
_ => throw new ArgumentException($"Unknown date unit {unit} supplied")
144-
};
145-
var newIsoDate = new DateIso8601(dto, Value.Precision);
146-
var result = new CqlDate(newIsoDate);
147-
return result;
148-
}
129+
public CqlDate? Subtract(CqlQuantity? quantity) => Add(-quantity);
149130

150131
/// <summary>
151132
/// Gets the component of this date.
@@ -299,15 +280,35 @@ public bool EquivalentToValue(CqlDate other, string? precision) =>
299280
/// Returns <see cref="DateIso8601.ToString"/> for <see cref="Value"/>.
300281
/// </summary>
301282
public override string ToString() => Value.ToString();
283+
302284
/// <summary>
303285
/// Compares this object to <paramref name="obj"/> for equality.
304286
/// </summary>
305287
/// <param name="obj">The object to compare against this value.</param>
306288
/// <returns><see langword="true"/> if equal.</returns>
307289
public override bool Equals(object? obj) => Value.Equals((obj as CqlDate)?.Value!);
290+
308291
/// <summary>
309292
/// Gets the value of <see cref="DateIso8601.GetHashCode"/> for <see cref="Value"/>.
310293
/// </summary>
311294
public override int GetHashCode() => Value.GetHashCode();
295+
296+
/// <summary>
297+
/// Adds a specified quantity to a date, returning a new date that is offset by the given quantity.
298+
/// </summary>
299+
/// <param name="left">The date to which the quantity will be added. May be null.</param>
300+
/// <param name="right">The quantity to add to the date. May be null.</param>
301+
/// <returns>A new <see cref="CqlDate"/> representing the result of adding <paramref name="right"/> to <paramref
302+
/// name="left"/>. Returns null if either argument is null.</returns>
303+
public static CqlDate? operator +(CqlDate? left, CqlQuantity? right) => left?.Add(right);
304+
305+
/// <summary>
306+
/// Subtracts the specified quantity from the given date, returning a new date that is offset by the quantity.
307+
/// </summary>
308+
/// <param name="left">The date from which to subtract the quantity. May be null.</param>
309+
/// <param name="right">The quantity to subtract from the date. May be null.</param>
310+
/// <returns>A new <see cref="CqlDate"/> representing the result of subtracting <paramref name="right"/> from <paramref
311+
/// name="left"/>. Returns null if either argument is null.</returns>
312+
public static CqlDate? operator -(CqlDate? left, CqlQuantity? right) => left?.Subtract(right);
312313
}
313314
}

Cql/Cql.Abstractions/Primitives/CqlDateTime.cs

Lines changed: 29 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,11 @@ namespace Hl7.Cql.Primitives
1717
/// </summary>
1818
/// <see href="https://cql.hl7.org/09-b-cqlreference.html#datetime"/>
1919
[CqlPrimitiveType(CqlPrimitiveType.DateTime)]
20-
public class CqlDateTime : ICqlComparable<CqlDateTime>, IEquivalentable<CqlDateTime>
20+
public class CqlDateTime :
21+
ICqlComparable<CqlDateTime>,
22+
IEquivalentable<CqlDateTime>,
23+
IAdditionOperators<CqlDateTime?, CqlQuantity?, CqlDateTime?>,
24+
ISubtractionOperators<CqlDateTime?, CqlQuantity?, CqlDateTime?>
2125
{
2226
/// <summary>
2327
/// Defines the minimum value for System date times (@0001-01-01T00:00:00.000Z).
@@ -151,9 +155,9 @@ public static bool TryParse(string s, out CqlDateTime? cqlDateTime)
151155
var dto = Value.DateTimeOffset;
152156
dto = unit switch
153157
{
154-
"a" => dto.AddDays(UCUMUnits.DaysPerYearDouble),
158+
"a" => dto.AddDays(Math.Sign(value) * UCUMUnits.DaysPerYearDouble),
155159
"year" or "years" => dto.AddYears((int)value),
156-
"mo" => dto.AddDays(UCUMUnits.DaysPerMonthDouble),
160+
"mo" => dto.AddDays(Math.Sign(value) * UCUMUnits.DaysPerMonthDouble),
157161
"month" or "months" => dto.AddMonths((int)value),
158162
"wk" or "week" or "weeks" => dto.AddDays((int)(value! * CqlDateTimeMath.DaysPerWeek)),
159163
"d" or "day" or "days" => dto.AddDays((int)value!),
@@ -175,31 +179,7 @@ public static bool TryParse(string s, out CqlDateTime? cqlDateTime)
175179
/// <param name="quantity">The quantity to subtract.</param>
176180
/// <returns>A new date time with <paramref name="quantity"/> subtracted from it.</returns>
177181
/// <exception cref="ArgumentException">If the quantity is not expressed in supported units, or an overflow occurs.</exception>
178-
public CqlDateTime? Subtract(CqlQuantity quantity)
179-
{
180-
if (quantity is not { value: { } value, unit: { } unit })
181-
return null;
182-
183-
var dto = Value.DateTimeOffset;
184-
dto = unit switch
185-
{
186-
"a" => dto.AddDays(-1 * UCUMUnits.DaysPerYearDouble),
187-
"year" or "years" => dto.AddYears((int)value),
188-
"mo" => dto.AddDays(-1 * UCUMUnits.DaysPerMonthDouble),
189-
"month" or "months" => dto.AddMonths((int)value),
190-
"wk" or "week" or "weeks" => dto.AddDays((int)(value! * CqlDateTimeMath.DaysPerWeek)),
191-
"d" or "day" or "days" => dto.AddDays((int)value!),
192-
"h" or "hour" or "hours" => dto.AddHours(Math.Truncate((double)value)),
193-
"min" or "minute" or "minutes" => dto.AddMinutes(Math.Truncate((double)value)),
194-
"s" or "second" or "seconds" => dto.AddSeconds(Math.Truncate((double)value)),
195-
"ms" or "millisecond" or "milliseconds" => dto.AddMilliseconds(Math.Truncate((double)value)),
196-
_ => throw new ArgumentException($"Unknown date unit {unit} supplied")
197-
};
198-
199-
var newIsoDate = new DateTimeIso8601(dto, Value.Precision);
200-
var result = new CqlDateTime(newIsoDate);
201-
return result;
202-
}
182+
public CqlDateTime? Subtract(CqlQuantity? quantity) => Add(-quantity);
203183

204184
/// <summary>
205185
/// Gets the component of this date time.
@@ -503,15 +483,36 @@ public bool EquivalentToValue(CqlDateTime other, string? precision) =>
503483
/// Returns <see cref="DateTimeIso8601.ToString"/> for <see cref="Value"/>.
504484
/// </summary>
505485
public override string ToString() => Value.ToString();
486+
506487
/// <summary>
507488
/// Compares this object to <paramref name="obj"/> for equality.
508489
/// </summary>
509490
/// <param name="obj">The object to compare against this value.</param>
510491
/// <returns><see langword="true"/> if equal.</returns>
511492
public override bool Equals(object? obj) => Value.Equals((obj as CqlDateTime)?.Value!);
493+
512494
/// <summary>
513495
/// Gets the value of <see cref="DateTimeIso8601.GetHashCode"/> for <see cref="Value"/>.
514496
/// </summary>
515497
public override int GetHashCode() => Value.GetHashCode();
498+
499+
/// <summary>
500+
/// Adds a specified quantity to a CqlDateTime value, returning a new CqlDateTime that represents the result.
501+
/// </summary>
502+
/// <param name="left">The CqlDateTime value to which the quantity will be added. May be null.</param>
503+
/// <param name="right">The CqlQuantity representing the amount to add to the date and time. May be null.</param>
504+
/// <returns>A new CqlDateTime that is the result of adding the specified quantity to the original value, or null if
505+
/// either operand is null.</returns>
506+
public static CqlDateTime? operator +(CqlDateTime? left, CqlQuantity? right) => left?.Add(right);
507+
508+
/// <summary>
509+
/// Subtracts a specified quantity from a CqlDateTime value, returning a new CqlDateTime that represents the
510+
/// result.
511+
/// </summary>
512+
/// <param name="left">The CqlDateTime value from which to subtract. May be null.</param>
513+
/// <param name="right">The CqlQuantity value to subtract from the CqlDateTime. May be null.</param>
514+
/// <returns>A new CqlDateTime representing the result of subtracting the specified quantity from the original date and
515+
/// time, or null if either operand is null.</returns>
516+
public static CqlDateTime? operator -(CqlDateTime? left, CqlQuantity? right) => left?.Subtract(right);
516517
}
517518
}

Cql/Cql.Abstractions/Primitives/CqlQuantity.cs

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,14 @@
66
* available at https://raw.githubusercontent.com/FirelyTeam/firely-cql-sdk/main/LICENSE
77
*/
88

9-
using Hl7.Cql.Abstractions;
10-
119
namespace Hl7.Cql.Primitives
1210
{
1311
/// <summary>
1412
/// Implements the System Quantity type.
1513
/// </summary>
1614
/// <see href="https://cql.hl7.org/09-b-cqlreference.html#quantity"/>
1715
[CqlPrimitiveType(CqlPrimitiveType.Quantity)]
18-
public class CqlQuantity
16+
public class CqlQuantity : IUnaryNegationOperators<CqlQuantity?, CqlQuantity?>
1917
{
2018
/// <summary>
2119
/// Creates an instance.
@@ -98,5 +96,14 @@ public static bool TryParse(string s, out CqlQuantity? q)
9896
else return v;
9997
}
10098

99+
public static CqlQuantity? operator -(CqlQuantity? value) => Negate(value)!;
100+
101+
public static CqlQuantity? Negate(CqlQuantity? cqlQuantity) =>
102+
cqlQuantity switch
103+
{
104+
{ value: { } value, unit: var unit } => new CqlQuantity(-value, unit),
105+
{ value: null, unit: var unit } => new CqlQuantity(null, unit),
106+
null => null,
107+
};
101108
}
102109
}

0 commit comments

Comments
 (0)