├── .nuget ├── NuGet.exe ├── NuGet.Config └── NuGet.targets ├── Extensions.DotLiquid ├── packages.config ├── Extensions.DotLiquid.nuspec ├── NaturalDateFilter.cs ├── Properties │ └── AssemblyInfo.cs └── Extensions.DotLiquid.csproj ├── Extensions.SmartFormatting ├── packages.config ├── SmartFormatterExtensions.cs ├── Extensions.SmartFormatting.nuspec ├── NaturalDateSource.cs ├── Properties │ └── AssemblyInfo.cs ├── NaturalDateFormatter.cs └── Extensions.SmartFormatting.csproj ├── .gitignore ├── packages └── repositories.config ├── Tests ├── packages.config ├── TestHelpers.cs ├── Properties │ └── AssemblyInfo.cs ├── Extensions │ ├── DotLiquidTests.cs │ └── SmartFormattingTests.cs ├── TimeParserTests.cs ├── Tests.csproj └── Plugins │ └── ArithmeticTimePluginTests.cs ├── Parser ├── DateTimeExtensions.cs ├── Tokenization │ ├── IApplyTimeTokens.cs │ ├── IParseTimeStrings.cs │ └── TimeToken.cs ├── Parser.nuspec ├── Plugins │ ├── RelativeTimeUnit.cs │ └── ArithmeticTimePlugin.cs ├── TimeParseFormatException.cs ├── Properties │ └── AssemblyInfo.cs ├── Parser.csproj └── TimeParser.cs ├── Pathoschild.NaturalTimeParser.sln └── README.md /.nuget/NuGet.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pathoschild/NaturalTimeParser/HEAD/.nuget/NuGet.exe -------------------------------------------------------------------------------- /Extensions.DotLiquid/packages.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /Extensions.SmartFormatting/packages.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.nuget/NuGet.Config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.nupkg 2 | *.sln.docstates 3 | *.suo 4 | *.user 5 | *.vsp 6 | Thumbs.db 7 | [Bb]in 8 | [Dd]ebug*/ 9 | obj/ 10 | packages/* 11 | !packages/repositories.config 12 | [Rr]elease*/ 13 | _ReSharper*/ 14 | *.orig -------------------------------------------------------------------------------- /packages/repositories.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /Tests/packages.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /Parser/DateTimeExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Pathoschild.NaturalTimeParser.Parser 4 | { 5 | /// Extends to support natural time offsets. 6 | public static class DateTimeExtensions 7 | { 8 | /// Apply a natural time offset to the date. 9 | /// The initial date to offset. 10 | /// A natural time offset (like "+5 months 2 days"). 11 | public static DateTime Offset(this DateTime date, string offset) 12 | { 13 | return TimeParser.Default.Parse(offset, date); 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Parser/Tokenization/IApplyTimeTokens.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Pathoschild.NaturalTimeParser.Parser.Tokenization 4 | { 5 | /// Applies time tokens to values. 6 | public interface IApplyTimeTokens 7 | { 8 | /// Apply a natural time token to a date value. 9 | /// The natural time token to apply. 10 | /// The date value to apply the token to. 11 | /// Returns the modified date, or null if the token is not supported. 12 | DateTime? TryApply(TimeToken token, DateTime date); 13 | } 14 | } -------------------------------------------------------------------------------- /Parser/Parser.nuspec: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Pathoschild.NaturalTimeParser 5 | 0.2.0-alpha 6 | Jesse Plamondon-Willard 7 | A partial C# implementation of natural time formats like "last month +3 days" which can be used in date arithmetic. 8 | https://creativecommons.org/licenses/by/3.0/ 9 | https://github.com/Pathoschild/NaturalTimeParser#readme 10 | https://upload.wikimedia.org/wikipedia/commons/thumb/d/d0/Noun_project_3067.svg/128px-Noun_project_3067.svg.png 11 | false 12 | datetime relative time 13 | 14 | -------------------------------------------------------------------------------- /Extensions.SmartFormatting/SmartFormatterExtensions.cs: -------------------------------------------------------------------------------- 1 | using SmartFormat; 2 | 3 | namespace Pathoschild.NaturalTimeParser.Extensions.SmartFormatting 4 | { 5 | /// Extends with plugin registration. 6 | public static class SmartFormatterExtensions 7 | { 8 | /// Register the and plugins for natural time parsing. 9 | /// The formatter to extend. 10 | public static SmartFormatter AddExtensionsForNaturalTime(this SmartFormatter formatter) 11 | { 12 | formatter.SourceExtensions.Add(new NaturalDateSource()); 13 | formatter.FormatterExtensions.Insert(0, new NaturalDateFormatter()); 14 | return formatter; 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /Parser/Tokenization/IParseTimeStrings.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Pathoschild.NaturalTimeParser.Parser.Tokenization 4 | { 5 | /// Parses a natural time format string into a set of tokens. 6 | /// This plugin is called to tokenize the input string. It should scan the front of the string for recognized tokens, and stop at the first unrecognized value. 7 | public interface IParseTimeStrings 8 | { 9 | /// Scan the front of an input string to read a set of matching tokens. 10 | /// The natural time format string. 11 | /// Returns a set of matching tokens, or an empty collection if no supported token was found. 12 | IEnumerable Tokenize(string input); 13 | } 14 | } -------------------------------------------------------------------------------- /Parser/Plugins/RelativeTimeUnit.cs: -------------------------------------------------------------------------------- 1 | namespace Pathoschild.NaturalTimeParser.Parser.Plugins 2 | { 3 | /// A supported relative time unit. 4 | public enum RelativeTimeUnit 5 | { 6 | /// A unit of one second. 7 | Seconds, 8 | 9 | /// A unit of one minute. 10 | Minutes, 11 | 12 | /// A unit of one hour. 13 | Hours, 14 | 15 | /// A unit of one day. 16 | Days, 17 | 18 | /// A unit of seven days. 19 | Weeks, 20 | 21 | /// A unit of fourteen days. 22 | Fortnights, 23 | 24 | /// A unit of one month. 25 | Months, 26 | 27 | /// A unit of one year. 28 | Years, 29 | 30 | /// An unknown unit of time. 31 | Unknown 32 | }; 33 | } -------------------------------------------------------------------------------- /Extensions.DotLiquid/Extensions.DotLiquid.nuspec: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Pathoschild.NaturalTimeParser.DotLiquid 5 | 0.2.0-alpha 6 | Jesse Plamondon-Willard 7 | Provides a DotLiquid filter which enables date arithmetic like {{ blog.date | date_add:'3 days' }} and tokens like {{ 'today' | date:'yyyy-mm-dd' }}. See the project page for usage. 8 | https://creativecommons.org/licenses/by/3.0/ 9 | https://github.com/Pathoschild/NaturalTimeParser#readme 10 | https://upload.wikimedia.org/wikipedia/commons/thumb/d/d0/Noun_project_3067.svg/128px-Noun_project_3067.svg.png 11 | false 12 | datetime string.format 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /Extensions.SmartFormatting/Extensions.SmartFormatting.nuspec: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Pathoschild.NaturalTimeParser.SmartFormat 5 | 0.2.0-alpha 6 | Jesse Plamondon-Willard 7 | Provides a SmartFormat plugin which adds relative time tokens like '{Today}' and arithmetic like "last month +3 days", which enables string format strings like {Today:yyyy-MM-dd|-3 days}. See the project page for usage. 8 | https://creativecommons.org/licenses/by/3.0/ 9 | https://github.com/Pathoschild/NaturalTimeParser#readme 10 | https://upload.wikimedia.org/wikipedia/commons/thumb/d/d0/Noun_project_3067.svg/128px-Noun_project_3067.svg.png 11 | false 12 | datetime string.format 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /Tests/TestHelpers.cs: -------------------------------------------------------------------------------- 1 | 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Globalization; 5 | using Pathoschild.NaturalTimeParser.Parser.Plugins; 6 | using Pathoschild.NaturalTimeParser.Parser.Tokenization; 7 | 8 | namespace Pathoschild.NaturalTimeParser.Tests 9 | { 10 | /// Provide convenience methods for testing time formats. 11 | public class TestHelpers 12 | { 13 | /// Get a string representation of a sequence of tokens for assertions. 14 | /// The tokens to represent. 15 | public static string GetRepresentation(IEnumerable tokens) 16 | { 17 | string result = ""; 18 | foreach (TimeToken token in tokens) 19 | result += String.Format("[{0}:{1}]", token.Context, token.Value); 20 | return result; 21 | } 22 | 23 | /// Get a string representation of a date for assertions. 24 | /// The date to represent. 25 | public static string GetRepresentation(DateTime? date) 26 | { 27 | return date.HasValue 28 | ? date.Value.ToString("s") 29 | : ""; 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Extensions.SmartFormatting/NaturalDateSource.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Pathoschild.NaturalTimeParser.Parser; 3 | using SmartFormat.Core.Extensions; 4 | using SmartFormat.Core.Parsing; 5 | 6 | namespace Pathoschild.NaturalTimeParser.Extensions.SmartFormatting 7 | { 8 | /// Enables basic date tokens like {Today} (current date without time) and {Now} (current date and time). 9 | public class NaturalDateSource : ISource 10 | { 11 | /// Process a selector token and get the represented value if it can be handled. 12 | /// The current token value. 13 | /// The selector token to apply. 14 | /// Whether this selector plugin can handle the format token. 15 | /// The selected value. 16 | /// The format metadata. 17 | public void EvaluateSelector(object current, Selector selector, ref bool handled, ref object result, FormatDetails formatDetails) 18 | { 19 | // parse date 20 | DateTime? parsed = new TimeParser().ParseName(selector.Text); 21 | if (!parsed.HasValue) 22 | return; 23 | 24 | // apply token 25 | result = parsed; 26 | handled = true; 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /Extensions.DotLiquid/NaturalDateFilter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using DotLiquid; 3 | using Pathoschild.NaturalTimeParser.Parser; 4 | 5 | namespace Pathoschild.NaturalTimeParser.Extensions.DotLiquid 6 | { 7 | /// Extends the DotLiquid to support natural time offsets on values. This class should be registered with . 8 | /// 9 | /// This filter can be applied to date tokens. For example: 10 | /// {{ blog.date | date_offset:'30 days' | date:'yyyy-MM-dd' }} 11 | /// 12 | public static class NaturalDateFilter 13 | { 14 | /// Apply a natural time offset to the date. 15 | /// The initial date to offset. 16 | /// A natural time offset (like "+5 months 2 days"). 17 | public static DateTime DateOffset(DateTime date, string offset) 18 | { 19 | return date.Offset(offset); 20 | } 21 | 22 | /// Convert a date token like 'today' to a date. 23 | /// The token value. Supported values are today/todayutc (date) and now/nowutc (datetime). 24 | public static DateTime AsDate(string token) 25 | { 26 | DateTime? result = new TimeParser().ParseName(token); 27 | return result ?? DateTime.MinValue; 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /Parser/TimeParseFormatException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Pathoschild.NaturalTimeParser.Parser 4 | { 5 | /// The exception that is thrown when the format of a string cannot be understood as part of a natural time format. The input format and the invalid token can be accessed through the and properties. 6 | public class TimeParseFormatException : FormatException 7 | { 8 | /********* 9 | ** Accessors 10 | *********/ 11 | /// The string that could not be parsed. 12 | public string Input { get; set; } 13 | 14 | /// The portion of the string that could not be understood as a natural time token. 15 | public string InvalidToken { get; set; } 16 | 17 | 18 | /********* 19 | ** Public methods 20 | *********/ 21 | /// Construct an instance. 22 | /// The string that could not be parsed. 23 | /// The portion of the string that could not be understood as a natural time token. 24 | public TimeParseFormatException(string input, string invalidToken) 25 | : base(String.Format("Could not parse natural time format '{0}'. The following portion could not be understood: '{1}'.", input, invalidToken)) 26 | { 27 | this.Input = input; 28 | this.InvalidToken = invalidToken; 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /Tests/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("Tests")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("Tests")] 13 | [assembly: AssemblyCopyright("Copyright © 2013")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("37b9f5ce-b1c1-4d90-a816-954bfb130877")] 24 | 25 | // Version information for an assembly consists of the following four values: 26 | // 27 | // Major Version 28 | // Minor Version 29 | // Build Number 30 | // Revision 31 | // 32 | // You can specify all the values or you can default the Build and Revision Numbers 33 | // by using the '*' as shown below: 34 | // [assembly: AssemblyVersion("1.0.*")] 35 | [assembly: AssemblyVersion("0.2.0.0")] 36 | -------------------------------------------------------------------------------- /Extensions.SmartFormatting/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.InteropServices; 3 | 4 | // General Information about an assembly is controlled through the following 5 | // set of attributes. Change these attribute values to modify the information 6 | // associated with an assembly. 7 | [assembly: AssemblyTitle("Extensions.SmartFormat")] 8 | [assembly: AssemblyDescription("Provides a SmartFormat plugin for using natural dates in string formats.")] 9 | [assembly: AssemblyConfiguration("")] 10 | [assembly: AssemblyCompany("")] 11 | [assembly: AssemblyProduct("Pathoschild.NaturalTimeParser")] 12 | [assembly: AssemblyCopyright("Copyright © Jesse Plamondon-Willard 2013")] 13 | [assembly: AssemblyTrademark("")] 14 | [assembly: AssemblyCulture("")] 15 | 16 | // Setting ComVisible to false makes the types in this assembly not visible 17 | // to COM components. If you need to access a type in this assembly from 18 | // COM, set the ComVisible attribute to true on that type. 19 | [assembly: ComVisible(false)] 20 | 21 | // The following GUID is for the ID of the typelib if this project is exposed to COM 22 | [assembly: Guid("239fb525-182f-4b09-8fb1-6c89b2ecad00")] 23 | 24 | // Version information for an assembly consists of the following four values: 25 | // 26 | // Major Version 27 | // Minor Version 28 | // Build Number 29 | // Revision 30 | // 31 | // You can specify all the values or you can default the Build and Revision Numbers 32 | // by using the '*' as shown below: 33 | // [assembly: AssemblyVersion("1.0.*")] 34 | [assembly: AssemblyVersion("0.2.0.0")] -------------------------------------------------------------------------------- /Parser/Tokenization/TimeToken.cs: -------------------------------------------------------------------------------- 1 | namespace Pathoschild.NaturalTimeParser.Parser.Tokenization 2 | { 3 | /// Represents a natural time token (such as "today" or "-2 days"). 4 | public struct TimeToken 5 | { 6 | /********* 7 | ** Accessors 8 | *********/ 9 | /// The arbitrary parser plugin key, intended to help match parsed values. 10 | public string Parser { get; set; } 11 | 12 | /// The substring that was matched. This value will be stripped from the input. 13 | public string Match { get; set; } 14 | 15 | /// The value associated with the token. 16 | public string Value { get; set; } 17 | 18 | /// The arbitrary context data associated with the token. This is used by the plugin. 19 | public object Context { get; set; } 20 | 21 | 22 | /********* 23 | ** Public methods 24 | *********/ 25 | /// Construct an instance. 26 | /// The arbitrary parser plugin key, intended to help match parsed values. 27 | /// The substring that was matched. This value will be stripped from the input. 28 | /// The value associated with the token. 29 | /// The arbitrary context data associated with the token. This is used by the plugin. 30 | public TimeToken(string type, string match, string value, object context = null) 31 | : this() 32 | { 33 | this.Parser = type; 34 | this.Match = match; 35 | this.Value = value; 36 | this.Context = context; 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /Extensions.DotLiquid/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("Extensions.DotLiquid")] 9 | [assembly: AssemblyDescription("Provides a DotLiquid filter for using natural dates in templates.")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("Pathoschild.NaturalTimeParser")] 13 | [assembly: AssemblyCopyright("Copyright © Jesse Plamondon-Willard 2013")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("f6c3381e-4114-4668-8643-10909116f13e")] 24 | 25 | // Version information for an assembly consists of the following four values: 26 | // 27 | // Major Version 28 | // Minor Version 29 | // Build Number 30 | // Revision 31 | // 32 | // You can specify all the values or you can default the Build and Revision Numbers 33 | // by using the '*' as shown below: 34 | // [assembly: AssemblyVersion("1.0.*")] 35 | [assembly: AssemblyVersion("0.2.0.0")] 36 | -------------------------------------------------------------------------------- /Parser/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("Parser")] 9 | [assembly: AssemblyDescription("A partial implementation of natural time formats like 'last month +3 days' which can be used in date arithmetic.")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("Pathoschild.NaturalTimeParser")] 13 | [assembly: AssemblyCopyright("Copyright © Jesse Plamondon-Willard 2013")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("1483fad9-d196-46a7-9f66-738ccd7359a7")] 24 | 25 | // Version information for an assembly consists of the following four values: 26 | // 27 | // Major Version 28 | // Minor Version 29 | // Build Number 30 | // Revision 31 | // 32 | // You can specify all the values or you can default the Build and Revision Numbers 33 | // by using the '*' as shown below: 34 | // [assembly: AssemblyVersion("1.0.*")] 35 | [assembly: AssemblyVersion("0.2.0.0")] 36 | -------------------------------------------------------------------------------- /Extensions.SmartFormatting/NaturalDateFormatter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Pathoschild.NaturalTimeParser.Parser; 3 | using SmartFormat.Core.Extensions; 4 | using SmartFormat.Core.Output; 5 | using SmartFormat.Core.Parsing; 6 | 7 | namespace Pathoschild.NaturalTimeParser.Extensions.SmartFormatting 8 | { 9 | /// Extends to support natural time offsets on values. 10 | public class NaturalDateFormatter : IFormatter 11 | { 12 | /// Process a format token and write the resulting output if it can be handled. 13 | /// The current token value. 14 | /// The format token to apply. 15 | /// Whether this formatter plugin can handle the format token. 16 | /// The result output to which to write the formatted value. 17 | /// The format metadata. 18 | public void EvaluateFormat(object current, Format format, ref bool handled, IOutput output, FormatDetails formatDetails) 19 | { 20 | // validate 21 | if (format == null || !(current is DateTime)) 22 | return; 23 | 24 | // parse token 25 | FormatItem item = format.Items[0]; 26 | string[] formatSpec = item.Text.Split('|'); 27 | if (formatSpec.Length < 2) 28 | return; 29 | string dateFormat = formatSpec[0]; 30 | string offset = formatSpec[1]; 31 | 32 | // write offset date 33 | DateTime date = (DateTime)current; 34 | try 35 | { 36 | date = date.Offset(offset); 37 | } 38 | catch (Exception ex) 39 | { 40 | throw new SmartFormat.Core.FormatException(format, ex, item.endIndex); 41 | } 42 | output.Write(date.ToString(dateFormat), formatDetails); 43 | handled = true; 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /Tests/Extensions/DotLiquidTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Globalization; 3 | using System.Linq; 4 | using System.Threading; 5 | using DotLiquid; 6 | using NUnit.Framework; 7 | using Pathoschild.NaturalTimeParser.Extensions.DotLiquid; 8 | 9 | namespace Pathoschild.NaturalTimeParser.Tests.Extensions 10 | { 11 | /// Asserts that the DotLiquid plugin support all valid scenarios. 12 | [TestFixture] 13 | public class DotLiquidTests 14 | { 15 | /********* 16 | ** Unit tests 17 | *********/ 18 | [Test(Description = "Assert that the datasource plugin correctly provides the {Today} token.")] 19 | public void Source_Today_TokenReplacedWithExpectedValue() 20 | { 21 | string withSource = this.GetTemplate("{{ 'today' | as_date | date_offset:'1 month ago' | date:'yyyy-MM-dd' }}").Render(); 22 | string withoutSource = this.GetTemplate("{{ date | date_offset:'1 month ago' | date:'yyyy-MM-dd' }}").Render(Hash.FromAnonymousObject(new { Date = DateTime.UtcNow.Date })); 23 | Assert.AreEqual(withoutSource, withSource); 24 | } 25 | 26 | [Test(Description = "Assert that the datasource plugin correctly provides the {Now} token.")] 27 | public void Source_Now_TokenReplacedWithExpectedValue() 28 | { 29 | string withSource = this.GetTemplate("{{ 'now' | as_date | date_offset:'1 hour ago' | date:'yyyy-MM-dd' }}").Render(); 30 | string withoutSource = this.GetTemplate("{{ date | date_offset:'1 hour ago' | date:'yyyy-MM-dd' }}").Render(Hash.FromAnonymousObject(new { Date = DateTime.Now })); 31 | 32 | Assert.AreEqual(withoutSource, withSource); 33 | } 34 | 35 | [Test(Description = "Assert that the formatter plugin returns the correct output for an offset date token.")] 36 | [TestCase("{{ date | date:'yyyy-MM-dd' }}", Result = "2000-01-01")] 37 | [TestCase("{{ date | date_offset:'10 years 2 months 3 days' |date:'yyyy-MM-dd' }}", Result = "2010-03-04")] 38 | [TestCase("{{ date | date_offset:'-1 year' | date:'yyyy' }}", Result = "1999")] 39 | [TestCase("{{ date | date_offset:'1 day ago' | date:'yyyy-MM-dd' }}", Result = "1999-12-31")] 40 | public string Formatter_BuildsExpectedOutput(string format) 41 | { 42 | return this.GetTemplate(format).Render(Hash.FromAnonymousObject(new { Date = new DateTime(2000, 1, 1) })); 43 | } 44 | 45 | 46 | /********* 47 | ** Protected methods 48 | *********/ 49 | /// Construct a formatter instance with the natural time filter registered. 50 | /// The message to format. 51 | public Template GetTemplate(string message) 52 | { 53 | Template.RegisterFilter(typeof(NaturalDateFilter)); 54 | return Template.Parse(message); 55 | } 56 | } 57 | } -------------------------------------------------------------------------------- /Parser/Parser.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {5F4A5633-AA16-4944-B344-3E6EC55EB9E4} 8 | Library 9 | Properties 10 | Pathoschild.NaturalTimeParser.Parser 11 | Pathoschild.NaturalTimeParser.Parser 12 | v4.5 13 | 512 14 | 15 | 16 | true 17 | full 18 | false 19 | bin\Debug\ 20 | DEBUG;TRACE 21 | prompt 22 | 4 23 | 24 | 25 | pdbonly 26 | true 27 | bin\Release\ 28 | TRACE 29 | prompt 30 | 4 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 57 | -------------------------------------------------------------------------------- /Pathoschild.NaturalTimeParser.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 2012 4 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Parser", "Parser\Parser.csproj", "{5F4A5633-AA16-4944-B344-3E6EC55EB9E4}" 5 | EndProject 6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{9A576D59-6A19-40B8-9903-6E50B6D753DB}" 7 | ProjectSection(SolutionItems) = preProject 8 | .gitignore = .gitignore 9 | README.md = README.md 10 | EndProjectSection 11 | EndProject 12 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tests", "Tests\Tests.csproj", "{EEB818B6-3542-41F8-AFCA-180A3DBA8CC5}" 13 | EndProject 14 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".nuget", ".nuget", "{EF0CF10C-4266-496C-8141-811B636C3842}" 15 | ProjectSection(SolutionItems) = preProject 16 | .nuget\NuGet.Config = .nuget\NuGet.Config 17 | .nuget\NuGet.exe = .nuget\NuGet.exe 18 | .nuget\NuGet.targets = .nuget\NuGet.targets 19 | EndProjectSection 20 | EndProject 21 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Extensions.SmartFormatting", "Extensions.SmartFormatting\Extensions.SmartFormatting.csproj", "{8F057237-1541-4743-A6F4-925AAC2E01E3}" 22 | EndProject 23 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Extensions.DotLiquid", "Extensions.DotLiquid\Extensions.DotLiquid.csproj", "{64DEDE41-461D-4FBE-9851-C71A77A2CD78}" 24 | EndProject 25 | Global 26 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 27 | Debug|Any CPU = Debug|Any CPU 28 | Release|Any CPU = Release|Any CPU 29 | EndGlobalSection 30 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 31 | {5F4A5633-AA16-4944-B344-3E6EC55EB9E4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 32 | {5F4A5633-AA16-4944-B344-3E6EC55EB9E4}.Debug|Any CPU.Build.0 = Debug|Any CPU 33 | {5F4A5633-AA16-4944-B344-3E6EC55EB9E4}.Release|Any CPU.ActiveCfg = Release|Any CPU 34 | {5F4A5633-AA16-4944-B344-3E6EC55EB9E4}.Release|Any CPU.Build.0 = Release|Any CPU 35 | {EEB818B6-3542-41F8-AFCA-180A3DBA8CC5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 36 | {EEB818B6-3542-41F8-AFCA-180A3DBA8CC5}.Debug|Any CPU.Build.0 = Debug|Any CPU 37 | {EEB818B6-3542-41F8-AFCA-180A3DBA8CC5}.Release|Any CPU.ActiveCfg = Release|Any CPU 38 | {EEB818B6-3542-41F8-AFCA-180A3DBA8CC5}.Release|Any CPU.Build.0 = Release|Any CPU 39 | {8F057237-1541-4743-A6F4-925AAC2E01E3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 40 | {8F057237-1541-4743-A6F4-925AAC2E01E3}.Debug|Any CPU.Build.0 = Debug|Any CPU 41 | {8F057237-1541-4743-A6F4-925AAC2E01E3}.Release|Any CPU.ActiveCfg = Release|Any CPU 42 | {8F057237-1541-4743-A6F4-925AAC2E01E3}.Release|Any CPU.Build.0 = Release|Any CPU 43 | {64DEDE41-461D-4FBE-9851-C71A77A2CD78}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 44 | {64DEDE41-461D-4FBE-9851-C71A77A2CD78}.Debug|Any CPU.Build.0 = Debug|Any CPU 45 | {64DEDE41-461D-4FBE-9851-C71A77A2CD78}.Release|Any CPU.ActiveCfg = Release|Any CPU 46 | {64DEDE41-461D-4FBE-9851-C71A77A2CD78}.Release|Any CPU.Build.0 = Release|Any CPU 47 | EndGlobalSection 48 | GlobalSection(SolutionProperties) = preSolution 49 | HideSolutionNode = FALSE 50 | EndGlobalSection 51 | EndGlobal 52 | -------------------------------------------------------------------------------- /Extensions.DotLiquid/Extensions.DotLiquid.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {64DEDE41-461D-4FBE-9851-C71A77A2CD78} 8 | Library 9 | Properties 10 | Pathoschild.NaturalTimeParser.Extensions.DotLiquid 11 | Pathoschild.NaturalTimeParser.Extensions.DotLiquid 12 | v4.5 13 | 512 14 | ..\ 15 | true 16 | 17 | 18 | true 19 | full 20 | false 21 | bin\Debug\ 22 | DEBUG;TRACE 23 | prompt 24 | 4 25 | 26 | 27 | pdbonly 28 | true 29 | bin\Release\ 30 | TRACE 31 | prompt 32 | 4 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | ..\packages\DotLiquid.1.7.0\lib\NET40\DotLiquid.dll 45 | 46 | 47 | 48 | 49 | 50 | {5f4a5633-aa16-4944-b344-3e6ec55eb9e4} 51 | Parser 52 | 53 | 54 | 55 | 56 | 63 | -------------------------------------------------------------------------------- /Extensions.SmartFormatting/Extensions.SmartFormatting.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {8F057237-1541-4743-A6F4-925AAC2E01E3} 8 | Library 9 | Properties 10 | Pathoschild.NaturalTimeParser.Extensions.SmartFormatting 11 | Pathoschild.NaturalTimeParser.Extensions.SmartFormatting 12 | v4.5 13 | 512 14 | ..\ 15 | true 16 | 17 | 18 | true 19 | full 20 | false 21 | bin\Debug\ 22 | DEBUG;TRACE 23 | prompt 24 | 4 25 | 26 | 27 | pdbonly 28 | true 29 | bin\Release\ 30 | TRACE 31 | prompt 32 | 4 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | {5f4a5633-aa16-4944-b344-3e6ec55eb9e4} 43 | Parser 44 | 45 | 46 | 47 | 48 | ..\packages\SmartFormat.NET.1.0.0.0\lib\net35-Client\SmartFormat.dll 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 65 | -------------------------------------------------------------------------------- /Tests/Extensions/SmartFormattingTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Globalization; 3 | using NUnit.Framework; 4 | using Pathoschild.NaturalTimeParser.Extensions.SmartFormatting; 5 | using SmartFormat; 6 | using SmartFormat.Core; 7 | 8 | namespace Pathoschild.NaturalTimeParser.Tests.Extensions 9 | { 10 | /// Asserts that the SmartFormat and plugins support all valid scenarios. 11 | [TestFixture] 12 | public class SmartFormattingTests 13 | { 14 | /********* 15 | ** Unit tests 16 | *********/ 17 | [Test(Description = "Assert that the datasource plugin correctly provides the {Today} token.")] 18 | public void Source_Today_TokenReplacedWithExpectedValue() 19 | { 20 | string withSource = this.GetFormatter().Format("{Today:yyyy-MM-dd|1 month ago}"); 21 | string withoutSource = this.GetFormatter().Format("{Date:yyyy-MM-dd|1 month ago}", new { Date = DateTime.UtcNow.Date }); 22 | 23 | Assert.AreEqual(withoutSource, withSource); 24 | } 25 | 26 | [Test(Description = "Assert that the datasource plugin correctly provides the {Now} token.")] 27 | public void Source_Now_TokenReplacedWithExpectedValue() 28 | { 29 | string withSource = this.GetFormatter().Format("{Now:yyyy-MM-dd HH:mm|1 hour ago}"); 30 | string withoutSource = this.GetFormatter().Format("{Date:yyyy-MM-dd HH:mm|1 hour ago}", new { Date = DateTime.Now }); 31 | 32 | Assert.AreEqual(withoutSource, withSource); 33 | } 34 | 35 | [Test(Description = "Assert that the formatter plugin returns the correct output for an offset date token.")] 36 | [TestCase("{Date}", Result = "01/01/2000 00:00:00")] 37 | [TestCase("{Date:yyyy-MM-dd}", Result = "2000-01-01")] 38 | [TestCase("{Date:|10 years 2 months 3 days}", Result = "2010-03-04 00:00:00")] 39 | [TestCase("{Date:yyyy|-1 year}", Result = "1999")] 40 | [TestCase("{Date:yyyy-MM-dd|1 day ago}", Result = "1999-12-31")] 41 | [TestCase("{Date:yyyy-MM-dd|1 day ago|extra|values|ignored}", Result = "1999-12-31")] 42 | public string Formatter_BuildsExpectedOutput(string format) 43 | { 44 | return this.GetFormatter().Format(CultureInfo.InvariantCulture, format, new { Date = new DateTime(2000, 1, 1) }); 45 | } 46 | 47 | [Test(Description = "Assert that the formatter plugin correctly handles errors and respects the configured error action.")] 48 | [TestCase("{Date:yyyy-MM-dd|invalid}", ErrorAction.Ignore, Result = "")] 49 | [TestCase("{Date:yyyy-MM-dd|invalid}", ErrorAction.MaintainTokens, Result = "{Date:yyyy-MM-dd|invalid}")] 50 | [TestCase("{Date:yyyy-MM-dd|invalid}", ErrorAction.ThrowError, ExpectedException = typeof(SmartFormat.Core.FormatException))] 51 | public string Formatter_RespectsErrorAction(string format, ErrorAction errorAction) 52 | { 53 | return this.GetFormatter(errorAction).Format(CultureInfo.InvariantCulture, format, new { Date = new DateTime(2000, 1, 1) }); 54 | } 55 | 56 | 57 | /********* 58 | ** Protected methods 59 | *********/ 60 | /// Construct a formatter instance with the natural time plugins registered. 61 | /// How to handle format errors. 62 | public SmartFormatter GetFormatter(ErrorAction? errorAction = null) 63 | { 64 | SmartFormatter formatter = Smart.CreateDefaultSmartFormat().AddExtensionsForNaturalTime(); 65 | if (errorAction.HasValue) 66 | formatter.ErrorAction = errorAction.Value; 67 | return formatter; 68 | } 69 | } 70 | } -------------------------------------------------------------------------------- /Tests/TimeParserTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using NUnit.Framework; 5 | using Pathoschild.NaturalTimeParser.Parser; 6 | using Pathoschild.NaturalTimeParser.Parser.Plugins; 7 | using Pathoschild.NaturalTimeParser.Parser.Tokenization; 8 | using Pathoschild.NaturalTimeParser.Tests.Plugins; 9 | 10 | namespace Pathoschild.NaturalTimeParser.Tests 11 | { 12 | /// Asserts that the default supports all valid scenarios. 13 | /// This tests the parser framework. More detailed input format assertions should be added for the plugins that implement them (e.g., ). 14 | [TestFixture] 15 | public class TimeParserTests 16 | { 17 | /********* 18 | ** Unit tests 19 | *********/ 20 | [Test(Description = "Assert that the parser has the default parser and applicator plugins registered.")] 21 | public void HasDefaultPlugins() 22 | { 23 | TimeParser parser = new TimeParser(); 24 | Assert.That(parser.Parsers.OfType().Any(), "The arithmetic time plugin isn't registered as a parser."); 25 | Assert.That(parser.Applicators.OfType().Any(), "The arithmetic time plugin isn't registered as an applicator."); 26 | } 27 | 28 | [Test(Description = "Assert that the parser can tokenize various representative input formats. More detailed format tokens are in the plugin tests.")] 29 | [TestCase("3 days ago", Result = "[Days:-3]")] 30 | [TestCase("14 months -3 days 2 hours", Result = "[Months:14][Days:-3][Hours:2]")] 31 | public string CanTokenize(string format) 32 | { 33 | IEnumerable tokens = new TimeParser().Tokenize(format); 34 | return TestHelpers.GetRepresentation(tokens); 35 | } 36 | 37 | [Test(Description = "Assert that the parser can apply tokens in various formats to a date.")] 38 | [TestCase(ArithmeticTimePlugin.Key, "42", RelativeTimeUnit.Days, Result = "2000-02-12T00:00:00")] 39 | [TestCase(ArithmeticTimePlugin.Key, "42", RelativeTimeUnit.Fortnights, Result ="2001-08-11T00:00:00")] 40 | public string CanApply(string type, string value, object context) 41 | { 42 | TimeToken token = new TimeToken(type, null, value, context); 43 | DateTime? date = new TimeParser().Apply(new DateTime(2000, 1, 1), new[] { token }); 44 | return TestHelpers.GetRepresentation(date); 45 | } 46 | 47 | [Test(Description = "Assert that the parser can parse a date format and apply it to a date.")] 48 | [TestCase("42 days", Result = "2000-02-12T00:00:00")] 49 | [TestCase("42 fortnights", Result = "2001-08-11T00:00:00")] 50 | public string CanParse(string format) 51 | { 52 | DateTime? date = new TimeParser().Parse(format, new DateTime(2000, 1, 1)); 53 | return TestHelpers.GetRepresentation(date); 54 | } 55 | 56 | [Test(Description = "Assert that valid names are supported for the ParseName method.")] 57 | public void CanParseName() 58 | { 59 | TimeParser parser = new TimeParser(); 60 | Assert.AreEqual(DateTime.Today, parser.ParseName("today"), "The 'today' name returned an unexpected value."); 61 | Assert.AreEqual(DateTime.UtcNow.Date, parser.ParseName("todayUTC"), "The 'todayUTC' name returned an unexpected value."); 62 | Assert.AreEqual(DateTime.Now, parser.ParseName("now"), "The 'now' name returned an unexpected value."); 63 | Assert.AreEqual(DateTime.UtcNow, parser.ParseName("nowUTC"), "The 'nowUTC' name returned an unexpected value."); 64 | } 65 | 66 | 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Tests/Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {EEB818B6-3542-41F8-AFCA-180A3DBA8CC5} 8 | Library 9 | Properties 10 | Pathoschild.NaturalTimeParser.Tests 11 | Pathoschild.NaturalTimeParser.Tests 12 | v4.5 13 | 512 14 | ..\ 15 | true 16 | 17 | 18 | true 19 | full 20 | false 21 | bin\Debug\ 22 | DEBUG;TRACE 23 | prompt 24 | 4 25 | 26 | 27 | pdbonly 28 | true 29 | bin\Release\ 30 | TRACE 31 | prompt 32 | 4 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | ..\packages\DotLiquid.1.7.0\lib\NET40\DotLiquid.dll 45 | 46 | 47 | ..\packages\NUnit.2.6.2\lib\nunit.framework.dll 48 | 49 | 50 | ..\packages\SmartFormat.NET.1.0.0.0\lib\net35-Client\SmartFormat.dll 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | {64dede41-461d-4fbe-9851-c71a77a2cd78} 60 | Extensions.DotLiquid 61 | 62 | 63 | {8F057237-1541-4743-A6F4-925AAC2E01E3} 64 | Extensions.SmartFormatting 65 | 66 | 67 | {5f4a5633-aa16-4944-b344-3e6ec55eb9e4} 68 | Parser 69 | 70 | 71 | 72 | 73 | 80 | -------------------------------------------------------------------------------- /Parser/TimeParser.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using Pathoschild.NaturalTimeParser.Parser.Plugins; 5 | using Pathoschild.NaturalTimeParser.Parser.Tokenization; 6 | 7 | namespace Pathoschild.NaturalTimeParser.Parser 8 | { 9 | /// Parses date input strings matching the GNU input date format. 10 | public class TimeParser 11 | { 12 | /********* 13 | ** Accessors 14 | *********/ 15 | /// The default instance. 16 | public static TimeParser Default = new TimeParser(); 17 | 18 | /// Plugins which parse tokens from the input string. 19 | public IList Parsers { get; protected set; } 20 | 21 | /// Plugins which apply time tokens to a date. 22 | public IList Applicators { get; set; } 23 | 24 | 25 | /********* 26 | ** Public methods 27 | *********/ 28 | /// Construct an instance with the default plugins. 29 | public TimeParser() 30 | { 31 | this.Parsers = new List(new[] { new ArithmeticTimePlugin() }); 32 | this.Applicators = new List(new[] { new ArithmeticTimePlugin() }); 33 | } 34 | 35 | /// Parse a date input string matching the GNU input date format. 36 | /// The date input string. 37 | public DateTime Parse(string input) 38 | { 39 | return this.Parse(input, DateTime.UtcNow); 40 | } 41 | 42 | /// Parse a date input string matching a natural name like 'today'. 43 | /// The date name. Accepted values are today/todayUTC (current date) and now/nowUTC (current datetime). 44 | /// The generated date, or null if the token name is not supported. 45 | public DateTime? ParseName(string token) 46 | { 47 | if (token == null) 48 | return null; 49 | token = token.Trim().ToLower(); 50 | switch (token) 51 | { 52 | case "now": 53 | return DateTime.Now; 54 | 55 | case "today": 56 | return DateTime.Now.Date; 57 | 58 | case "nowutc": 59 | return DateTime.UtcNow; 60 | 61 | case "todayutc": 62 | return DateTime.UtcNow.Date; 63 | 64 | default: 65 | return null; 66 | } 67 | } 68 | 69 | /// Parse a date input string matching the GNU input date format. 70 | /// The date input string. 71 | /// The initial date to which to apply relative formats. 72 | public DateTime Parse(string input, DateTime initial) 73 | { 74 | TimeToken[] tokens = this.Tokenize(input).ToArray(); 75 | return this.Apply(initial, tokens); 76 | } 77 | 78 | /// Converts an input string into a sequence of time tokens. 79 | /// The date input string. 80 | /// A portion of the input string could not be understood as a time token. 81 | /// A parse plugin behaved in an unexpected way. 82 | public IEnumerable Tokenize(string input) 83 | { 84 | string remaining = input; 85 | while (true) 86 | { 87 | // input parsing complete 88 | bool matched = false; 89 | remaining = remaining.Trim(); 90 | if (remaining.Length == 0) 91 | yield break; 92 | 93 | // call each parser 94 | foreach (IParseTimeStrings parser in this.Parsers) 95 | { 96 | // parse tokens 97 | TimeToken[] tokens = parser.Tokenize(remaining).ToArray(); 98 | if (!tokens.Any()) 99 | continue; 100 | 101 | // handle matched tokens 102 | matched = true; 103 | foreach (TimeToken token in tokens) 104 | { 105 | // strip token from input 106 | int tokenIndex = remaining.IndexOf(token.Match, StringComparison.InvariantCulture); 107 | if (tokenIndex == -1) 108 | throw new InvalidOperationException(String.Format("The matched time token '{0}' was not found in the input string.", token.Match)); 109 | if (tokenIndex != 0) 110 | throw new InvalidOperationException(String.Format("The matched time token '{0}' did not match the next segment of the input string.", token.Match)); 111 | remaining = remaining.Substring(token.Match.Length); 112 | 113 | // return token 114 | yield return token; 115 | } 116 | 117 | // start over with new string 118 | break; 119 | } 120 | 121 | // no parser matched 122 | if (!matched) 123 | throw new TimeParseFormatException(input, remaining); 124 | } 125 | } 126 | 127 | /// Apply a sequence of time tokens to a date. 128 | /// The initial date. 129 | /// The tokens which modify the date. 130 | public DateTime Apply(DateTime date, IEnumerable tokens) 131 | { 132 | foreach (TimeToken token in tokens) 133 | { 134 | bool matched = false; 135 | foreach (IApplyTimeTokens applicator in this.Applicators) 136 | { 137 | DateTime? result = applicator.TryApply(token, date); 138 | if (result != null) 139 | { 140 | matched = true; 141 | date = result.Value; 142 | break; 143 | } 144 | } 145 | if (!matched) 146 | throw new InvalidOperationException(String.Format("There is no time applicator plugin which recognizes the token '{0}'. The parsed type is '{1}' with a value of '{2}'.", token.Match, token.Parser, token.Value)); 147 | } 148 | 149 | return date; 150 | } 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /Parser/Plugins/ArithmeticTimePlugin.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Globalization; 4 | using System.Text.RegularExpressions; 5 | using Pathoschild.NaturalTimeParser.Parser.Tokenization; 6 | 7 | namespace Pathoschild.NaturalTimeParser.Parser.Plugins 8 | { 9 | /// Parses a natural time format string containing time arithmetic (like "+2 days") into a set of tokens. 10 | /// This is an implementation of GNU relative items: http://www.gnu.org/software/tar/manual/html_node/Relative-items-in-date-strings.html 11 | public class ArithmeticTimePlugin : IParseTimeStrings, IApplyTimeTokens 12 | { 13 | /********* 14 | ** Properties 15 | *********/ 16 | /// The regular expression that matches the date tokens in the input expression. 17 | protected readonly Regex ParsePattern = new Regex(@"^(?\s*((?[\+\-]{0,1})\s*(?\d*))?\s*\b(?\w+)\b(?(\s*\bago\b)?)\s*)+", RegexOptions.Compiled | RegexOptions.ExplicitCapture); 18 | 19 | 20 | /********* 21 | ** Accessors 22 | *********/ 23 | /// The arbitrary key which identifies this plugin. 24 | public const string Key = "Arithmetic"; 25 | 26 | /// The supported time units. 27 | /// This provides a case-insensitive unit lookup when parsing relative time items. The optional -s suffix is stripped before this lookup. 28 | public readonly IDictionary SupportedUnits = new Dictionary(StringComparer.InvariantCultureIgnoreCase) 29 | { 30 | { "sec", RelativeTimeUnit.Seconds }, 31 | { "second", RelativeTimeUnit.Seconds }, 32 | { "min", RelativeTimeUnit.Minutes }, 33 | { "minute", RelativeTimeUnit.Minutes }, 34 | { "hour", RelativeTimeUnit.Hours }, 35 | { "day", RelativeTimeUnit.Days }, 36 | { "week", RelativeTimeUnit.Weeks }, 37 | { "fortnight", RelativeTimeUnit.Fortnights}, 38 | { "month", RelativeTimeUnit.Months }, 39 | { "year", RelativeTimeUnit.Years } 40 | }; 41 | 42 | 43 | /********* 44 | ** Public methods 45 | *********/ 46 | /// Scan the front of an input string to read a set of matching tokens. 47 | /// The natural time format string. 48 | /// Returns a set of matching tokens, or an empty collection if no supported token was found. 49 | public IEnumerable Tokenize(string input) 50 | { 51 | // parse input 52 | Match match = this.ParsePattern.Match(input); 53 | if (!match.Success) 54 | yield break; 55 | 56 | // extract tokens 57 | int patternCount = match.Groups["expression"].Captures.Count; 58 | for (int i = 0; i < patternCount; i++) 59 | { 60 | // extract parts 61 | string expression = match.Groups["expression"].Captures[i].Value; 62 | string sign = match.Groups["sign"].Captures[i].Value; 63 | string rawValue = match.Groups["value"].Captures[i].Value; 64 | string rawUnit = match.Groups["unit"].Captures[i].Value; 65 | bool negate = match.Groups["negate"].Captures[i].Value.Length > 0; 66 | 67 | // parse value 68 | int value = string.IsNullOrWhiteSpace(rawValue) ? 1 : int.Parse(rawValue); 69 | if (sign == "-") 70 | value *= -1; 71 | if (negate) 72 | value *= -1; // note: double-negation (like -1 year ago) is valid 73 | 74 | // parse unit 75 | RelativeTimeUnit unit = this.ParseUnit(rawUnit); 76 | if (unit == RelativeTimeUnit.Unknown) 77 | yield break; // unsupported unit 78 | 79 | // return token 80 | yield return new TimeToken(ArithmeticTimePlugin.Key, expression, value.ToString(CultureInfo.InvariantCulture), unit); 81 | } 82 | } 83 | 84 | /// Apply a natural time token to a date value. 85 | /// The natural time token to apply. 86 | /// The date value to apply the token to. 87 | /// Returns the modified date, or null if the token is not supported. 88 | public DateTime? TryApply(TimeToken token, DateTime date) 89 | { 90 | // parse token 91 | if (token.Parser != ArithmeticTimePlugin.Key || !(token.Context is RelativeTimeUnit)) 92 | return null; 93 | RelativeTimeUnit unit = (RelativeTimeUnit)token.Context; 94 | int value = int.Parse(token.Value); 95 | 96 | // apply 97 | switch (unit) 98 | { 99 | case RelativeTimeUnit.Seconds: 100 | return date.AddSeconds(value); 101 | 102 | case RelativeTimeUnit.Minutes: 103 | return date.AddMinutes(value); 104 | 105 | case RelativeTimeUnit.Hours: 106 | return date.AddHours(value); 107 | 108 | case RelativeTimeUnit.Days: 109 | return date.AddDays(value); 110 | 111 | case RelativeTimeUnit.Weeks: 112 | return date.AddDays(value * 7); 113 | 114 | case RelativeTimeUnit.Fortnights: 115 | return date.AddDays(value * 14); 116 | 117 | case RelativeTimeUnit.Months: 118 | return date.AddMonths(value); 119 | 120 | case RelativeTimeUnit.Years: 121 | return date.AddYears(value); 122 | 123 | default: 124 | throw new FormatException(String.Format("Invalid arithmetic time unit: {0}", unit)); 125 | } 126 | } 127 | 128 | 129 | /********* 130 | ** Protected methods 131 | *********/ 132 | /// Parse a localized time unit (like "secs") into a . 133 | /// The localized time unit. 134 | protected RelativeTimeUnit ParseUnit(string unit) 135 | { 136 | // exact match 137 | if (this.SupportedUnits.ContainsKey(unit)) 138 | return this.SupportedUnits[unit]; 139 | 140 | // without -s suffix 141 | if (unit.EndsWith("s", StringComparison.InvariantCultureIgnoreCase)) 142 | { 143 | unit = unit.Substring(0, unit.Length - 1); 144 | if (this.SupportedUnits.ContainsKey(unit)) 145 | return this.SupportedUnits[unit]; 146 | } 147 | 148 | // unknown unit 149 | return RelativeTimeUnit.Unknown; 150 | } 151 | } 152 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **Pathoschild.NaturalTimeParser** implements part of the 2 | [GNU date input format](http://www.gnu.org/software/tar/manual/html_node/Date-input-formats.html). 3 | It lets you use date math with natural date strings (like 4 | `DateTime.Now.Offset("+5 days 14 hours -2 minutes")`), and will eventually support creating dates 5 | from natural time formats (like `"last month +2 days"`). The parser can be used by itself, or 6 | integrated with a templating engine like DotLiquid or SmartFormat.NET (see details below). 7 | 8 | This is used in at least one production system, so it's reasonably robust. Contributions to further 9 | develop the library are welcome. 10 | 11 | ## Usage 12 | Download the [`Pathoschild.NaturalTimeParser` NuGet package](https://nuget.org/packages/Pathoschild.NaturalTimeParser/) 13 | and reference the `Pathoschild.NaturalTimeParser` namespace. This lets you apply a natural offset 14 | to a date: 15 | ```c# 16 | // both lines are equivalent 17 | DateTime result = DateTime.Now.Offset("2 years ago"); 18 | DateTime result = TimeParser.Default.Parse("2 years ago"); 19 | ``` 20 | 21 | ### Relative time units (date arithmetic) 22 | The parser has full support for [relative time units](http://www.gnu.org/software/tar/manual/html_node/Relative-items-in-date-strings.html#SEC125). 23 | For example, the following formats are supported: 24 | 25 | * `1 year ago` 26 | * `-2 years` 27 | * `16 fortnights` 28 | * `-1 year ago` (next year) 29 | 30 | You can also chain relative units: 31 | 32 | * `1 year 2 months` (14 months from now) 33 | * `1 year -2 fortnights` (almost 11 months from now) 34 | * `1 year ago 1 year` (today; equivalent to `-1 year +1 year`) 35 | 36 | ### Integrated with template engines 37 | ##### DotLiquid 38 | The parser is available as a plugin for [DotLiquid](http://dotliquidmarkup.org/) through the 39 | `Pathoschild.NaturalTimeParser.DotLiquid` NuGet package. DotLiquid is a safe templating library 40 | that lets you format strings with token replacement, basic logic, and text transforms. For example, 41 | this lets us format messages like this: 42 | ```c# 43 | Template.RegisterFilter(typeof(NaturalDateFilter)); 44 | string message = "Your trial will expire in 30 days (on {{ 'today' | as_date | date_offset:'30 days' | date:'yyyy-MM-dd' }})."; 45 | message = Template.Parse(message).Render(); // "Your trial will expire in 30 days (on 2013-06-01)." 46 | ``` 47 | 48 | The plugin adds four custom tokens (`{Today}`/`{TodayUTC}` for the current local/UTC date, and 49 | `{Now}`/`{NowUTC}` for the current local/UTC date & time) and adds support for applying relative 50 | time units to any date. For example, you can format an arbitrary date token: 51 | ```c# 52 | Template.RegisterFilter(typeof(NaturalDateFilter)); 53 | string message = "Your trial will expire in a long time (on {{ expiry_date | date_offset:'30 days' | date:'yyyy-MM-dd' }})."; 54 | message = Template.Parse(message).Render(Hash.FromAnonymousObject(new { ExpiryDate = new DateTime(2050, 01, 01) } }); // "Your trial will expire in a long time (on 2050-01-31)." 55 | ``` 56 | 57 | ##### SmartFormat 58 | The parser is available as a plugin for [SmartFormat.NET](https://github.com/scottrippey/SmartFormat.NET) 59 | through the `Pathoschild.NaturalTimeParser.SmartFormat` NuGet package. SmartFormat is a string 60 | composition library that enables advanced token replacement. For example, this lets us format 61 | messages like this: 62 | ```c# 63 | SmartFormatter formatter = Smart.CreateDefaultSmartFormat().AddExtensionsForNaturalTime(); 64 | string message = "Your trial will expire in 30 days (on {Today:yyyy-MM-dd|+30 days})."; 65 | message = formatter.Format(message); // "Your trial will expire in 30 days (on 2013-06-01)."; 66 | ``` 67 | 68 | The plugin adds four custom tokens (`{Today}`/`{TodayUTC}` for the current local/UTC date, and 69 | `{Now}`/`{NowUTC}` for the current local/UTC date & time) and adds support for applying relative 70 | time units to any date. For example, you can format an arbitrary date token: 71 | ```c# 72 | SmartFormatter formatter = Smart.CreateDefaultSmartFormat().AddExtensionsForNaturalTime(); 73 | string message = "Your trial will expire in a long time (on {ExpiryDate:yyyy-MM-dd|+30 days})."; 74 | message = formatter.Format(message, new { ExpiryDate = new DateTime(2050, 01, 01) }); // "Your trial will expire in a long time (on 2050-01-31)."; 75 | ``` 76 | 77 | ## Extending the parser 78 | ### Localization 79 | The default implementation is English but can support other languages. For example, you can enable 80 | relative time units in French: 81 | ```c# 82 | // configure French units 83 | ArithmeticTimePlugin plugin = TimeParser.Default.Parsers.OfType().First(); 84 | plugin.SupportedUnits["jour"] = ArithmeticTimePlugin.RelativeTimeUnit.Days; 85 | plugin.SupportedUnits["heure"] = ArithmeticTimePlugin.RelativeTimeUnit.Hours; 86 | 87 | // now you can use French 88 | DateTime.Now.Offset("3 jours 4 heures"); 89 | ``` 90 | 91 | ### Plugins 92 | This is implemented as a simple plugin-based lexer, which breaks down an input string into 93 | its constituent tokens. For example, the string "`yesterday +1 day`" can be broken down into two 94 | tokens: 95 | ```js 96 | [ 97 | ["yesterday"], 98 | ["days", 1] 99 | ] 100 | ``` 101 | 102 | The parsing is provided by a set of plugins which implement `IParseTimeStrings` or 103 | `IApplyTimeTokens`: 104 | 105 | * `IParseTimeStrings` plugins are called to tokenize the input string. Each plugin scans the 106 | front of the string for recognized tokens, and stops at the first unrecognized value. Each 107 | matched token is stripped, and this is repeated until the entire string has been tokenized, or a 108 | portion is not recognized by any of the plugins (in which case a `TimeParseFormatException` is 109 | thrown). 110 | * `IApplyTimeTokens` plugins are called to apply a token to a date. For example, the 111 | `ArithmeticTimePlugin` applies a token like `+3 days` by returning `date.AddDays(3)`. 112 | 113 | New plugins can be added easily: 114 | ```c# 115 | TimeParser parser = new TimeParser(); // or TimeParser.Default 116 | parser.Parsers.Add(new SomePlugin()); 117 | parser.Applicators.Add(new SomePlugin()); 118 | ``` -------------------------------------------------------------------------------- /.nuget/NuGet.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | $(MSBuildProjectDirectory)\..\ 5 | 6 | 7 | false 8 | 9 | 10 | false 11 | 12 | 13 | true 14 | 15 | 16 | false 17 | 18 | 19 | 20 | 21 | 22 | 26 | 27 | 28 | 29 | 30 | $([System.IO.Path]::Combine($(SolutionDir), ".nuget")) 31 | $([System.IO.Path]::Combine($(ProjectDir), "packages.config")) 32 | 33 | 34 | 35 | 36 | $(SolutionDir).nuget 37 | packages.config 38 | 39 | 40 | 41 | 42 | $(NuGetToolsPath)\nuget.exe 43 | @(PackageSource) 44 | 45 | "$(NuGetExePath)" 46 | mono --runtime=v4.0.30319 $(NuGetExePath) 47 | 48 | $(TargetDir.Trim('\\')) 49 | 50 | -RequireConsent 51 | 52 | $(NuGetCommand) install "$(PackagesConfig)" -source "$(PackageSources)" $(RequireConsentSwitch) -solutionDir "$(SolutionDir) " 53 | $(NuGetCommand) pack "$(ProjectPath)" -p Configuration=$(Configuration) -o "$(PackageOutputDir)" -symbols 54 | 55 | 56 | 57 | RestorePackages; 58 | $(BuildDependsOn); 59 | 60 | 61 | 62 | 63 | $(BuildDependsOn); 64 | BuildPackage; 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 87 | 88 | 91 | 92 | 93 | 94 | 96 | 97 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 148 | 149 | 150 | 151 | -------------------------------------------------------------------------------- /Tests/Plugins/ArithmeticTimePluginTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Globalization; 4 | using NUnit.Framework; 5 | using Pathoschild.NaturalTimeParser.Parser; 6 | using Pathoschild.NaturalTimeParser.Parser.Plugins; 7 | using Pathoschild.NaturalTimeParser.Parser.Tokenization; 8 | 9 | namespace Pathoschild.NaturalTimeParser.Tests.Plugins 10 | { 11 | /// Asserts that the supports all valid scenarios. 12 | [TestFixture] 13 | public class ArithmeticTimePluginTests 14 | { 15 | /********* 16 | ** Unit tests 17 | *********/ 18 | /*** 19 | ** Tokenize 20 | ***/ 21 | [Test(Description = "Assert that standard GNU relative time units are correctly tokenized.")] 22 | [TestCase("42 sec", Result = "[Seconds:42]")] 23 | [TestCase("42 secs", Result = "[Seconds:42]")] 24 | [TestCase("42 seconds", Result = "[Seconds:42]")] 25 | [TestCase("42 seconds", Result = "[Seconds:42]")] 26 | [TestCase("42 min", Result = "[Minutes:42]")] 27 | [TestCase("42 mins", Result = "[Minutes:42]")] 28 | [TestCase("42 minute", Result = "[Minutes:42]")] 29 | [TestCase("42 minutes", Result = "[Minutes:42]")] 30 | [TestCase("42 hour", Result = "[Hours:42]")] 31 | [TestCase("42 hours", Result = "[Hours:42]")] 32 | [TestCase("42 day", Result = "[Days:42]")] 33 | [TestCase("42 days", Result = "[Days:42]")] 34 | [TestCase("42 week", Result = "[Weeks:42]")] 35 | [TestCase("42 weeks", Result = "[Weeks:42]")] 36 | [TestCase("42 fortnight", Result = "[Fortnights:42]")] 37 | [TestCase("42 fortnights", Result = "[Fortnights:42]")] 38 | [TestCase("42 month", Result = "[Months:42]")] 39 | [TestCase("42 months", Result = "[Months:42]")] 40 | [TestCase("42 year", Result = "[Years:42]")] 41 | [TestCase("42 years", Result = "[Years:42]")] 42 | public string Tokenize_SupportsStandardUnits(string format) 43 | { 44 | return this.Tokenize(new ArithmeticTimePlugin(), format); 45 | } 46 | 47 | [Test(Description = "Assert that all the standard GNU formats are correctly tokenized. This includes keywords like 'ago', and optional signs and multipliers.")] 48 | [TestCase("years", Result = "[Years:1]")] 49 | [TestCase("+years", Result = "[Years:1]")] 50 | [TestCase("-years", Result = "[Years:-1]")] 51 | [TestCase("years ago", Result = "[Years:-1]")] 52 | [TestCase("+years ago", Result = "[Years:-1]")] 53 | [TestCase("-years ago", Result = "[Years:1]")] 54 | [TestCase("15 years", Result = "[Years:15]")] 55 | [TestCase("+15 years", Result = "[Years:15]")] 56 | [TestCase("-15 years", Result = "[Years:-15]")] 57 | [TestCase("15 years ago", Result = "[Years:-15]")] 58 | [TestCase("+15 years ago", Result = "[Years:-15]")] 59 | [TestCase("-15 years ago", Result = "[Years:15]")] 60 | [TestCase(" -15 years ago", Result = "[Years:15]")] 61 | public string Tokenize_SupportsStandardFormats(string format) 62 | { 63 | return this.Tokenize(new ArithmeticTimePlugin(), format); 64 | } 65 | 66 | [Test(Description = "Assert that invalid formats are ignored by the plugin. It should return no tokens since it could not parse them.")] 67 | [TestCase("invalid format", Result = "")] 68 | [TestCase("15 eggs ago", Result = "")] 69 | [TestCase("four eggs ago", Result = "")] 70 | [TestCase("four eggs ago 15 days ago", Result = "")] 71 | [TestCase(null, ExpectedException = typeof(ArgumentNullException))] 72 | public string Tokenize_IgnoresInvalidFormats(string format) 73 | { 74 | return this.Tokenize(new ArithmeticTimePlugin(), format); 75 | } 76 | 77 | [Test(Description = "Assert that time units are case-insensitive (so '5 months' is identical to '5 MONTHS').")] 78 | [TestCase("42 FORTNIGHTS", Result = "[Fortnights:42]")] 79 | [TestCase("42 MoNth", Result = "[Months:42]")] 80 | public string Tokenize_IsCaseInsensitive(string format) 81 | { 82 | return this.Tokenize(new ArithmeticTimePlugin(), format); 83 | } 84 | 85 | [Test(Description = "Assert that time units are correctly tokenized when they're chained together.")] 86 | [TestCase("15 years 3 months 2 hours", Result = "[Years:15][Months:3][Hours:2]")] 87 | [TestCase("15 years -12 months 2 fortnights 3 weeks -17 days ago -hours 2 minutes secs", Result = "[Years:15][Months:-12][Fortnights:2][Weeks:3][Days:17][Hours:-1][Minutes:2][Seconds:1]")] 88 | [TestCase("15 years -months +months ago -2 fortnights 3 weeks -17 days ago -hours 2 minutes secs", Result = "[Years:15][Months:-1][Months:-1][Fortnights:-2][Weeks:3][Days:17][Hours:-1][Minutes:2][Seconds:1]")] 89 | public string Tokenize_CanChainUnits(string format) 90 | { 91 | return this.Tokenize(new ArithmeticTimePlugin(), format); 92 | } 93 | 94 | [Test(Description = "Assert that the units can be localized.")] 95 | [TestCase("42 secondes", Result = "[Seconds:42]")] 96 | [TestCase("42 minutes", Result = "[Minutes:42]")] 97 | [TestCase("42 heures", Result = "[Hours:42]")] 98 | [TestCase("42 jours", Result = "[Days:42]")] 99 | [TestCase("42 semaines", Result = "[Weeks:42]")] 100 | [TestCase("42 mois", Result = "[Months:42]")] 101 | [TestCase("42 ans", Result = "[Years:42]")] 102 | [TestCase("42 années", Result = "[Years:42]")] 103 | public string Tokenize_CanLocalizeUnits(string format) 104 | { 105 | ArithmeticTimePlugin plugin = new ArithmeticTimePlugin(); 106 | plugin.SupportedUnits["seconde"] = RelativeTimeUnit.Seconds; 107 | plugin.SupportedUnits["heure"] = RelativeTimeUnit.Hours; 108 | plugin.SupportedUnits["jour"] = RelativeTimeUnit.Days; 109 | plugin.SupportedUnits["semaine"] = RelativeTimeUnit.Weeks; 110 | plugin.SupportedUnits["mois"] = RelativeTimeUnit.Months; 111 | plugin.SupportedUnits["an"] = RelativeTimeUnit.Years; 112 | plugin.SupportedUnits["année"] = RelativeTimeUnit.Years; 113 | return this.Tokenize(plugin, format); 114 | } 115 | 116 | /*** 117 | ** Apply 118 | ***/ 119 | [Test(Description = "Assert that tokens for supported time units are correctly applied to a date.")] 120 | [TestCase(42, RelativeTimeUnit.Seconds, Result = "2000-01-01T00:00:42")] 121 | [TestCase(42, RelativeTimeUnit.Minutes, Result = "2000-01-01T00:42:00")] 122 | [TestCase(42, RelativeTimeUnit.Hours, Result = "2000-01-02T18:00:00")] 123 | [TestCase(42, RelativeTimeUnit.Days, Result = "2000-02-12T00:00:00")] 124 | [TestCase(42, RelativeTimeUnit.Weeks, Result = "2000-10-21T00:00:00")] 125 | [TestCase(42, RelativeTimeUnit.Fortnights, Result = "2001-08-11T00:00:00")] 126 | [TestCase(42, RelativeTimeUnit.Months, Result = "2003-07-01T00:00:00")] 127 | [TestCase(42, RelativeTimeUnit.Years, Result = "2042-01-01T00:00:00")] 128 | [TestCase(42, RelativeTimeUnit.Unknown, ExpectedException = typeof(FormatException))] 129 | public string Apply_SupportsStandardUnits(int value, RelativeTimeUnit unit) 130 | { 131 | return this.TryApply(value, unit); 132 | } 133 | 134 | [Test(Description = "Assert that tokens for negative multipliers of supported time units are correctly applied to a date.")] 135 | [TestCase(-42, RelativeTimeUnit.Seconds, Result = "1999-12-31T23:59:18")] 136 | [TestCase(-42, RelativeTimeUnit.Minutes, Result = "1999-12-31T23:18:00")] 137 | [TestCase(-42, RelativeTimeUnit.Hours, Result = "1999-12-30T06:00:00")] 138 | [TestCase(-42, RelativeTimeUnit.Days, Result = "1999-11-20T00:00:00")] 139 | [TestCase(-42, RelativeTimeUnit.Weeks, Result = "1999-03-13T00:00:00")] 140 | [TestCase(-42, RelativeTimeUnit.Fortnights, Result = "1998-05-23T00:00:00")] 141 | [TestCase(-42, RelativeTimeUnit.Months, Result = "1996-07-01T00:00:00")] 142 | [TestCase(-42, RelativeTimeUnit.Years, Result = "1958-01-01T00:00:00")] 143 | [TestCase(-42, RelativeTimeUnit.Unknown, ExpectedException = typeof(FormatException))] 144 | public string Apply_SupportsNegativeUnits(int value, RelativeTimeUnit unit) 145 | { 146 | return this.TryApply(value, unit); 147 | } 148 | 149 | [Test(Description = "Assert that tokens for zero multipliers of supported time units are equivalent to the original date.")] 150 | [TestCase(0, RelativeTimeUnit.Seconds, Result = "2000-01-01T00:00:00")] 151 | [TestCase(0, RelativeTimeUnit.Minutes, Result = "2000-01-01T00:00:00")] 152 | [TestCase(0, RelativeTimeUnit.Hours, Result = "2000-01-01T00:00:00")] 153 | [TestCase(0, RelativeTimeUnit.Days, Result = "2000-01-01T00:00:00")] 154 | [TestCase(0, RelativeTimeUnit.Weeks, Result = "2000-01-01T00:00:00")] 155 | [TestCase(0, RelativeTimeUnit.Fortnights, Result = "2000-01-01T00:00:00")] 156 | [TestCase(0, RelativeTimeUnit.Months, Result = "2000-01-01T00:00:00")] 157 | [TestCase(0, RelativeTimeUnit.Years, Result = "2000-01-01T00:00:00")] 158 | [TestCase(0, RelativeTimeUnit.Unknown, ExpectedException = typeof(FormatException))] 159 | public string Apply_SupportsZeroUnits(int value, RelativeTimeUnit unit) 160 | { 161 | return this.TryApply(value, unit); 162 | } 163 | 164 | 165 | /********* 166 | ** Protected methods 167 | *********/ 168 | /// Tokenize a date format using a new instance, and return a string representation of the resulting tokens. 169 | /// The plugin which to tokenize the string. 170 | /// The relative time format. 171 | protected string Tokenize(ArithmeticTimePlugin plugin, string format) 172 | { 173 | IEnumerable tokens = plugin.Tokenize(format); 174 | return TestHelpers.GetRepresentation(tokens); 175 | } 176 | 177 | /// Apply a relative time token to the a UTC date for 2000-01-01 using a new instance, and return a string representation of the resulting date. 178 | /// The relative time multiplier. 179 | /// The relative time unit. 180 | protected string TryApply(int value, RelativeTimeUnit unit) 181 | { 182 | DateTime date = new DateTime(2000, 1, 1); 183 | TimeToken token = new TimeToken(ArithmeticTimePlugin.Key, null, value.ToString(CultureInfo.InvariantCulture), unit); 184 | DateTime? result = new ArithmeticTimePlugin().TryApply(token, date); 185 | return TestHelpers.GetRepresentation(result); 186 | } 187 | } 188 | } 189 | --------------------------------------------------------------------------------