TimeSpanParser Class
Extended duration parsing with calendar-based units and human-readable formats
The TimeSpanParser provides extended duration parsing with support for calendar-based units like years (‘Y’) and months (‘M’) that cannot be precisely represented as fixed TimeSpans.
In particular, it enables human-readable duration expressions (e.g., “6M”, “1.5Y”, “2W3D”) while maintaining backward compatibility with standard TimeSpan.Parse formats.
TimeSpanParser allows parsing those expressions with Parse() GetExpressionOccurrence() that can be used to calendar independent intervals (based on standard 365 days year), or calendar-accurate date calculations (based on a reference date).
Table of Contents
📋 Overview
The TimeSpanParser extends standard .NET TimeSpan functionality by supporting calendar-based duration units (years and months) that have variable lengths. It provides a simple, human-readable syntax for expressing durations while offering both approximate TimeSpan conversions and calendar-accurate date arithmetic.
Key Features
- Calendar-Based Units: Support for years (Y) and months (M) in addition to standard TimeSpan units
- Human-Readable Format: Intuitive expression syntax (e.g., “6M”, “1.5Y”, “2W3D”)
- Fractional Values: Decimal support for all time units (e.g., “1.5Y”, “2.5M”)
- Multiple Parsing Modes: Generic (approximate) and CalendarAccurate parsing strategies
- Dual Calculation Modes: Both approximate TimeSpan conversion and calendar-accurate date calculations
- Backward Compatible: Falls back to standard
TimeSpan.Parsefor compatibility - Case Insensitive: Accepts both uppercase and lowercase unit identifiers
- Safe Parsing:
TryParsemethod for error-free parsing scenarios - Predefined Examples: Built-in constants for common duration patterns
Supported Units
The parser supports the following time units in descending order:
| Unit | Identifier | Case Sensitivity | Example | Description |
|---|---|---|---|---|
| Years | Y, y | Insensitive | “1Y”, “1.5Y” | Calendar years (365.25 days average) |
| Months | M | Sensitive (uppercase only) | “6M”, “2.5M” | Calendar months (30.44 days average) |
| Weeks | W, w | Insensitive | “2W”, “1.5W” | Weeks (7 days) |
| Days | D, d | Insensitive | “30D”, “1.5D” | Days (24 hours) |
| Hours | H, h | Insensitive | “12H”, “0.5H” | Hours (60 minutes) |
| Minutes | m | Sensitive (lowercase only) | “30m”, “1.5m” | Minutes (60 seconds) |
| Seconds | S, s | Insensitive | “45S”, “1.5S” | Seconds |
Example expressions: “6M”, “1.5Y”, “2W3D”, “1Y6M2W1D12H30m45S”
Important: The identifiers M (months, uppercase) and m (minutes, lowercase) are case-sensitive to distinguish between these two units.
Parsing Types
The parser supports two parsing strategies via the ParsingType enum:
ParsingType.Generic: Uses approximate conversions (1 year = 365.25 days, 1 month = 30.44 days)ParsingType.CalendarAccurate: Uses calendar-accurate calculations with actual month/year handling relative to a reference date
🔍 Additional Details
Approximate vs Calendar-Accurate Calculations
The TimeSpanParser provides two different calculation approaches depending on your needs:
Approximate TimeSpan Conversion (obtained with Parse(string) or ParsingType.Generic argument): - Converts all units to a single TimeSpan using average values - 1 year = 365.25 days (accounts for leap years) - 1 month = 30.44 days (365.25 / 12) - Best for: Duration measurements, timeouts, intervals, approximate comparisons
Calendar-Accurate Calculation (obtained with ParsingType.CalendarAccurate arguments): - Uses DateTimeOffset.AddYears and AddMonths for precise calendar arithmetic - Properly handles varying month lengths (28-31 days) - Correctly accounts for leap years - Best for: Calculating past dates like retention periods, lookback windows, historical data queries
// Approximate: Always returns same TimeSpan
var duration = TimeSpanParser.Parse("1M"); // ~30.44 days
// Calendar-accurate with ParsingType
var now = DateTimeOffset.UtcNow;
var exactDuration = TimeSpanParser.Parse("1M", ParsingType.CalendarAccurate, now, -1);
// Returns actual TimeSpan between now and 1 month ago
// Calendar-accurate: Respects actual month length
var jan31 = new DateTimeOffset(2024, 1, 31, 0, 0, 0, TimeSpan.Zero);
var result = TimeSpanParser.GetExpressionOccurrence(jan31, "1M", -1);
// Result: December 31, 2023 (1 month back from Jan 31, 2024)
var mar31 = new DateTimeOffset(2024, 3, 31, 0, 0, 0, TimeSpan.Zero);
var result2 = TimeSpanParser.GetExpressionOccurrence(mar31, "1M", -1);
// Result: February 29, 2024 (1 month back from Mar 31, 2024 - handles leap year)Fractional Values
All time units support fractional (decimal) values:
// Fractional years converted to months
var oneAndHalfYears = TimeSpanParser.Parse("1.5Y");
// Equivalent to: 1 year + 6 months
// Fractional months converted to days
var twoAndHalfMonths = TimeSpanParser.Parse("2.5M");
// Equivalent to: 2 months + ~15 days
// Fractional smaller units
var oneAndHalfWeeks = TimeSpanParser.Parse("1.5W");
// Equivalent to: 10.5 daysConversion Rules: - Fractional years → converted to whole months (1.5Y → 1Y + 6M) - Fractional months → converted to days using 30.44 days/month - Fractional weeks, days, hours, minutes, seconds → converted directly
Case Sensitivity
The parser is case-insensitive for most unit identifiers, with two important exceptions:
// Case-insensitive units
TimeSpanParser.Parse("1Y"); // 1 year
TimeSpanParser.Parse("1y"); // 1 year (same)
TimeSpanParser.Parse("2W"); // 2 weeks
TimeSpanParser.Parse("2w"); // 2 weeks (same)
// Case-SENSITIVE units - Must use exact case:
// M (uppercase) = Months ONLY
TimeSpanParser.Parse("6M"); // 6 months ✓
TimeSpanParser.Parse("6m"); // 6 MINUTES (not months!) ✓
// m (lowercase) = Minutes ONLY
TimeSpanParser.Parse("30m"); // 30 minutes ✓
TimeSpanParser.Parse("30M"); // Would fail - M without digits before it is invalidImportant: - M (uppercase) = Months (case-sensitive, uppercase only) - m (lowercase) = Minutes (case-sensitive, lowercase only) - All other units (Y, W, D, H, S) are case-insensitive
Best Practice: For clarity and consistency, use the exact case shown in the documentation: - Years: Y or y - Months: M (uppercase only) - Weeks: W or w - Days: D or d - Hours: H or h - Minutes: m (lowercase only) - Seconds: S or s
Backward Compatibility
The parser maintains full compatibility with standard TimeSpan.Parse:
// Standard TimeSpan formats work
var ts1 = TimeSpanParser.Parse("01:30:00"); // 1 hour 30 minutes
var ts2 = TimeSpanParser.Parse("1.12:30:00"); // 1 day, 12 hours, 30 minutes
var ts3 = TimeSpanParser.Parse("00:00:45"); // 45 seconds
// Extended formats
var ts4 = TimeSpanParser.Parse("6M"); // ~6 months
var ts5 = TimeSpanParser.Parse("1Y6M"); // ~1.5 yearsThe parser attempts standard TimeSpan.Parse first, then falls back to extended format parsing if that fails.
Unit Ordering
Units must appear in descending order (largest to smallest):
// ✓ Correct ordering
TimeSpanParser.Parse("1Y6M2W3D12H30m45S");
// ✗ Incorrect ordering (will fail to parse)
TimeSpanParser.Parse("6M1Y"); // Months before years
TimeSpanParser.Parse("3D2W"); // Days before weeks
TimeSpanParser.Parse("45S30m"); // Seconds before minutes
// ✓ You can skip units
TimeSpanParser.Parse("1Y3D"); // Year and days (skipping months and weeks)
TimeSpanParser.Parse("2W12H"); // Weeks and hours (skipping days)⚙️ Configuration
Default Period
When no expression is provided or the expression is null/whitespace, the parser uses a default period:
You can reference this constant in your code:
Conversion Factors
The parser uses the following conversion factors for approximate calculations:
| Unit | Days Equivalent | Notes |
|---|---|---|
| Year | 365.25 | Accounts for leap years (Julian year) |
| Month | 30.44 | Average month length (365.25 / 12) |
| Week | 7 | Fixed |
| Day | 1 | Base unit |
These factors are used internally by the Parse method. The GetExpressionOccurrence method uses calendar-accurate operations instead.
💡 Usage Examples
Basic Parsing
using Diginsight.Components;
public class RetentionPolicyService
{
public void ConfigureRetention(string retentionExpression)
{
// Parse various duration formats using approximate conversions
var sixMonths = TimeSpanParser.Parse("6M");
var oneYear = TimeSpanParser.Parse("1Y");
var twoWeeks = TimeSpanParser.Parse("2W");
var thirtyDays = TimeSpanParser.Parse("30D");
Console.WriteLine($"6M = {sixMonths.TotalDays:F1} days"); // ~182.6 days
Console.WriteLine($"1Y = {oneYear.TotalDays:F1} days"); // ~365.25 days
Console.WriteLine($"2W = {twoWeeks.TotalDays:F1} days"); // 14.0 days
Console.WriteLine($"30D = {thirtyDays.TotalDays:F1} days"); // 30.0 days
}
public bool IsExpired(DateTime createdDate, string retentionPeriod)
{
var retention = TimeSpanParser.Parse(retentionPeriod);
return DateTime.UtcNow - createdDate > retention;
}
}Calendar-Accurate Parsing with ParsingType
public class CalendarAccurateParsingService
{
public void DemonstrateParsingModes()
{
var now = DateTimeOffset.UtcNow;
// Generic (approximate) parsing
var approxDuration = TimeSpanParser.Parse("6M", ParsingType.Generic, now, -1);
Console.WriteLine($"Approximate 6M: {approxDuration.TotalDays:F1} days");
// Always ~182.6 days regardless of reference date
// Calendar-accurate parsing going backward in time
var exactDurationBack = TimeSpanParser.Parse("6M", ParsingType.CalendarAccurate, now, -1);
Console.WriteLine($"Exact 6M backward: {exactDurationBack.TotalDays:F1} days");
// Actual days between now and 6 months ago
// Calendar-accurate parsing going forward in time
var exactDurationForward = TimeSpanParser.Parse("6M", ParsingType.CalendarAccurate, now, 1);
Console.WriteLine($"Exact 6M forward: {exactDurationForward.TotalDays:F1} days");
// Actual days between now and 6 months ahead
}
public TimeSpan GetRetentionDuration(DateTimeOffset referenceDate, string retentionPeriod)
{
// Get calendar-accurate duration relative to reference date
return TimeSpanParser.Parse(
retentionPeriod,
ParsingType.CalendarAccurate,
referenceDate,
-1 // -1 = backward in time, 1 = forward in time
);
}
}Calendar-Accurate Date Calculations
public class SubscriptionService
{
public DateTimeOffset CalculateExpirationDate(
DateTimeOffset startDate,
string retentionPeriod)
{
// Calculate when data should be deleted (going back in time from now)
// This properly handles month-end dates and leap years
var expirationDate = TimeSpanParser.GetExpressionOccurrence(
DateTimeOffset.UtcNow,
retentionPeriod,
-1 // Negative occurrence = go backward in time
);
return expirationDate;
}
public List<DateTimeOffset> GetRetentionCheckpoints(
DateTimeOffset currentDate,
string checkpointInterval,
int numberOfCheckpoints)
{
var dates = new List<DateTimeOffset>();
var checkpointDate = currentDate;
for (int i = 0; i < numberOfCheckpoints; i++)
{
checkpointDate = TimeSpanParser.GetExpressionOccurrence(
checkpointDate,
checkpointInterval,
-1 // Negative occurrence = go backward in time
);
dates.Add(checkpointDate);
}
return dates;
}
public void DemonstrateDateAccuracy()
{
var today = new DateTimeOffset(2024, 3, 31, 0, 0, 0, TimeSpan.Zero);
// Calculate 1 month ago from March 31
var oneMonthAgo = TimeSpanParser.GetExpressionOccurrence(today, "1M", -1);
// Result: February 29, 2024 (handles leap year correctly)
var jan31 = new DateTimeOffset(2024, 1, 31, 0, 0, 0, TimeSpan.Zero);
var oneMonthBeforeJan = TimeSpanParser.GetExpressionOccurrence(jan31, "1M", -1);
// Result: December 31, 2023 (goes back 1 month accurately)
}
}Safe Parsing with TryParse
public class ConfigurationService
{
private readonly ILogger<ConfigurationService> _logger;
public TimeSpan GetRetentionPeriod(string userInput)
{
// Simple TryParse (Generic mode)
if (TimeSpanParser.TryParse(userInput, out var duration))
{
_logger.LogInformation(
"Parsed retention period: {Expression} = {Days} days",
userInput,
duration.TotalDays
);
return duration;
}
_logger.LogWarning(
"Invalid retention expression: {Expression}, using default",
userInput
);
return TimeSpanParser.Parse(TimeSpanParser.DefaultPeriod);
}
public TimeSpan GetCalendarAccurateDuration(
string userInput,
DateTimeOffset referenceDate,
int sign)
{
// TryParse with ParsingType.CalendarAccurate
if (TimeSpanParser.TryParse(
userInput,
ParsingType.CalendarAccurate,
referenceDate,
sign,
out var duration))
{
_logger.LogInformation(
"Parsed calendar-accurate duration: {Expression} = {Days} days",
userInput,
duration.TotalDays
);
return duration;
}
_logger.LogWarning(
"Invalid duration expression: {Expression}, using default",
userInput
);
return TimeSpanParser.Parse(
TimeSpanParser.DefaultPeriod,
ParsingType.CalendarAccurate,
referenceDate,
sign
);
}
public void ValidateUserInput(string input)
{
if (!TimeSpanParser.TryParse(input, out var result))
{
throw new ArgumentException(
$"Invalid duration format: '{input}'. " +
$"Use format like '6M', '1Y', or '2W3D'."
);
}
// Further validation
if (result.TotalDays < 1)
{
throw new ArgumentException("Duration must be at least 1 day");
}
if (result.TotalDays > 3650) // ~10 years
{
throw new ArgumentException("Duration cannot exceed 10 years");
}
}
}Complex Duration Expressions
public class DurationExamples
{
public void ComplexExpressions()
{
// Combine multiple units
var complex1 = TimeSpanParser.Parse("1Y6M2W3D");
// 1 year + 6 months + 2 weeks + 3 days
var complex2 = TimeSpanParser.Parse("1Y6M2W1D12H30m45S");
// All units combined
var complex3 = TimeSpanParser.Parse("2W3D12H");
// Skip some units (no years/months)
// Fractional values
var fractional1 = TimeSpanParser.Parse("1.5Y"); // 1.5 years
var fractional2 = TimeSpanParser.Parse("2.5M"); // 2.5 months
var fractional3 = TimeSpanParser.Parse("1.5W3D"); // 1.5 weeks + 3 days
// Comparison: approximate vs actual days
var sixMonthsApprox = TimeSpanParser.Parse("6M");
Console.WriteLine($"6M ≈ {sixMonthsApprox.TotalDays:F1} days");
// For actual calculations going backward in time, use GetExpressionOccurrence
var today = DateTimeOffset.UtcNow;
var sixMonthsAgo = TimeSpanParser.GetExpressionOccurrence(today, "6M", -1);
var actualDays = (today - sixMonthsAgo).TotalDays;
Console.WriteLine($"6M actual = {actualDays:F1} days (calendar-accurate, going backward)");
}
}Precision Considerations
Be aware of the precision requirements of your application when using TimeSpanParser:
- For approximate duration measurements,
Parse(string)orParsingType.Genericprovides sufficient accuracy and simplicity. - When exact date calculations are needed (e.g., billing cycles, contract durations), use
ParsingType.CalendarAccurateto ensure correct month and year handling. - For critical calculations involving legal or financial implications, prefer the clarity and precision of calendar-accurate operations.
public class PrecisionExamples
{
public void ComparePrecision()
{
var referenceDate = new DateTimeOffset(2024, 1, 15, 0, 0, 0, TimeSpan.Zero);
// Approximate calculation
var approxDuration = TimeSpanParser.Parse("3M");
var approxDate = referenceDate - approxDuration;
Console.WriteLine($"Approximate: {approxDate:yyyy-MM-dd}");
// Uses 30.44 * 3 = ~91.3 days
// Calendar-accurate calculation
var exactDate = TimeSpanParser.GetExpressionOccurrence(referenceDate, "3M", -1);
Console.WriteLine($"Exact: {exactDate:yyyy-MM-dd}");
// Uses actual calendar months
// Difference can be significant
var difference = approxDate - exactDate;
Console.WriteLine($"Difference: {difference.TotalDays:F1} days");
}
public void LeapYearConsiderations()
{
var leapYearDate = new DateTimeOffset(2024, 2, 29, 0, 0, 0, TimeSpan.Zero);
// Subtracting 1 year from Feb 29, 2024 (leap year)
var oneYearBefore = TimeSpanParser.GetExpressionOccurrence(leapYearDate, "1Y", -1);
// Result: Feb 28, 2023 (2023 is not a leap year, so Feb 29 doesn't exist)
Console.WriteLine($"One year before {leapYearDate:yyyy-MM-dd} is {oneYearBefore:yyyy-MM-dd}");
}
public void EdgeCases()
{
// Month-end boundary handling
var jan31 = new DateTimeOffset(2024, 1, 31, 0, 0, 0, TimeSpan.Zero);
var oneMonthBack = TimeSpanParser.GetExpressionOccurrence(jan31, "1M", -1);
// Result: Dec 31, 2023 (both months have 31 days)
var mar31 = new DateTimeOffset(2024, 3, 31, 0, 0, 0, TimeSpan.Zero);
var oneMonthBack2 = TimeSpanParser.GetExpressionOccurrence(mar31, "1M", -1);
// Result: Feb 29, 2024 (Feb has only 29 days in 2024, so Mar 31 -> Feb 29)
// Fractional month precision
var fractionalApprox = TimeSpanParser.Parse("2.5M");
// 2.5 months ≈ 76.1 days (2 * 30.44 + 0.5 * 30.44)
var fractionalExact = TimeSpanParser.Parse("2.5M", ParsingType.CalendarAccurate, jan31, -1);
// Actual duration from Jan 31 going back 2 whole months + 15 days (0.5 * 30.44)
}
}📚 Reference
Enums
ParsingType
Defines the parsing strategy for duration expressions.
Classes
TimeSpanParser: Static class providing duration parsing functionalityTimeSpanParser.Examples: Static class containing predefined example constants
Methods
Parse (Simple)
Parses an extended TimeSpan expression into a TimeSpan using approximate conversions.
Parameters: - expression (string): Duration expression (e.g., “6M”, “1.5Y”, “2W3D”) or standard TimeSpan format. If null/whitespace, returns DefaultPeriod.
Returns: TimeSpan representing the approximate duration.
Exceptions: - ArgumentException: Invalid expression format.
Example:
Parse (With ParsingType)
Parses an extended TimeSpan expression into a TimeSpan using the specified parsing strategy.
Parameters: - expression (string): Duration expression to parse. If null/whitespace, returns DefaultPeriod. - parsingType (ParsingType): The parsing strategy (Generic or CalendarAccurate). - referenceDateTimeOffset (DateTimeOffset): Reference date for CalendarAccurate evaluation (ignored for Generic). - sign (int): Direction for CalendarAccurate evaluation: -1 for backward in time, 1 for forward in time (ignored for Generic).
Returns: - For ParsingType.Generic: Approximate duration using average values. - For ParsingType.CalendarAccurate: Actual duration between the reference date and the date after applying the expression in the specified direction.
Exceptions: - ArgumentException: Invalid expression format.
Example:
var now = DateTimeOffset.Now;
// Generic parsing (approximate)
var approxSixMonths = TimeSpanParser.Parse("6M", ParsingType.Generic, now, -1);
// Calendar-accurate parsing
var exactSixMonthsBack = TimeSpanParser.Parse("6M", ParsingType.CalendarAccurate, now, -1);
var exactOneYearForward = TimeSpanParser.Parse("1Y", ParsingType.CalendarAccurate, now, 1);TryParse (Simple)
Attempts to parse an extended TimeSpan expression without throwing exceptions.
Parameters: - expression (string): Duration expression to parse. - result (out TimeSpan): Parsed TimeSpan if successful, otherwise TimeSpan.Zero.
Returns: true if parsing succeeded, otherwise false.
Example:
TryParse (With ParsingType)
Attempts to parse an extended TimeSpan expression without throwing exceptions, using the specified parsing strategy.
Parameters: - expression (string): Duration expression to parse. - parsingType (ParsingType): The parsing strategy (Generic or CalendarAccurate). - referenceDateTimeOffset (DateTimeOffset): Reference date for CalendarAccurate evaluation (ignored for Generic). - sign (int): Direction for CalendarAccurate evaluation: -1 for backward, 1 for forward (ignored for Generic). - result (out TimeSpan): Parsed TimeSpan if successful, otherwise TimeSpan.Zero.
Returns: true if parsing succeeded, otherwise false.
Example:
GetExpressionOccurrence
Calculates a date/time by applying an extended duration expression to a reference date using calendar-accurate operations.
Parameters: - referenceDate (DateTimeOffset): Starting date/time. - expression (string): Duration expression. If null/whitespace, uses DefaultPeriod. - occurrence (int): Number of times to apply the duration. Negative values move backward in time, positive values move forward.
Returns: DateTimeOffset representing the calculated date.
Exceptions: - ArgumentException: Invalid expression format.
Example:
var today = DateTimeOffset.Now;
var sixMonthsAgo = TimeSpanParser.GetExpressionOccurrence(today, "6M", -1);
var oneYearAgo = TimeSpanParser.GetExpressionOccurrence(today, "1Y", -1);
var twoYearsAgo = TimeSpanParser.GetExpressionOccurrence(today, "1Y", -2);
var sixMonthsAhead = TimeSpanParser.GetExpressionOccurrence(today, "6M", 1);
var oneYearAhead = TimeSpanParser.GetExpressionOccurrence(today, "1Y", 1);Constants
DefaultPeriod
The default period used when parsing expressions. If the expression is null or whitespace, this period is used.
Example:
Expression Format
Extended duration expressions are specified using a combination of the following units:
- Y or y: Years (e.g., “1Y”, “2.5Y”) - case-insensitive
- M: Months (e.g., “6M”, “1.5M”) - uppercase only
- W or w: Weeks (e.g., “2W”, “1.5W”) - case-insensitive
- D or d: Days (e.g., “30D”, “1.5D”) - case-insensitive
- H or h: Hours (e.g., “12H”, “0.5H”) - case-insensitive
- m: Minutes (e.g., “30m”, “1.5m”) - lowercase only
- S or s: Seconds (e.g., “45S”, “1.5S”) - case-insensitive
Format Rules: - Units must appear in the order shown above (descending: Years → Months → Weeks → Days → Hours → Minutes → Seconds) - Any unit can be omitted - Fractional values are supported for all units (e.g., “1.5Y”, “2.5M”) - No spaces allowed between components - M (uppercase) and m (lowercase) are case-sensitive to distinguish months from minutes
Regular Expression Pattern:
Valid Examples: - "6M" - 6 months - "1.5Y" - 1.5 years - "2W3D" - 2 weeks and 3 days - "1Y6M2W1D12H30m45S" - All units combined - "30m" - 30 minutes - "2W3d" - 2 weeks and 3 days (mixed case for case-insensitive units)
Invalid Examples: - "6m" without preceding digits would be interpreted as 6 minutes, not 6 months - "30M" - Cannot be interpreted as 30 minutes (M is months, not minutes) - "3D2W" - Wrong order (weeks must come before days) - "+1Y" or "1Y1M1D" (ambiguous with plus sign or missing unit)