Skip to content

Commit 331d884

Browse files
committed
Handle range inclusivity, escaping and short forms
1 parent dc9b7c0 commit 331d884

8 files changed

Lines changed: 330 additions & 210 deletions

File tree

AGENTS.md

Lines changed: 25 additions & 200 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
1-
# Agent Guidelines for Exceptionless.DateTimeExtensions
1+
# AGENTS.md
22

3-
You are an expert .NET engineer working on Exceptionless.DateTimeExtensions, a focused utility library providing DateTimeRange, Business Day/Week calculations, Elasticsearch-compatible date math parsing, and extension methods for DateTime, DateTimeOffset, and TimeSpan. Your changes must maintain backward compatibility, correctness across edge cases (especially timezone handling), and parsing reliability. Approach each task methodically: research existing patterns, make surgical changes, and validate thoroughly.
3+
This file provides guidance to WARP (warp.dev) when working with code in this repository.
44

5-
**Craftsmanship Mindset**: Every line of code should be intentional, readable, and maintainable. Write code you'd be proud to have reviewed by senior engineers. Prefer simplicity over cleverness. When in doubt, favor explicitness and clarity.
6-
7-
## Repository Overview
5+
## Overview
86

97
Exceptionless.DateTimeExtensions provides date/time utilities for .NET applications:
108

@@ -18,19 +16,24 @@ Exceptionless.DateTimeExtensions provides date/time utilities for .NET applicati
1816

1917
Design principles: **parse flexibility**, **timezone correctness**, **comprehensive edge case handling**, **netstandard2.0 compatibility**.
2018

21-
## Quick Start
19+
## Build Commands
2220

2321
```bash
2422
# Build
2523
dotnet build Exceptionless.DateTimeExtensions.slnx
2624

27-
# Test
25+
# Test all
2826
dotnet test Exceptionless.DateTimeExtensions.slnx
2927

28+
# Test specific file/method
29+
dotnet test --filter "FullyQualifiedName~DateMathTests"
30+
3031
# Format code
3132
dotnet format Exceptionless.DateTimeExtensions.slnx
3233
```
3334

35+
Warnings are treated as errors (`<WarningsAsErrors>true</WarningsAsErrors>`).
36+
3437
## Project Structure
3538

3639
```text
@@ -100,21 +103,11 @@ The library uses a **priority-ordered parser chain** for `DateTimeRange.Parse()`
100103

101104
## Coding Standards
102105

103-
### Style & Formatting
104-
105-
- Follow `.editorconfig` rules (file-scoped namespaces enforced, IDE0005 as error)
106-
- Follow [Microsoft C# conventions](https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/coding-style/coding-conventions)
107-
- Run `dotnet format` to auto-format code
108-
- Match existing file style; minimize diffs
109-
- No code comments unless necessary—code should be self-explanatory
110-
111-
### Code Quality
112-
113-
- Write complete, runnable code—no placeholders, TODOs, or `// existing code...` comments
114-
- Use modern C# features compatible with **netstandard2.0**: be mindful of API availability
115-
- Follow SOLID, DRY principles; remove unused code and parameters
116-
- Clear, descriptive naming; prefer explicit over clever
106+
- File-scoped namespaces enforced (`csharp_style_namespace_declarations = file_scoped:error`)
107+
- Unnecessary usings are errors (`IDE0005`)
108+
- Target **netstandard2.0**: be mindful of API availability
117109
- Use `ConfigureAwait(false)` in library code (not in tests)
110+
- Private/internal fields use `_camelCase` prefix
118111

119112
### Parsing & Date Math Patterns
120113

@@ -128,192 +121,24 @@ The library uses a **priority-ordered parser chain** for `DateTimeRange.Parse()`
128121
### Common Patterns
129122

130123
- **Extension methods**: Group by target type (`DateTimeExtensions.cs`, `DateTimeOffsetExtensions.cs`, `TimeSpanExtensions.cs`)
131-
- **Safe arithmetic**: Use overflow-protected add/subtract methods (`SafeAdd`, `SafeSubtract`) to avoid `ArgumentOutOfRangeException`
132-
- **Start/End helpers**: Provide `StartOf*` and `EndOf*` for all time periods (minute, hour, day, week, month, year)
133-
- **Exceptions**: Use `ArgumentException` for invalid input. Use `FormatException` for parsing failures. Return `false` from `TryParse` methods instead of throwing.
134-
135-
### Single Responsibility
136-
137-
- Each class has one reason to change
138-
- Methods do one thing well; extract when doing multiple things
139-
- Keep files focused: one primary type per file
140-
- Each parser handles one specific format pattern
141-
- If a method needs a comment explaining what it does, it should probably be extracted
142-
143-
### Performance Considerations
144-
145-
- **Avoid allocations in hot paths**: Parsing methods are called frequently; minimize string allocations
124+
- **Safe arithmetic**: Use `SafeAdd`, `SafeSubtract` to avoid `ArgumentOutOfRangeException`
125+
- **Exceptions**: Use `ArgumentException` for invalid input, `FormatException` for parsing failures. Return `false` from `TryParse` methods.
146126
- **Compiled regex**: Use `RegexOptions.Compiled` for frequently-used patterns
147-
- **Span-based parsing**: Where compatible with netstandard2.0, prefer span-based approaches
148-
- **Profile before optimizing**: Don't guess—measure
149-
- **Cache parser instances**: Parsers are discovered once via reflection and reused
150-
151-
## Making Changes
152-
153-
### Before Starting
154-
155-
1. **Gather context**: Read related files, search for similar implementations, understand the full scope
156-
2. **Research patterns**: Find existing usages of the code you're modifying using grep/semantic search
157-
3. **Understand completely**: Know the problem, side effects, and edge cases before coding
158-
4. **Plan the approach**: Choose the simplest solution that satisfies all requirements
159-
5. **Check dependencies**: Verify you understand how changes affect dependent code
160-
161-
### Pre-Implementation Analysis
162-
163-
Before writing any implementation code, think critically:
164-
165-
1. **What could go wrong?** Consider timezone edge cases, overflow/underflow, leap years, DST transitions
166-
2. **What are the parsing edge cases?** Ambiguous input, whitespace, case sensitivity, partial matches
167-
3. **What assumptions am I making?** Validate each assumption against existing tests
168-
4. **Is this the root cause?** Don't fix symptoms—trace to the core problem
169-
5. **Is there existing code that does this?** Search before creating new utilities
170-
171-
### Test-First Development
172-
173-
**Always write or extend tests before implementing changes:**
174-
175-
1. **Find existing tests first**: Search for tests covering the code you're modifying
176-
2. **Extend existing tests**: Add test cases to existing test classes/methods when possible for maintainability
177-
3. **Write failing tests**: Create tests that demonstrate the bug or missing feature
178-
4. **Implement the fix**: Write minimal code to make tests pass
179-
5. **Refactor**: Clean up while keeping tests green
180-
6. **Verify edge cases**: Add tests for boundary conditions, timezone handling, and error paths
181-
182-
**Why extend existing tests?** Consolidates related test logic, reduces duplication, improves discoverability, maintains consistent test patterns.
183-
184-
### While Coding
185-
186-
- **Minimize diffs**: Change only what's necessary, preserve formatting and structure
187-
- **Preserve behavior**: Don't break existing functionality or change semantics unintentionally
188-
- **Build incrementally**: Run `dotnet build` after each logical change to catch errors early
189-
- **Test continuously**: Run `dotnet test` frequently to verify correctness
190-
- **Match style**: Follow the patterns in surrounding code exactly
191-
192-
### Validation
193-
194-
Before marking work complete, verify:
195-
196-
1. **Builds successfully**: `dotnet build Exceptionless.DateTimeExtensions.slnx` exits with code 0
197-
2. **All tests pass**: `dotnet test Exceptionless.DateTimeExtensions.slnx` shows no failures
198-
3. **No new warnings**: Check build output for new compiler warnings (warnings are treated as errors)
199-
4. **API compatibility**: Public API changes are intentional and backward-compatible when possible
200-
5. **Timezone correctness**: Verify explicit timezones are preserved and rounding respects offset
201-
6. **Breaking changes flagged**: Clearly identify any breaking changes for review
202-
203-
### Error Handling
204-
205-
- **Validate inputs**: Check for null, empty strings, invalid ranges at method entry
206-
- **Fail fast**: Throw exceptions immediately for invalid arguments (don't propagate bad data)
207-
- **Meaningful messages**: Include parameter names and expected values in exception messages
208-
- **TryParse pattern**: Always provide a `TryParse` alternative that returns `bool` instead of throwing
209-
- **Use guard clauses**: Early returns for invalid conditions, keep happy path unindented
210127

211128
## Testing
212129

213-
### Philosophy: Battle-Tested Code
214-
215-
Tests are not just validation—they're **executable documentation** and **design tools**. Well-tested code is:
216-
217-
- **Trustworthy**: Confidence to refactor and extend
218-
- **Documented**: Tests show how the API should be used
219-
- **Resilient**: Edge cases are covered before they become production bugs
220-
221-
### Framework
222-
223-
- **xUnit** as the primary testing framework
224-
- **Foundatio.Xunit** provides `TestWithLoggingBase` for test output logging
225-
- Follow [Microsoft unit testing best practices](https://learn.microsoft.com/en-us/dotnet/core/testing/unit-testing-best-practices)
226-
227-
### Test-First Workflow
228-
229-
1. **Search for existing tests**: `dotnet test --filter "FullyQualifiedName~MethodYouAreChanging"`
230-
2. **Extend existing test classes**: Add new `[Fact]` or `[Theory]` cases to existing files
231-
3. **Write the failing test first**: Verify it fails for the right reason
232-
4. **Implement minimal code**: Just enough to pass the test
233-
5. **Add edge case tests**: Null inputs, timezone boundaries, leap years, DST transitions, overflow
234-
6. **Run full test suite**: Ensure no regressions
235-
236-
### Test Principles (FIRST)
237-
238-
- **Fast**: Tests execute quickly
239-
- **Isolated**: No dependencies on external services or execution order
240-
- **Repeatable**: Consistent results every run
241-
- **Self-checking**: Tests validate their own outcomes
242-
- **Timely**: Write tests alongside code
243-
244-
### Naming Convention
245-
246-
Use the pattern: `MethodName_StateUnderTest_ExpectedBehavior`
247-
248-
Examples:
249-
250-
- `Parse_WithNowPlusOneHour_ReturnsOffsetDateTime`
251-
- `TryParse_WithInvalidExpression_ReturnsFalse`
252-
- `StartOfDay_WithDateTimeOffset_PreservesTimezone`
253-
254-
### Test Structure
255-
256-
Follow the AAA (Arrange-Act-Assert) pattern:
257-
258-
```csharp
259-
[Fact]
260-
public void Parse_WithExplicitUtcDate_PreservesTimezone()
261-
{
262-
// Arrange
263-
var expression = "2025-01-01T01:25:35Z||+3d/d";
264-
var baseTime = DateTimeOffset.UtcNow;
265-
266-
// Act
267-
var result = DateMath.Parse(expression, baseTime);
268-
269-
// Assert
270-
Assert.Equal(TimeSpan.Zero, result.Offset);
271-
Assert.Equal(new DateTimeOffset(2025, 1, 4, 0, 0, 0, TimeSpan.Zero), result);
272-
}
273-
```
274-
275-
### Parameterized Tests
276-
277-
Use `[Theory]` with `[InlineData]` for multiple scenarios:
278-
279-
```csharp
280-
[Theory]
281-
[InlineData("1s", 1000)]
282-
[InlineData("1m", 60000)]
283-
[InlineData("1h", 3600000)]
284-
[InlineData("1d", 86400000)]
285-
public void Parse_WithValidTimeUnit_ReturnsExpectedMilliseconds(string input, double expectedMs)
286-
{
287-
var result = TimeUnit.Parse(input);
288-
Assert.Equal(expectedMs, result.TotalMilliseconds);
289-
}
290-
```
291-
292-
### Test Organization
293-
294-
- Mirror the main code structure (e.g., `FormatParsers/` tests for `src/.../FormatParsers/`)
130+
- **xUnit** with **Foundatio.Xunit** (`TestWithLoggingBase` for test output logging)
131+
- Naming: `MethodName_StateUnderTest_ExpectedBehavior`
132+
- Use `[Theory]` with `[InlineData]` for parameterized tests
295133
- Use `FormatParserTestsBase` and `PartParserTestsBase` for parser tests
296-
- Inject `ITestOutputHelper` for test logging via `TestWithLoggingBase`
297-
298-
### Running Tests
299-
300-
```bash
301-
# All tests
302-
dotnet test Exceptionless.DateTimeExtensions.slnx
303-
304-
# Specific test file
305-
dotnet test --filter "FullyQualifiedName~DateMathTests"
306-
307-
# With logging
308-
dotnet test --logger "console;verbosity=detailed"
309-
```
134+
- Test structure mirrors main code (e.g., `tests/.../FormatParsers/` for `src/.../FormatParsers/`)
310135

311-
## Debugging
136+
### Critical Edge Cases
312137

313-
1. **Reproduce** with minimal steps—write a failing test
314-
2. **Understand** the root cause before fixing (especially for timezone and parsing issues)
315-
3. **Test** the fix thoroughly with multiple timezone scenarios
316-
4. **Document** non-obvious fixes in code if needed
138+
- Timezone boundaries (`Z`, `+05:00`, `-08:00`) - explicit timezones must be preserved
139+
- Leap years, DST transitions
140+
- Overflow/underflow in date arithmetic
141+
- Ambiguous parsing input, whitespace, case sensitivity
317142

318143
## Resources
319144

README.md

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,12 +56,52 @@ var wildcardRange = DateTimeRange.Parse("[2023-01-01 TO *]", DateTime.Now); // F
5656

5757
#### Date Math Features
5858

59-
Supports full Elasticsearch date math syntax following [official specifications](https://www.elastic.co/guide/en/elasticsearch/reference/current/common-options.html#date-math):
59+
Supports full [Elasticsearch date math syntax](https://www.elastic.co/guide/en/elasticsearch/reference/current/common-options.html#date-math):
6060

6161
- **Anchors**: `now`, explicit dates with `||` separator
62-
- **Operations**: `+1d` (add), `-1h` (subtract), `/d` (round down)
62+
- **Operations**: `+1d` (add), `-1h` (subtract), `/d` (round)
6363
- **Units**: `y` (years), `M` (months), `w` (weeks), `d` (days), `h`/`H` (hours), `m` (minutes), `s` (seconds)
6464
- **Timezone Support**: Preserves explicit timezones (`Z`, `+05:00`, `-08:00`) or uses system timezone as fallback
65+
- **Escaped slashes**: `\/d` is treated identically to `/d` for contexts where `/` must be escaped
66+
67+
##### Real-World Patterns
68+
69+
```csharp
70+
// 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
78+
79+
// 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
84+
```
85+
86+
##### Rounding Behavior with Boundaries
87+
88+
Rounding direction (`/d`, `/M`, etc.) is controlled by boundary inclusivity, following [Elasticsearch range query rounding rules](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-range-query.html#date-math-rounding):
89+
90+
| Boundary | Bracket | Operator | Rounding direction | `now/d` resolves to |
91+
| ---------- | --------- | ---------- | -------------------- | --------------------- |
92+
| Inclusive lower | `[` | `>=` | Floor (start of period) | `2025-02-16T00:00:00` |
93+
| Exclusive lower | `{` | `>` | Ceiling (end of period) | `2025-02-16T23:59:59.999` |
94+
| Inclusive upper | `]` | `<=` | Ceiling (end of period) | `2025-02-16T23:59:59.999` |
95+
| Exclusive upper | `}` | `<` | Floor (start of period) | `2025-02-16T00:00:00` |
96+
97+
All four bracket combinations are supported: `[]`, `[}`, `{]`, `{}`.
98+
99+
```csharp
100+
// [now/d TO now/d] → start of today through end of today (full day, inclusive both ends)
101+
// [now/d TO now/d} → start of today through start of today (exclusive upper)
102+
// {now/d TO now/d] → end of today through end of today (exclusive lower)
103+
// {now/d TO now/d} → collapsed to start of today (exclusive both ends inverts the range, which is then normalized)
104+
```
65105

66106
Examples:
67107

src/Exceptionless.DateTimeExtensions/DateMath.cs

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,12 @@ public static class DateMath
2929
// https://www.elastic.co/docs/reference/elasticsearch/rest-apis/common-options
3030
internal static readonly Regex Parser = new(
3131
@"\G(?<anchor>now|(?<date>\d{4}-?\d{2}-?\d{2}(?:[T\s](?:\d{1,2}(?::?\d{2}(?::?\d{2})?)?(?:\.\d{1,3})?)?(?:[+-]\d{2}:?\d{2}|Z)?)?)\|\|)" +
32-
@"(?<operations>(?:[+\-/]\d*[yMwdhHms])*)(?=\s|$|[\]\}])",
32+
@"(?<operations>(?:(?:[+\-]|\\?/)\d*[yMwdhHms])*)(?=\s|$|[\]\}])",
3333
RegexOptions.Compiled);
3434

3535
// Pre-compiled regex for operation parsing to avoid repeated compilation
36-
private static readonly Regex _operationRegex = new(@"([+\-/])(\d*)([yMwdhHms])", RegexOptions.Compiled);
36+
// Supports both / and \/ (escaped forward slash) for rounding operations
37+
private static readonly Regex _operationRegex = new(@"([+\-]|\\?/)(\d*)([yMwdhHms])", RegexOptions.Compiled);
3738

3839
// Pre-compiled regex for offset parsing to avoid repeated compilation
3940
private static readonly Regex _offsetRegex = new(@"(Z|[+-]\d{2}:\d{2})$", RegexOptions.Compiled);
@@ -461,12 +462,12 @@ public static DateTimeOffset ApplyOperations(DateTimeOffset baseTime, string ope
461462
throw new ArgumentException("Invalid operations");
462463
}
463464

464-
// Validate that rounding operations (/) are only at the end
465+
// Validate that rounding operations (/ or \/) are only at the end
465466
// According to Elasticsearch spec, rounding must be the final operation
466467
bool foundRounding = false;
467468
for (int i = 0; i < matches.Count; i++)
468469
{
469-
string operation = matches[i].Groups[1].Value;
470+
string operation = matches[i].Groups[1].Value.TrimStart('\\');
470471
if (String.Equals(operation, "/"))
471472
{
472473
if (foundRounding)
@@ -485,7 +486,8 @@ public static DateTimeOffset ApplyOperations(DateTimeOffset baseTime, string ope
485486

486487
foreach (Match opMatch in matches)
487488
{
488-
string operation = opMatch.Groups[1].Value;
489+
// Normalize escaped forward slash (\/) to unescaped (/) for rounding operations
490+
string operation = opMatch.Groups[1].Value.TrimStart('\\');
489491
string amountStr = opMatch.Groups[2].Value;
490492
string unit = opMatch.Groups[3].Value;
491493

0 commit comments

Comments
 (0)