This regular expression parses ISO-8601 duration strings e.g., P1DT2H, into named groups for easy extraction.
(?n)^P(?=.)(((?<CY>\d+)Y)?((?<CM>\d+)M)?((?<CW>\d+)W)?((?<CD>\d+)D)?)?(T(?=.+)(((?<TH>\d+)H)?((?<TM>\d+)M)?((?<TS>\d+)S)?)?)?$
See the PowerShell script below for how this was generated.
PowerShell
This PowerShell script uses regex to parse the ISO8601 duration and then applies the calendar-based time deltas to the specified date.
<#
.SYNOPSIS
Adds an ISO-8601 calendar-based duration to a date.
.PARAM Duration
The duration to add
.PARAM Date
The reference date, defaults to Now.
.EXAMPLES
P1Y: 1 year
P1Y2M3W4D: 1 year, 2months, 3 weeks, 4 days
PT5H6M7S: 5 hours, 6 minutes, 7 seconds
P1Y2M3W4DT5H6M7S: 1 year, 2months, 3 weeks, 4 days, 5 hours, 6 minutes, 7 seconds
#>
function Add-Duration {
param (
[Parameter(Mandatory)]
[string]$Duration,
[DateTime]$Date = (Get-Date),
[Switch]$Past
)
# Compute ISO-8601 duration from the anchor
$ReCalendarParts=("YMWD".ToCharArray() | %{ "((?<C$_>\d+)$_)?" }) -join ""
$ReTimeParts=("HMS".ToCharArray() | %{ "((?<T$_>\d+)$_)?" }) -join ""
$ReISO8601P = "(?n)^P(?=.)($ReCalendarParts)?(T(?=.+)($ReTimeParts)?)?$"
$m = ([Regex]$ReISO8601P).Match($Duration)
if (-Not $m.Success) { throw "Unexpected -Duration value $Duration. Expected P30D or similar ISO-8601 duration" }
foreach ($group in $m.Groups) {
if ($group.Name -ne "0" -and $group.Success) {
$interval = [int]$group.Value
if ($Past) { $interval *= -1 }
$Date = switch -exact ($group.Name) {
CY { $Date.AddYears($interval) }
CM { $Date.AddMonths($interval) }
CW { $Date.AddDays($interval * 7) }
CD { $Date.AddDays($interval) }
TH { $Date.AddHours($interval) }
TM { $Date.AddMinutes($interval) }
TS { $Date.AddSeconds($interval) }
default { throw "Unsupported interval $($Group.Name)" }
}
}
}
$Date
}
C#
This C# code uses regex to parse the ISO8601 duration and then applies the calendar-based time deltas to the specified date.
See the PowerShell example above for how the Regex pattern is constructed.
Console.WriteLine(DateTime.Now.AddDuration("PT1H"));
internal static class DateTimeExtensions
{
const string Iso8601DurationPattern = @"(?n)^P(?=.)(((?<CY>\d+)Y)?((?<CM>\d+)M)?((?<CW>\d+)W)?((?<CD>\d+)D)?)?(T(?=.+)(((?<TH>\d+)H)?((?<TM>\d+)M)?((?<TS>\d+)S)?)?)?$";
private static readonly Regex Iso8601DurationRegex = new Regex(Iso8601DurationPattern, RegexOptions.Compiled);
public static DateTime AddDuration(this DateTime value, string duration, bool past = false)
{
var match = Iso8601DurationRegex.Match(duration);
if (!match.Success)
{
throw new ArgumentOutOfRangeException(nameof(duration));
}
foreach (Group group in match.Groups)
{
if (group.Success && group.Name != "0")
{
var interval = Int32.Parse(group.Value);
if (past) { interval *= -1; }
value = group.Name switch
{
"CY" => value.AddYears(interval),
"CM" => value.AddMonths(interval),
"CW" => value.AddDays(interval * 7),
"CD" => value.AddDays(interval),
"TH" => value.AddHours(interval),
"TM" => value.AddMinutes(interval),
"TS" => value.AddSeconds(interval),
_ => throw new NotSupportedException("Unsupported duration element $($Group.Name)")
};
}
}
return value;
}
}