├── .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 |
--------------------------------------------------------------------------------