From 7d62d40a6971ec7d1b0ca9744b005208a2ae5798 Mon Sep 17 00:00:00 2001 From: Daniel Stolt Date: Fri, 25 Jul 2014 16:51:29 +0200 Subject: [PATCH] Integrated the IDateFormatter abstraction and a first draft implementation and unit tests. --- .../Localization/DateParseResultTests.cs | 32 ++++ .../Localization/DefaultDateFormatterTests.cs | 65 ++++++++ .../Orchard.Framework.Tests.csproj | 2 + src/Orchard/Localization/Models/DateParts.cs | 45 ++++++ .../Localization/Models/DateTimeParts.cs | 42 ++++++ src/Orchard/Localization/Models/TimeParts.cs | 56 +++++++ .../Services/DefaultDateFormatter.cs | 141 ++++++++++++++++++ .../Localization/Services/IDateFormatter.cs | 16 ++ src/Orchard/Orchard.Framework.csproj | 5 + 9 files changed, 404 insertions(+) create mode 100644 src/Orchard.Tests/Localization/DateParseResultTests.cs create mode 100644 src/Orchard.Tests/Localization/DefaultDateFormatterTests.cs create mode 100644 src/Orchard/Localization/Models/DateParts.cs create mode 100644 src/Orchard/Localization/Models/DateTimeParts.cs create mode 100644 src/Orchard/Localization/Models/TimeParts.cs create mode 100644 src/Orchard/Localization/Services/DefaultDateFormatter.cs create mode 100644 src/Orchard/Localization/Services/IDateFormatter.cs diff --git a/src/Orchard.Tests/Localization/DateParseResultTests.cs b/src/Orchard.Tests/Localization/DateParseResultTests.cs new file mode 100644 index 000000000..32cd85003 --- /dev/null +++ b/src/Orchard.Tests/Localization/DateParseResultTests.cs @@ -0,0 +1,32 @@ +using System; +using NUnit.Framework; +using Orchard.Framework.Localization.Models; + +namespace Orchard.Framework.Tests.Localization { + + [TestFixture] + public class DateParseResultTests { + + [Test] + [Description("Equal instances return equality.")] + public void EqualsTest01() { + var target = new DateTimeParts(2014, 5, 31, 10, 0, 0, 0); + var other = new DateTimeParts(2014, 5, 31, 10, 0, 0, 0); + + var result = target.Equals(other); + + Assert.IsTrue(result); + } + + [Test] + [Description("Different instances do not return equality.")] + public void EqualsTest02() { + var target = new DateTimeParts(2014, 5, 31, 10, 0, 0, 0); + var other = new DateTimeParts(2014, 5, 31, 10, 0, 0, 1); + + var result = target.Equals(other); + + Assert.IsFalse(result); + } + } +} diff --git a/src/Orchard.Tests/Localization/DefaultDateFormatterTests.cs b/src/Orchard.Tests/Localization/DefaultDateFormatterTests.cs new file mode 100644 index 000000000..761a22813 --- /dev/null +++ b/src/Orchard.Tests/Localization/DefaultDateFormatterTests.cs @@ -0,0 +1,65 @@ +using System; +using System.Globalization; +using NUnit.Framework; +using Orchard.Framework.Localization.Models; +using Orchard.Framework.Localization.Services; + +namespace Orchard.Framework.Tests.Localization { + + [TestFixture] + public class DefaultDateFormatterTests { + + [Test] + [Description("Correct Swedish date is parsed correctly.")] + public void ParseTest01() { + IDateFormatter target = new DefaultDateFormatter(); + var cultureInfo = CultureInfo.GetCultureInfo("sv-SE"); + var dateTimeString = "2014-05-31 10:00:00"; + + var result = target.ParseDateTime(dateTimeString, cultureInfo); + + var expected = new DateTimeParts(2014, 5, 31, 10, 0, 0, 0); + Assert.AreEqual(expected, result); + } + + [Test] + [Description("Correct US English date is parsed correctly.")] + public void ParseTest02() { + IDateFormatter target = new DefaultDateFormatter(); + var cultureInfo = CultureInfo.GetCultureInfo("en-US"); + + var dateString = "5/31/2014 10:00:00 AM"; + var result = target.ParseDateTime(dateString, cultureInfo); + + var expected = new DateTimeParts(2014, 5, 31, 10, 0, 0, 0); + Assert.AreEqual(expected, result); + } + + [Test] + [Description("Incorrect US English date yields a FormatException.")] + [ExpectedException(typeof(FormatException))] + public void ParseTest03() { + IDateFormatter target = new DefaultDateFormatter(); + var cultureInfo = CultureInfo.GetCultureInfo("en-US"); + var dateString = "blablabla"; + + target.ParseDateTime(dateString, cultureInfo); + } + + [Test] + [Description("Loop through all cultures. Test Parse method by all possible DateTimeFormats.")] + public void ParseTest04() { + IDateFormatter target = new DefaultDateFormatter(); + var cultures = CultureInfo.GetCultures(CultureTypes.AllCultures); + foreach (CultureInfo cultureInfo in cultures) { + DateTime dateTime = new DateTime(2014, 12, 31, 10, 20, 40, 567); + cultureInfo.DateTimeFormat.Calendar = new GregorianCalendar(); + var dateString = dateTime.ToString("G", cultureInfo); + var result = target.ParseDateTime(dateString, cultureInfo); + var millisecond = DateTime.Parse(dateString, cultureInfo.DateTimeFormat).Millisecond; + var expected = new DateTimeParts(2014, 12, 31, 10, 20, 40, millisecond); + Assert.AreEqual(expected, result); + } + } + } +} diff --git a/src/Orchard.Tests/Orchard.Framework.Tests.csproj b/src/Orchard.Tests/Orchard.Framework.Tests.csproj index 90c04ced1..30c4e6718 100644 --- a/src/Orchard.Tests/Orchard.Framework.Tests.csproj +++ b/src/Orchard.Tests/Orchard.Framework.Tests.csproj @@ -259,6 +259,8 @@ + + diff --git a/src/Orchard/Localization/Models/DateParts.cs b/src/Orchard/Localization/Models/DateParts.cs new file mode 100644 index 000000000..d61930a86 --- /dev/null +++ b/src/Orchard/Localization/Models/DateParts.cs @@ -0,0 +1,45 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Orchard.Framework.Localization.Models { + public struct DateParts { + + public DateParts(int year, int month, int day) { + _year = year; + _month = month; + _day = day; + } + + private readonly int _day; + private readonly int _month; + private readonly int _year; + + public int Year { + get { + return _year; + } + } + public int Month { + get { + return _month; + } + } + public int Day { + get { + return _day; + } + } + + //public override bool Equals(object obj) { + // var other = (DateParts)obj; + + // if (Year != other.Year || + // Month != other.Month || + // Day != other.Day) + // return false; + + // return true; + //} + } +} diff --git a/src/Orchard/Localization/Models/DateTimeParts.cs b/src/Orchard/Localization/Models/DateTimeParts.cs new file mode 100644 index 000000000..82f43bfa7 --- /dev/null +++ b/src/Orchard/Localization/Models/DateTimeParts.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Orchard.Framework.Localization.Models { + public struct DateTimeParts { + + public DateTimeParts(int year, int month, int day, int hour, int minute, int second, int millisecond) { + _date = new DateParts(year, month, day); + _time = new TimeParts(hour, minute, second, millisecond); + } + + public DateTimeParts(DateParts dateParts, TimeParts timeParts) { + _date = dateParts; + _time = timeParts; + } + + private readonly DateParts _date; + private readonly TimeParts _time; + + public DateParts Date { + get { + return _date; + } + } + public TimeParts Time { + get { + return _time; + } + } + + //public override bool Equals(object obj) { + // var other = (DateTimeParts)obj; + + // if (!(Date.Equals(other.Date)) || + // !(Time.Equals(other.Time))) + // return false; + + // return true; + //} + } +} diff --git a/src/Orchard/Localization/Models/TimeParts.cs b/src/Orchard/Localization/Models/TimeParts.cs new file mode 100644 index 000000000..f71a50392 --- /dev/null +++ b/src/Orchard/Localization/Models/TimeParts.cs @@ -0,0 +1,56 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Orchard.Framework.Localization.Models { + public struct TimeParts { + + public TimeParts(int hour, int minute, int second, int millisecond) { + _hour = hour; + _minute = minute; + _second = second; + _millisecond = millisecond; + } + + private readonly int _hour; + private readonly int _minute; + private readonly int _second; + private readonly int _millisecond; + + public int Hour { + get { + return _hour; + } + } + + public int Minute { + get { + return _minute; + } + } + + public int Second { + get { + return _second; + } + } + + public int Millisecond { + get { + return _millisecond; + } + } + + //public override bool Equals(object obj) { + // var other = (TimeParts)obj; + + // if (Hour != other.Hour || + // Minute != other.Minute || + // Second != other.Second || + // Millisecond != other.Millisecond) + // return false; + + // return Hour == other.Hour; + //} + } +} diff --git a/src/Orchard/Localization/Services/DefaultDateFormatter.cs b/src/Orchard/Localization/Services/DefaultDateFormatter.cs new file mode 100644 index 000000000..433264daf --- /dev/null +++ b/src/Orchard/Localization/Services/DefaultDateFormatter.cs @@ -0,0 +1,141 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text.RegularExpressions; +using Orchard.Framework.Localization.Models; + +namespace Orchard.Framework.Localization.Services { + public class DefaultDateFormatter : IDateFormatter { + + DateTimeParts IDateFormatter.ParseDateTime(string dateTimeString, CultureInfo culture) { + DateParts date = ((IDateFormatter)this).ParseDate(dateTimeString, culture); + TimeParts time = ((IDateFormatter)this).ParseTime(dateTimeString, culture); + return new DateTimeParts(date, time); + } + + DateParts IDateFormatter.ParseDate(string dateString, CultureInfo culture) { + var dateFormatString = Regex.Replace(culture.DateTimeFormat.ShortDatePattern, @"\.|\$|\^|\{|\[|\(|\||\)|\*|\+|\?|\\", m => String.Format(@"\{0}", m.Value)); + dateFormatString = Regex.Replace(dateFormatString, @"(? String.Format("(.{{{0}}})", m.Value.Replace("\\", "").Length - 2)); + + var dateFormat = ReplaceAll(dateFormatString, GetDateParseReplacements(culture)); + + if (!Regex.IsMatch(dateString, dateFormat, RegexOptions.IgnoreCase)) + throw new FormatException("Invalid date format."); + + Match dateMatch = Regex.Match(dateString, dateFormat, RegexOptions.IgnoreCase); + + int year = 0, + month = 0, + day = 0; + + if (dateMatch.Groups["year"].Success) { + int.TryParse(dateMatch.Groups["year"].Value, out year); + year = culture.DateTimeFormat.Calendar.ToFourDigitYear(year); + } + if (dateMatch.Groups["month"].Success) { + int.TryParse(dateMatch.Groups["month"].Value, out month); + } + if (dateMatch.Groups["day"].Success) { + int.TryParse(dateMatch.Groups["day"].Value, out day); + } + + return new DateParts(year, month, day); + } + + TimeParts IDateFormatter.ParseTime(string timeString, CultureInfo culture) { + var timeFormatString = Regex.Replace(culture.DateTimeFormat.LongTimePattern, @"\.|\$|\^|\{|\[|\(|\||\)|\*|\+|\?|\\", m => String.Format(@"\{0}", m.Value)); + timeFormatString = Regex.Replace(timeFormatString, @"(? String.Format("(.{{{0}}})", m.Value.Replace("\\", "").Length - 2)); + + var timeFormat = ReplaceAll(timeFormatString, GetTimeParseReplacements(culture)); + + if (!Regex.IsMatch(timeString, timeFormat, RegexOptions.IgnoreCase)) + throw new FormatException("Invalid time format."); + + Match timeMatch = Regex.Match(timeString, timeFormat, RegexOptions.IgnoreCase); + + int hour = 0, + minute = 0, + second = 0, + millisecond = 0; + + if (timeMatch.Groups["hour"].Success) { + int.TryParse(timeMatch.Groups["hour"].Value, out hour); + } + if (timeMatch.Groups["minute"].Success) { + int.TryParse(timeMatch.Groups["minute"].Value, out minute); + } + if (timeMatch.Groups["second"].Success) { + int.TryParse(timeMatch.Groups["second"].Value, out second); + } + if (timeMatch.Groups["millisecond"].Success) { + int.TryParse(timeMatch.Groups["millisecond"].Value, out millisecond); + } + + return new TimeParts(hour, minute, second, millisecond); + } + + string IDateFormatter.FormatDateTime(DateTimeParts parts, CultureInfo culture) { + // TODO: Mahsa should implement! + throw new NotImplementedException(); + } + + string IDateFormatter.FormatDate(DateParts parts, CultureInfo culture) { + // TODO: Mahsa should implement! + throw new NotImplementedException(); + } + + string IDateFormatter.FormatTime(TimeParts parts, CultureInfo culture) { + // TODO: Mahsa should implement! + throw new NotImplementedException(); + } + + private Dictionary GetDateParseReplacements(CultureInfo culture) { + return new Dictionary() { + {"dddd", "(?[0-9]{4})"}, + {"ddd", "(?[0-9]{3})"}, + {"dd", "(?[0-9]{2})"}, + {"d", "(?[0-9]{1,2})"}, + {"MMMM", String.Format("(?{0})", String.Join("|", culture.DateTimeFormat.MonthNames))}, + {"MMM", String.Format("(?{0})", String.Join("|", culture.DateTimeFormat.AbbreviatedMonthNames))}, + {"MM", "(?[0-9]{2})"}, + {"M", "(?[0-9]{1,2})"}, + {"yyyyy", "(?[0-9]{5})"}, + {"yyyy", "(?[0-9]{4})"}, + {"yyy", "(?[0-9]{3})"}, + {"yy", "(?[0-9]{2})"}, + {"y", "(?[0-9]{1})"} + }; + } + + private Dictionary GetTimeParseReplacements(CultureInfo culture) { + return new Dictionary() { + {"HH", "(?[0-9]{2})"}, + {"H", "(?[0-9]{1,2})"}, + {"hh", "(?[0-9]{2})"}, + {"h", "(?[0-9]{1,2})"}, + {"MM", "(?[0-9]{2})"}, + {"M", "(?[0-9]{1,2})"}, + {"mm", "(?[0-9]{2})"}, + {"m", "(?[0-9]{1,2})"}, + {"ss", "(?[0-9]{2})"}, + {"s", "(?[0-9]{1,2})"}, + {"f", "(?[0-9]{1})"}, + {"ff", "(?[0-9]{2})"}, + {"fff", "(?[0-9]{3})"}, + {"ffff", "(?[0-9]{4})"}, + {"fffff", "(?[0-9]{5})"}, + {"ffffff", "(?[0-9]{6})"}, + {"tt", String.Format("\\s*(?{0}|{1})\\s*", culture.DateTimeFormat.AMDesignator, culture.DateTimeFormat.PMDesignator)}, + {"t", String.Format("\\s*(?{0}|{1})\\s*", culture.DateTimeFormat.AMDesignator, culture.DateTimeFormat.PMDesignator)}, + {" tt", String.Format("\\s*(?{0}|{1})\\s*", culture.DateTimeFormat.AMDesignator, culture.DateTimeFormat.PMDesignator)}, + {" t", String.Format("\\s*(?{0}|{1})\\s*", culture.DateTimeFormat.AMDesignator, culture.DateTimeFormat.PMDesignator)} + }; + } + + private string ReplaceAll(string original, IDictionary replacements) { + var pattern = String.Format("{0}", String.Join("|", replacements.Keys.ToArray())); + return Regex.Replace(original, pattern, (match) => replacements[match.Value]); + } + } +} diff --git a/src/Orchard/Localization/Services/IDateFormatter.cs b/src/Orchard/Localization/Services/IDateFormatter.cs new file mode 100644 index 000000000..555b3915d --- /dev/null +++ b/src/Orchard/Localization/Services/IDateFormatter.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using Orchard.Framework.Localization.Models; + +namespace Orchard.Framework.Localization.Services { + public interface IDateFormatter { + DateTimeParts ParseDateTime(string dateTimeString, CultureInfo culture); + DateParts ParseDate(string dateString, CultureInfo culture); + TimeParts ParseTime(string timeString, CultureInfo culture); + string FormatDateTime(DateTimeParts parts, CultureInfo culture); + string FormatDate(DateParts parts, CultureInfo culture); + string FormatTime(TimeParts parts, CultureInfo culture); + } +} diff --git a/src/Orchard/Orchard.Framework.csproj b/src/Orchard/Orchard.Framework.csproj index a1c48c56b..ac911703a 100644 --- a/src/Orchard/Orchard.Framework.csproj +++ b/src/Orchard/Orchard.Framework.csproj @@ -261,8 +261,12 @@ + + + + @@ -270,6 +274,7 @@ +