// Copyright (C) 2025, The Duplicati Team
// https://duplicati.com, hello@duplicati.com
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the "Software"),
// to deal in the Software without restriction, including without limitation
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
// and/or sell copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
using System;
using System.Text.RegularExpressions;
namespace Duplicati.Library.Utility
{
///
/// Utility class to parse relative date/time offset strings
///
public static class Timeparser
{
///
/// Parses a time interval string and returns a TimeSpan object
///
/// The time interval string
/// The calculated TimeSpan
public static TimeSpan ParseTimeSpan(string datestring)
=> ParseTimeSpan(datestring, false);
///
/// Parses a time interval string and returns a TimeSpan object
///
/// The time interval string
/// True if the interval should be negated from the input
/// The calculated TimeSpan
public static TimeSpan ParseTimeSpan(string datestring, bool negate)
{
var multiplier = negate ? -1 : 1;
var offset = TimeSpan.Zero;
if (string.IsNullOrEmpty(datestring))
return offset;
if (long.TryParse(datestring, System.Globalization.NumberStyles.Integer, null, out var l))
return offset.Add(TimeSpan.FromSeconds(l * multiplier));
var separators = new char[] { 's', 'm', 'h', 'D', 'W', 'M', 'Y' };
int index;
var previndex = 0;
while ((index = datestring.IndexOfAny(separators, previndex)) > 0)
{
var partial = datestring.Substring(previndex, index - previndex).Trim();
if (!int.TryParse(partial, System.Globalization.NumberStyles.Integer, null, out var factor))
throw new Exception(Strings.Timeparser.InvalidIntegerError(partial));
factor *= multiplier;
offset += datestring[index] switch
{
's' => TimeSpan.FromSeconds(factor),
'm' => TimeSpan.FromMinutes(factor),
'h' => TimeSpan.FromHours(factor),
'D' => TimeSpan.FromDays(factor),
'W' => TimeSpan.FromDays(factor * 7),
'M' => TimeSpan.FromDays(factor * 30),
'Y' => TimeSpan.FromDays(factor * 365),
_ => throw new Exception(Strings.Timeparser.InvalidSpecifierError(datestring[index])),
};
previndex = index + 1;
}
if (datestring.Substring(previndex).Trim().Length > 0)
throw new Exception(Strings.Timeparser.UnparsedDataFragmentError(datestring.Substring(previndex)));
return offset;
}
///
/// Matches a time interval string
///
private static readonly Regex _timeIntervalRegex = new Regex(@"^(\d+[smhDWMY])+$", RegexOptions.Compiled | RegexOptions.IgnoreCase);
///
/// Parses a time interval string and returns a DateTime object
///
/// The time interval string
/// >The base time to add the interval to
/// The calculated DateTime
/// Note: The offset is assumed to be in local time if unspecified
public static DateTime ParseTimeInterval(string datestring, DateTime offset)
=> ParseTimeInterval(datestring, offset, false);
///
/// Parses a time interval string and returns a DateTime object
///
/// >The time interval string
/// >The base time to add the interval to
/// True if the interval should be subtracted
/// >The calculated DateTime
/// Note: The offset is assumed to be in local time if unspecified
public static DateTime ParseTimeInterval(string datestring, DateTime offset, bool negate)
{
if (string.IsNullOrEmpty(datestring))
return offset;
if (offset.Kind == DateTimeKind.Unspecified)
offset = new DateTime(offset.Ticks, DateTimeKind.Local);
var multiplier = negate ? -1 : 1;
if (_timeIntervalRegex.IsMatch(datestring))
{
var separators = new char[] { 's', 'm', 'h', 'D', 'W', 'M', 'Y' };
int index;
var previndex = 0;
while ((index = datestring.IndexOfAny(separators, previndex)) > 0)
{
var partial = datestring.Substring(previndex, index - previndex).Trim();
if (!int.TryParse(partial, System.Globalization.NumberStyles.Integer, null, out var factor))
throw new Exception(Strings.Timeparser.InvalidIntegerError(partial));
factor *= multiplier;
offset = datestring[index] switch
{
's' => offset.AddSeconds(factor),
'm' => offset.AddMinutes(factor),
'h' => offset.AddHours(factor),
'D' => offset.AddDays(factor),
'W' => offset.AddDays(factor * 7),
'M' => offset.AddMonths(factor),
'Y' => offset.AddYears(factor),
_ => throw new Exception(Strings.Timeparser.InvalidSpecifierError(datestring[index])),
};
previndex = index + 1;
}
if (datestring.Substring(previndex).Trim().Length > 0)
throw new Exception(Strings.Timeparser.UnparsedDataFragmentError(datestring.Substring(previndex)));
return offset;
}
if (string.Equals(datestring.Trim(), "now", StringComparison.OrdinalIgnoreCase))
return DateTime.Now;
if (long.TryParse(datestring, System.Globalization.NumberStyles.Integer, null, out var l))
return offset.AddSeconds(l * multiplier);
if (DateTime.TryParse(datestring, System.Globalization.CultureInfo.CurrentCulture, System.Globalization.DateTimeStyles.AssumeLocal, out var t))
return t;
if (Utility.TryDeserializeDateTime(datestring, out t))
return t;
throw new Exception(Strings.Timeparser.InvalidDateTimeError(datestring));
}
///
/// Parses a time interval string with a timezone offset, retaining the local time
///
/// The repeating interval string
/// The base time to add the interval to
/// The timezone to use for the calculation
/// True if the interval should be subtracted
/// The calculated time
public static DateTime DSTAwareParseTimeInterval(string datestring, DateTime rawoffset, TimeZoneInfo timeZoneInfo, bool negate = false)
{
if (string.IsNullOrEmpty(datestring))
return rawoffset;
var result = new DateTimeOffset(rawoffset.Kind == DateTimeKind.Local ? rawoffset.ToUniversalTime() : rawoffset, TimeSpan.Zero);
int multiplier = negate ? -1 : 1;
var before = result;
if (long.TryParse(datestring, System.Globalization.NumberStyles.Integer, null, out var l))
datestring = $"{l}s";
if (_timeIntervalRegex.IsMatch(datestring))
{
var separators = new char[] { 's', 'm', 'h', 'D', 'W', 'M', 'Y' };
int index;
var previndex = 0;
var keepTimeOfDay = true;
while ((index = datestring.IndexOfAny(separators, previndex)) > 0)
{
var partial = datestring.Substring(previndex, index - previndex).Trim();
if (!int.TryParse(partial, System.Globalization.NumberStyles.Integer, null, out var factor))
throw new Exception(Strings.Timeparser.InvalidIntegerError(partial));
if (datestring[index] == 's' || datestring[index] == 'm' || datestring[index] == 'h')
keepTimeOfDay = false;
factor *= multiplier;
result = datestring[index] switch
{
's' => result.AddSeconds(factor),
'm' => result.AddMinutes(factor),
'h' => result.AddHours(factor),
'D' => result.AddDays(factor),
'W' => result.AddDays(factor * 7),
'M' => result.AddMonths(factor),
'Y' => result.AddYears(factor),
_ => throw new Exception(Strings.Timeparser.InvalidSpecifierError(datestring[index])),
};
previndex = index + 1;
}
if (datestring.Substring(previndex).Trim().Length > 0)
throw new Exception(Strings.Timeparser.UnparsedDataFragmentError(datestring.Substring(previndex)));
var beforeLocal = TimeZoneInfo.ConvertTime(before, timeZoneInfo);
var resultLocal = TimeZoneInfo.ConvertTime(result, timeZoneInfo);
if (keepTimeOfDay && beforeLocal.Offset != resultLocal.Offset)
{
var diff = beforeLocal.Offset - resultLocal.Offset;
result = result.Add(diff);
}
return result.ToUniversalTime().UtcDateTime;
}
if (string.Equals(datestring.Trim(), "now", StringComparison.OrdinalIgnoreCase))
return DateTime.UtcNow;
if (DateTime.TryParse(datestring, System.Globalization.CultureInfo.CurrentCulture, System.Globalization.DateTimeStyles.AssumeLocal, out var t))
return t;
if (Utility.TryDeserializeDateTime(datestring, out t))
return t;
throw new Exception(Strings.Timeparser.InvalidDateTimeError(datestring));
}
}
}