Skip to content

Commit e2b45dc

Browse files
committed
Improves DateTimeRange parsing examples
Refines the DateTimeRange parsing examples in the README to provide clearer and more comprehensive usage scenarios. Adds examples for common date ranges, including last hour, yesterday, last week, and last year. Updates tests to validate new examples and clarify rounding behavior with different bracket combinations. Clarifies the behavior of exclusive upper bounds in date math expressions, especially around zero-width ranges.
1 parent 67d068f commit e2b45dc

File tree

2 files changed

+297
-22
lines changed

2 files changed

+297
-22
lines changed

README.md

Lines changed: 46 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -68,19 +68,25 @@ Supports full [Elasticsearch date math syntax](https://www.elastic.co/guide/en/e
6868

6969
```csharp
7070
// Common date range queries
71-
var thisMonth = DateTimeRange.Parse("[now/M TO now/M]", now); // Start of month through end of month
72-
var lastMonth = DateTimeRange.Parse("[now-1M/M TO now/M}", now); // Start of last month through start of this month
73-
var yearToDate = DateTimeRange.Parse("[now/y TO now]", now); // Start of year through now
74-
var last7Days = DateTimeRange.Parse("[now-7d/d TO now]", now); // Start of 7 days ago through now
75-
var last15Minutes = DateTimeRange.Parse("[now-15m TO now]", now); // 15 minutes ago through now
76-
var last24Hours = DateTimeRange.Parse("[now-24h TO now]", now); // 24 hours ago through now
77-
var tomorrow = DateTimeRange.Parse("[now+1d/d TO now+1d/d]", now); // Start through end of tomorrow
71+
var last15Minutes = DateTimeRange.Parse("[now-15m TO now]", now); // 15 minutes ago through now
72+
var lastHour = DateTimeRange.Parse("[now-1h/h TO now-1h/h]", now); // Start through end of last hour
73+
var last24Hours = DateTimeRange.Parse("[now-24h TO now]", now); // 24 hours ago through now
74+
var today = DateTimeRange.Parse("[now/d TO now/d]", now); // Start of day through end of day
75+
var yesterday = DateTimeRange.Parse("[now-1d/d TO now-1d/d]", now); // Start through end of yesterday
76+
var tomorrow = DateTimeRange.Parse("[now+1d/d TO now+1d/d]", now); // Start through end of tomorrow
77+
var last7Days = DateTimeRange.Parse("[now-7d/d TO now-1d/d]", now); // Last 7 full days (not including today)
78+
var thisWeek = DateTimeRange.Parse("[now/w TO now/w]", now); // Start of week through end of week
79+
var lastWeek = DateTimeRange.Parse("[now-1w/w TO now-1w/w]", now); // Start through end of last week
80+
var thisMonth = DateTimeRange.Parse("[now/M TO now/M]", now); // Start of month through end of month
81+
var lastMonth = DateTimeRange.Parse("[now-1M/M TO now-1M/M]", now); // Start through end of last month
82+
var yearToDate = DateTimeRange.Parse("[now/y TO now]", now); // Start of year through now
83+
var lastYear = DateTimeRange.Parse("[now-1y/y TO now-1y/y]", now); // Start through end of last year
7884
7985
// Short-form comparison operators
80-
var recentItems = DateTimeRange.Parse(">=now-1h", now); // From 1 hour ago to max
81-
var futureOnly = DateTimeRange.Parse(">now", now); // After now to max
82-
var beforeToday = DateTimeRange.Parse("<now/d", now); // Min to start of today
83-
var throughToday = DateTimeRange.Parse("<=now/d", now); // Min to end of today
86+
var recentItems = DateTimeRange.Parse(">=now-1h", now); // From 1 hour ago to max
87+
var futureOnly = DateTimeRange.Parse(">now", now); // After now to max
88+
var beforeToday = DateTimeRange.Parse("<now/d", now); // Min to start of today
89+
var throughToday = DateTimeRange.Parse("<=now/d", now); // Min to end of today
8490
```
8591

8692
##### Rounding Behavior with Boundaries
@@ -128,27 +134,33 @@ All four bracket combinations are supported (including mixed):
128134

129135
| Query | Rounding | Effective |
130136
| ----- | -------- | --------- |
137+
| `[now/h TO now/h]` | min: start of hour, max: end of hour | Entire current hour |
131138
| `[now/d TO now/d]` | min: start, max: end | Entire current day |
132139
| `[now/d TO now/d}` | min: start, max: start | Empty (start = start) |
133140
| `{now/d TO now/d]` | min: end, max: end | Empty (end = end) |
134141
| `[now/M TO now/M]` | min: start of month, max: end of month | Entire current month |
135-
| `[now/h TO now/h]` | min: start of hour, max: end of hour | Entire current hour |
136142

137143
Common date range patterns:
138144

139145
```text
146+
// Last 15 minutes (rolling)
147+
[now-15m TO now]
148+
149+
// Last hour (rounded to hour boundaries)
150+
[now-1h/h TO now-1h/h]
151+
152+
// Last 4 full hours (rounded to hour boundaries)
153+
[now-4h/h TO now/h]
154+
155+
// Last 24 hours (rolling, including partial today)
156+
[now-24h TO now]
157+
140158
// Today (start of day through end of day)
141159
[now/d TO now/d]
142160
143161
// Yesterday
144162
[now-1d/d TO now-1d/d]
145163
146-
// This month
147-
[now/M TO now/M]
148-
149-
// Last month
150-
[now-1M/M TO now-1M/M]
151-
152164
// Last 7 full days (not including today)
153165
[now-7d/d TO now-1d/d]
154166
@@ -158,13 +170,25 @@ Common date range patterns:
158170
// This week
159171
[now/w TO now/w]
160172
161-
// Last hour
162-
[now-1h/h TO now-1h/h]
173+
// Last week
174+
[now-1w/w TO now-1w/w]
163175
164-
// Last 4 full hours (rounded to hour boundaries)
165-
[now-4h/h TO now/h]
176+
// This month
177+
[now/M TO now/M]
178+
179+
// Last month
180+
[now-1M/M TO now-1M/M]
181+
182+
// Year to date (start of year through now)
183+
[now/y TO now]
184+
185+
// Last year
186+
[now-1y/y TO now-1y/y]
166187
```
167188

189+
> **Important**: `[now-1d/d TO now-1d/d}` (same date, exclusive upper) produces a zero-width range because
190+
> both sides round to start-of-day. Use `[now-1d/d TO now-1d/d]` (inclusive both ends) instead.
191+
168192
### DateMath Utility
169193

170194
For applications that need standalone date math parsing without the range functionality, the `DateMath` utility class provides direct access to Elasticsearch date math expression parsing. Check out our [unit tests](https://github.com/exceptionless/Exceptionless.DateTimeExtensions/blob/main/tests/Exceptionless.DateTimeExtensions.Tests/DateMathTests.cs) for more usage samples.

tests/Exceptionless.DateTimeExtensions.Tests/DateTimeRangeTests.cs

Lines changed: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -534,4 +534,255 @@ public void Parse_ComparisonOperatorWithoutExpression_ReturnsEmpty(string input)
534534
var range = DateTimeRange.Parse(input, _now);
535535
Assert.Equal(DateTimeRange.Empty, range);
536536
}
537+
538+
[Fact]
539+
public void Parse_PreviousDay_InclusiveBothEnds_ReturnsFullYesterday()
540+
{
541+
// Arrange
542+
var baseTime = new DateTime(2023, 12, 25, 12, 0, 0);
543+
544+
// Act — [now-1d/d TO now-1d/d] inclusive both: start of yesterday to end of yesterday
545+
var range = DateTimeRange.Parse("[now-1d/d TO now-1d/d]", baseTime);
546+
547+
// Assert
548+
Assert.NotEqual(DateTimeRange.Empty, range);
549+
Assert.Equal(baseTime.AddDays(-1).StartOfDay(), range.Start);
550+
Assert.Equal(baseTime.AddDays(-1).EndOfDay(), range.End);
551+
}
552+
553+
[Fact]
554+
public void Parse_PreviousDay_ExclusiveEnd_CollapseToStartOfYesterday()
555+
{
556+
// Arrange
557+
var baseTime = new DateTime(2023, 12, 25, 12, 0, 0);
558+
559+
// Act — [now-1d/d TO now-1d/d} inclusive min (start of yesterday), exclusive max (start of yesterday)
560+
// Both resolve to start of yesterday → zero-width range, NOT a full day
561+
var range = DateTimeRange.Parse("[now-1d/d TO now-1d/d}", baseTime);
562+
563+
// Assert
564+
Assert.NotEqual(DateTimeRange.Empty, range);
565+
Assert.Equal(baseTime.AddDays(-1).StartOfDay(), range.Start);
566+
Assert.Equal(baseTime.AddDays(-1).StartOfDay(), range.End);
567+
}
568+
569+
[Fact]
570+
public void Parse_PreviousWeek_InclusiveBothEnds_ReturnsFullWeek()
571+
{
572+
// Arrange
573+
var baseTime = new DateTime(2023, 12, 25, 12, 0, 0);
574+
575+
// Act — [now-1w/w TO now-1w/w] full previous week (start to end)
576+
var range = DateTimeRange.Parse("[now-1w/w TO now-1w/w]", baseTime);
577+
578+
// Assert
579+
Assert.NotEqual(DateTimeRange.Empty, range);
580+
Assert.Equal(baseTime.AddDays(-7).StartOfWeek(), range.Start);
581+
Assert.Equal(baseTime.AddDays(-7).EndOfWeek(), range.End);
582+
Assert.True(range.Start < range.End);
583+
}
584+
585+
[Fact]
586+
public void Parse_PreviousWeek_ExclusiveEnd_CollapseToStartOfWeek()
587+
{
588+
// Arrange
589+
var baseTime = new DateTime(2023, 12, 25, 12, 0, 0);
590+
591+
// Act — [now-1w/w TO now-1w/w} inclusive min (start of prev week), exclusive max (start of prev week)
592+
// Both resolve to start of previous week → zero-width range
593+
var range = DateTimeRange.Parse("[now-1w/w TO now-1w/w}", baseTime);
594+
595+
// Assert
596+
Assert.NotEqual(DateTimeRange.Empty, range);
597+
Assert.Equal(baseTime.AddDays(-7).StartOfWeek(), range.Start);
598+
Assert.Equal(baseTime.AddDays(-7).StartOfWeek(), range.End);
599+
}
600+
601+
[Fact]
602+
public void Parse_PreviousMonth_InclusiveBothEnds_ReturnsFullMonth()
603+
{
604+
// Arrange
605+
var baseTime = new DateTime(2023, 12, 25, 12, 0, 0);
606+
607+
// Act — [now-1M/M TO now-1M/M] full previous month (start to end)
608+
var range = DateTimeRange.Parse("[now-1M/M TO now-1M/M]", baseTime);
609+
610+
// Assert
611+
Assert.NotEqual(DateTimeRange.Empty, range);
612+
Assert.Equal(baseTime.AddMonths(-1).StartOfMonth(), range.Start);
613+
Assert.Equal(baseTime.AddMonths(-1).EndOfMonth(), range.End);
614+
Assert.True(range.Start < range.End);
615+
}
616+
617+
[Fact]
618+
public void Parse_PreviousMonth_ExclusiveEnd_CollapseToStartOfMonth()
619+
{
620+
// Arrange
621+
var baseTime = new DateTime(2023, 12, 25, 12, 0, 0);
622+
623+
// Act — [now-1M/M TO now-1M/M} inclusive min (start of prev month), exclusive max (start of prev month)
624+
// Both resolve to start of previous month → zero-width range
625+
var range = DateTimeRange.Parse("[now-1M/M TO now-1M/M}", baseTime);
626+
627+
// Assert
628+
Assert.NotEqual(DateTimeRange.Empty, range);
629+
Assert.Equal(baseTime.AddMonths(-1).StartOfMonth(), range.Start);
630+
Assert.Equal(baseTime.AddMonths(-1).StartOfMonth(), range.End);
631+
}
632+
633+
[Fact]
634+
public void Parse_ThisWeek_InclusiveBothEnds_ReturnsFullWeek()
635+
{
636+
// Arrange
637+
var baseTime = new DateTime(2023, 12, 25, 12, 0, 0);
638+
639+
// Act — [now/w TO now/w] full current week
640+
var range = DateTimeRange.Parse("[now/w TO now/w]", baseTime);
641+
642+
// Assert
643+
Assert.NotEqual(DateTimeRange.Empty, range);
644+
Assert.Equal(baseTime.StartOfWeek(), range.Start);
645+
Assert.Equal(baseTime.EndOfWeek(), range.End);
646+
}
647+
648+
[Fact]
649+
public void Parse_YearToDate_ReturnsStartOfYearToNow()
650+
{
651+
// Arrange
652+
var baseTime = new DateTime(2023, 12, 25, 12, 0, 0);
653+
654+
// Act — [now/y TO now] start of year through now
655+
var range = DateTimeRange.Parse("[now/y TO now]", baseTime);
656+
657+
// Assert
658+
Assert.NotEqual(DateTimeRange.Empty, range);
659+
Assert.Equal(baseTime.StartOfYear(), range.Start);
660+
Assert.Equal(baseTime, range.End);
661+
}
662+
663+
[Fact]
664+
public void Parse_LastYear_InclusiveBothEnds_ReturnsFullYear()
665+
{
666+
// Arrange
667+
var baseTime = new DateTime(2023, 12, 25, 12, 0, 0);
668+
669+
// Act — [now-1y/y TO now-1y/y] full previous year
670+
var range = DateTimeRange.Parse("[now-1y/y TO now-1y/y]", baseTime);
671+
672+
// Assert
673+
Assert.NotEqual(DateTimeRange.Empty, range);
674+
Assert.Equal(baseTime.AddYears(-1).StartOfYear(), range.Start);
675+
Assert.Equal(baseTime.AddYears(-1).EndOfYear(), range.End);
676+
}
677+
678+
[Fact]
679+
public void Parse_Last7FullDays_ReturnsCorrectRange()
680+
{
681+
// Arrange
682+
var baseTime = new DateTime(2023, 12, 25, 12, 0, 0);
683+
684+
// Act — [now-7d/d TO now-1d/d] last 7 full days, not including today
685+
var range = DateTimeRange.Parse("[now-7d/d TO now-1d/d]", baseTime);
686+
687+
// Assert
688+
Assert.NotEqual(DateTimeRange.Empty, range);
689+
Assert.Equal(baseTime.AddDays(-7).StartOfDay(), range.Start);
690+
Assert.Equal(baseTime.AddDays(-1).EndOfDay(), range.End);
691+
}
692+
693+
[Fact]
694+
public void Parse_LastHour_InclusiveBothEnds_ReturnsFullHour()
695+
{
696+
// Arrange
697+
var baseTime = new DateTime(2023, 12, 25, 12, 30, 0);
698+
699+
// Act — [now-1h/h TO now-1h/h] full previous hour
700+
var range = DateTimeRange.Parse("[now-1h/h TO now-1h/h]", baseTime);
701+
702+
// Assert
703+
Assert.NotEqual(DateTimeRange.Empty, range);
704+
Assert.Equal(baseTime.AddHours(-1).StartOfHour(), range.Start);
705+
Assert.Equal(baseTime.AddHours(-1).EndOfHour(), range.End);
706+
}
707+
708+
[Fact]
709+
public void Parse_Last4FullHours_ReturnsCorrectRange()
710+
{
711+
// [now-4h/h TO now/h] — last 4 hours rounded to hour boundaries
712+
var baseTime = new DateTime(2023, 12, 25, 12, 30, 0);
713+
var range = DateTimeRange.Parse("[now-4h/h TO now/h]", baseTime);
714+
715+
Assert.NotEqual(DateTimeRange.Empty, range);
716+
Assert.Equal(baseTime.AddHours(-4).StartOfHour(), range.Start);
717+
Assert.Equal(baseTime.EndOfHour(), range.End);
718+
}
719+
720+
[Fact]
721+
public void Parse_ExplicitDateRange_InclusiveExclusive_RoundsCorrectly()
722+
{
723+
// [2024-01-01||/M TO 2024-03-01||/M} — explicit dates with month rounding
724+
// Min (inclusive): start of January, Max (exclusive): start of March
725+
var baseTime = new DateTimeOffset(2024, 6, 15, 12, 30, 0, TimeSpan.Zero);
726+
var range = DateTimeRange.Parse("[2024-01-01||/M TO 2024-03-01||/M}", baseTime);
727+
728+
Assert.NotEqual(DateTimeRange.Empty, range);
729+
Assert.Equal(new DateTime(2024, 1, 1, 0, 0, 0), range.Start);
730+
Assert.Equal(new DateTime(2024, 3, 1, 0, 0, 0), range.End);
731+
}
732+
733+
[Fact]
734+
public void Parse_ExplicitDateRange_InclusiveBothEnds_RoundsMinDownMaxUp()
735+
{
736+
// [2024-01-01||/M TO 2024-03-01||/M] — inclusive both: start of Jan to end of March
737+
var baseTime = new DateTimeOffset(2024, 6, 15, 12, 30, 0, TimeSpan.Zero);
738+
var range = DateTimeRange.Parse("[2024-01-01||/M TO 2024-03-01||/M]", baseTime);
739+
740+
Assert.NotEqual(DateTimeRange.Empty, range);
741+
Assert.Equal(new DateTime(2024, 1, 1, 0, 0, 0), range.Start);
742+
Assert.Equal(new DateTime(2024, 3, 31, 23, 59, 59, 999), range.End);
743+
}
744+
745+
[Fact]
746+
public void Parse_ExclusiveInclusiveRange_DifferentDates_RoundsCorrectly()
747+
{
748+
// {now-7d/d TO now/d] — exclusive lower (end of 7 days ago), inclusive upper (end of today)
749+
var baseTime = new DateTime(2023, 12, 25, 12, 0, 0);
750+
var range = DateTimeRange.Parse("{now-7d/d TO now/d]", baseTime);
751+
752+
Assert.NotEqual(DateTimeRange.Empty, range);
753+
Assert.Equal(baseTime.AddDays(-7).EndOfDay(), range.Start);
754+
Assert.Equal(baseTime.EndOfDay(), range.End);
755+
}
756+
757+
[Fact]
758+
public void Parse_InclusiveExclusiveRange_HourRounding_BothFloor()
759+
{
760+
// Arrange
761+
var baseTime = new DateTime(2023, 12, 25, 12, 30, 0);
762+
763+
// Act — [now/h TO now/h} inclusive min (start of hour), exclusive max (start of hour)
764+
var range = DateTimeRange.Parse("[now/h TO now/h}", baseTime);
765+
766+
// Assert
767+
Assert.NotEqual(DateTimeRange.Empty, range);
768+
Assert.Equal(baseTime.StartOfHour(), range.Start);
769+
Assert.Equal(baseTime.StartOfHour(), range.End);
770+
}
771+
772+
[Fact]
773+
public void Parse_ExclusiveRange_HourRounding_CeilAndFloor()
774+
{
775+
// Arrange
776+
var baseTime = new DateTime(2023, 12, 25, 12, 30, 0);
777+
778+
// Act — {now/h TO now/h} exclusive min (end of hour), exclusive max (start of hour)
779+
// End > Start → collapsed to start of hour
780+
var range = DateTimeRange.Parse("{now/h TO now/h}", baseTime);
781+
782+
// Assert
783+
Assert.NotEqual(DateTimeRange.Empty, range);
784+
Assert.Equal(baseTime.StartOfHour(), range.Start);
785+
Assert.Equal(baseTime.StartOfHour(), range.End);
786+
}
787+
537788
}

0 commit comments

Comments
 (0)