From 176f5820d9460c9f185984284d8208ceaad3d6b7 Mon Sep 17 00:00:00 2001 From: Jeff Hansen Date: Thu, 3 Dec 2020 20:56:57 -0500 Subject: [PATCH 01/98] Target Standard1.2 and 2.0, no more FX4.5 Update lang version to 9.0 and enable null refs Add VariableMustExist getter to IFormatter (#20) --- .gitignore | 1 + src/.idea/.idea.MessageFormat/.idea/.name | 1 + .../.idea/codeStyles/codeStyleConfig.xml | 5 + .../.idea/contentModel.xml | 109 ++++++++++++++++++ .../.idea.MessageFormat/.idea/encodings.xml | 4 + .../.idea.MessageFormat/.idea/indexLayout.xml | 8 ++ .../.idea.MessageFormat/.idea/modules.xml | 8 ++ .../.idea/projectSettingsUpdater.xml | 6 + src/.idea/.idea.MessageFormat/.idea/vcs.xml | 6 + src/.idea/.idea.MessageFormat/riderModule.iml | 10 ++ .../Formatting/BaseFormatterTests.cs | 12 +- .../Formatters/SelectFormatterTests.cs | 4 +- .../Formatters/VariableFormatterTests.cs | 4 +- .../Helpers/ObjectHelperTests.cs | 2 +- .../Jeffijoe.MessageFormat.Tests.csproj | 4 +- .../MessageFormatterCachingTests.cs | 4 +- .../MessageFormatterFullIntegrationTests.cs | 102 ++++++++-------- .../MessageFormatterTests.cs | 43 ++++++- .../MessageFormatterUsingRealParserTests.cs | 2 +- .../Formatting/BaseFormatter.cs | 12 ++ .../Formatting/FormatterRequest.cs | 6 +- .../Formatting/Formatters/PluralFormatter.cs | 17 ++- .../Formatting/Formatters/SelectFormatter.cs | 17 ++- .../Formatters/VariableFormatter.cs | 15 ++- .../Formatting/IFormatter.cs | 14 ++- .../Formatting/IFormatterLibrary.cs | 2 +- .../Helpers/ObjectHelper.cs | 4 +- .../IMessageFormatter.cs | 2 +- .../Jeffijoe.MessageFormat.csproj | 4 +- .../MessageFormatter.cs | 40 ++----- .../Parsing/LiteralParser.cs | 9 +- .../Parsing/MalformedLiteralException.cs | 7 +- .../Parsing/PatternParser.cs | 33 ++++-- .../Properties/AssemblyInfo.cs | 6 +- 34 files changed, 380 insertions(+), 143 deletions(-) create mode 100644 src/.idea/.idea.MessageFormat/.idea/.name create mode 100644 src/.idea/.idea.MessageFormat/.idea/codeStyles/codeStyleConfig.xml create mode 100644 src/.idea/.idea.MessageFormat/.idea/contentModel.xml create mode 100644 src/.idea/.idea.MessageFormat/.idea/encodings.xml create mode 100644 src/.idea/.idea.MessageFormat/.idea/indexLayout.xml create mode 100644 src/.idea/.idea.MessageFormat/.idea/modules.xml create mode 100644 src/.idea/.idea.MessageFormat/.idea/projectSettingsUpdater.xml create mode 100644 src/.idea/.idea.MessageFormat/.idea/vcs.xml create mode 100644 src/.idea/.idea.MessageFormat/riderModule.iml diff --git a/.gitignore b/.gitignore index 9df2302..6e012ea 100644 --- a/.gitignore +++ b/.gitignore @@ -27,3 +27,4 @@ obj bin imagecache /src/.vs +src/.idea/.idea.MessageFormat/.idea/workspace.xml diff --git a/src/.idea/.idea.MessageFormat/.idea/.name b/src/.idea/.idea.MessageFormat/.idea/.name new file mode 100644 index 0000000..662f195 --- /dev/null +++ b/src/.idea/.idea.MessageFormat/.idea/.name @@ -0,0 +1 @@ +MessageFormat \ No newline at end of file diff --git a/src/.idea/.idea.MessageFormat/.idea/codeStyles/codeStyleConfig.xml b/src/.idea/.idea.MessageFormat/.idea/codeStyles/codeStyleConfig.xml new file mode 100644 index 0000000..a55e7a1 --- /dev/null +++ b/src/.idea/.idea.MessageFormat/.idea/codeStyles/codeStyleConfig.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/src/.idea/.idea.MessageFormat/.idea/contentModel.xml b/src/.idea/.idea.MessageFormat/.idea/contentModel.xml new file mode 100644 index 0000000..6674849 --- /dev/null +++ b/src/.idea/.idea.MessageFormat/.idea/contentModel.xml @@ -0,0 +1,109 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/.idea/.idea.MessageFormat/.idea/encodings.xml b/src/.idea/.idea.MessageFormat/.idea/encodings.xml new file mode 100644 index 0000000..df87cf9 --- /dev/null +++ b/src/.idea/.idea.MessageFormat/.idea/encodings.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/.idea/.idea.MessageFormat/.idea/indexLayout.xml b/src/.idea/.idea.MessageFormat/.idea/indexLayout.xml new file mode 100644 index 0000000..27ba142 --- /dev/null +++ b/src/.idea/.idea.MessageFormat/.idea/indexLayout.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/src/.idea/.idea.MessageFormat/.idea/modules.xml b/src/.idea/.idea.MessageFormat/.idea/modules.xml new file mode 100644 index 0000000..bdeedf2 --- /dev/null +++ b/src/.idea/.idea.MessageFormat/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/src/.idea/.idea.MessageFormat/.idea/projectSettingsUpdater.xml b/src/.idea/.idea.MessageFormat/.idea/projectSettingsUpdater.xml new file mode 100644 index 0000000..4bb9f4d --- /dev/null +++ b/src/.idea/.idea.MessageFormat/.idea/projectSettingsUpdater.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/src/.idea/.idea.MessageFormat/.idea/vcs.xml b/src/.idea/.idea.MessageFormat/.idea/vcs.xml new file mode 100644 index 0000000..6c0b863 --- /dev/null +++ b/src/.idea/.idea.MessageFormat/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/.idea/.idea.MessageFormat/riderModule.iml b/src/.idea/.idea.MessageFormat/riderModule.iml new file mode 100644 index 0000000..a59a05c --- /dev/null +++ b/src/.idea/.idea.MessageFormat/riderModule.iml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/src/Jeffijoe.MessageFormat.Tests/Formatting/BaseFormatterTests.cs b/src/Jeffijoe.MessageFormat.Tests/Formatting/BaseFormatterTests.cs index e62c0dd..39121d0 100644 --- a/src/Jeffijoe.MessageFormat.Tests/Formatting/BaseFormatterTests.cs +++ b/src/Jeffijoe.MessageFormat.Tests/Formatting/BaseFormatterTests.cs @@ -145,7 +145,7 @@ public void ParseArguments( string[] blocks) { var subject = new BaseFormatterImpl(); - var req = new FormatterRequest(new Literal(1, 1, 1, 1, new StringBuilder()), null, null, args); + var req = new FormatterRequest(new Literal(1, 1, 1, 1, new StringBuilder()), string.Empty, null, args); var actual = subject.ParseArguments(req); Assert.Equal(extensionKeys.Length, actual.Extensions.Count()); @@ -183,7 +183,7 @@ public void ParseArguments( public void ParseArguments_invalid(string args) { var subject = new BaseFormatterImpl(); - var req = new FormatterRequest(new Literal(1, 1, 1, 1, new StringBuilder()), null, null, args); + var req = new FormatterRequest(new Literal(1, 1, 1, 1, new StringBuilder()), string.Empty, null, args); var ex = Assert.Throws(() => subject.ParseArguments(req)); this.outputHelper.WriteLine(ex.Message); } @@ -210,7 +210,7 @@ public void ParseExtensions(string args, string extension, string value, int exp { var subject = new BaseFormatterImpl(); int index; - var req = new FormatterRequest(new Literal(1, 1, 1, 1, new StringBuilder()), null, null, args); + var req = new FormatterRequest(new Literal(1, 1, 1, 1, new StringBuilder()), string.Empty, null, args); // Warmup subject.ParseExtensions(req, out index); @@ -242,7 +242,7 @@ public void ParseExtensions_multiple() var args = " offset:2 code:js "; var expectedIndex = 17; - var req = new FormatterRequest(new Literal(1, 1, 1, 1, new StringBuilder()), null, null, args); + var req = new FormatterRequest(new Literal(1, 1, 1, 1, new StringBuilder()), string.Empty, null, args); var actual = subject.ParseExtensions(req, out index); Assert.NotEmpty(actual); @@ -274,7 +274,7 @@ public void ParseExtensions_multiple() public void ParseKeyedBlocks(string args, string[] keys, string[] values) { var subject = new BaseFormatterImpl(); - var req = new FormatterRequest(new Literal(1, 1, 1, 1, new StringBuilder()), null, null, args); + var req = new FormatterRequest(new Literal(1, 1, 1, 1, new StringBuilder()), string.Empty, null, args); // Warm-up subject.ParseKeyedBlocks(req, 0); @@ -316,7 +316,7 @@ public void ParseKeyedBlocks(string args, string[] keys, string[] values) public void ParseKeyedBlocks_unclosed_escape_sequence(string args) { var subject = new BaseFormatterImpl(); - var req = new FormatterRequest(new Literal(1, 1, 1, 1, new StringBuilder()), null, null, args); + var req = new FormatterRequest(new Literal(1, 1, 1, 1, new StringBuilder()), string.Empty, null, args); Assert.Throws(() => subject.ParseKeyedBlocks(req, 0)); } diff --git a/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/SelectFormatterTests.cs b/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/SelectFormatterTests.cs index 88061b1..7472463 100644 --- a/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/SelectFormatterTests.cs +++ b/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/SelectFormatterTests.cs @@ -59,14 +59,14 @@ public void Format(string formatterArgs, string gender, string expectedBlock) { var subject = new SelectFormatter(); var messageFormatterMock = new Mock(); - messageFormatterMock.Setup(x => x.FormatMessage(It.IsAny(), It.IsAny>())) + messageFormatterMock.Setup(x => x.FormatMessage(It.IsAny(), It.IsAny>())) .Returns((string input, Dictionary a) => input); var req = new FormatterRequest( new Literal(1, 1, 1, 1, new StringBuilder()), "gender", "select", formatterArgs); - var args = new Dictionary { { "gender", gender } }; + var args = new Dictionary { { "gender", gender } }; var result = subject.Format("en", req, args, gender, messageFormatterMock.Object); Assert.Equal(expectedBlock, result); } diff --git a/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/VariableFormatterTests.cs b/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/VariableFormatterTests.cs index 584df63..121ece2 100644 --- a/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/VariableFormatterTests.cs +++ b/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/VariableFormatterTests.cs @@ -58,7 +58,7 @@ public VariableFormatterTests() public void VerifyAnEmptyStringIsReturnedWhenTheArgumentIsNull() { var req = CreateRequest(); - var args = new Dictionary(); + var args = new Dictionary(); Assert.Equal(string.Empty, this.subject.Format("en", req, args, null, this.formatterMock.Object)); } @@ -70,7 +70,7 @@ public void VerifyAnEmptyStringIsReturnedWhenTheArgumentIsNull() public void VerifyTheValueFromTheGivenArgumentsIsReturnedAsAString() { var req = CreateRequest(); - var args = new Dictionary(); + var args = new Dictionary(); Assert.Equal("is good", this.subject.Format("en", req, args, "is good", this.formatterMock.Object)); } diff --git a/src/Jeffijoe.MessageFormat.Tests/Helpers/ObjectHelperTests.cs b/src/Jeffijoe.MessageFormat.Tests/Helpers/ObjectHelperTests.cs index 1a32ba6..21b95a6 100644 --- a/src/Jeffijoe.MessageFormat.Tests/Helpers/ObjectHelperTests.cs +++ b/src/Jeffijoe.MessageFormat.Tests/Helpers/ObjectHelperTests.cs @@ -106,7 +106,7 @@ private class Base /// /// Gets or sets the prop 1. /// - public string Prop1 { get; set; } + public string? Prop1 { get; set; } #endregion } diff --git a/src/Jeffijoe.MessageFormat.Tests/Jeffijoe.MessageFormat.Tests.csproj b/src/Jeffijoe.MessageFormat.Tests/Jeffijoe.MessageFormat.Tests.csproj index 5368cf0..c522346 100644 --- a/src/Jeffijoe.MessageFormat.Tests/Jeffijoe.MessageFormat.Tests.csproj +++ b/src/Jeffijoe.MessageFormat.Tests/Jeffijoe.MessageFormat.Tests.csproj @@ -1,10 +1,12 @@  - net452 + netcoreapp3.1 True MessageFormat.snk False + 9 + enable diff --git a/src/Jeffijoe.MessageFormat.Tests/MessageFormatterCachingTests.cs b/src/Jeffijoe.MessageFormat.Tests/MessageFormatterCachingTests.cs index 6554720..9a72d70 100644 --- a/src/Jeffijoe.MessageFormat.Tests/MessageFormatterCachingTests.cs +++ b/src/Jeffijoe.MessageFormat.Tests/MessageFormatterCachingTests.cs @@ -120,7 +120,7 @@ private void Benchmark(MessageFormatter subject) + "zero {no messages}" + "one {just one message}" + "=42 {a universal amount of messages}" + "other {uuhm... let's see.. Oh yeah, # messages - and here's a pound: '#'}" + "}!"; int iterations = 100000; - var args = new Dictionary[iterations]; + var args = new Dictionary[iterations]; var rnd = new Random(); for (int i = 0; i < iterations; i++) { @@ -129,7 +129,7 @@ private void Benchmark(MessageFormatter subject) new { gender = val % 2 == 0 ? "male" : "female", - name = val % 2 == 0 ? "Jeff" : "Amanda", + name = val % 2 == 0 ? "Jeff" : "Marcela", messageCount = val }.ToDictionary(); } diff --git a/src/Jeffijoe.MessageFormat.Tests/MessageFormatterFullIntegrationTests.cs b/src/Jeffijoe.MessageFormat.Tests/MessageFormatterFullIntegrationTests.cs index 551f37e..5148af6 100644 --- a/src/Jeffijoe.MessageFormat.Tests/MessageFormatterFullIntegrationTests.cs +++ b/src/Jeffijoe.MessageFormat.Tests/MessageFormatterFullIntegrationTests.cs @@ -53,70 +53,70 @@ public static IEnumerable EscapingTests new object[] { "This '{isn''t}' obvious", - new Dictionary(), + new Dictionary(), "This {isn't} obvious" }; yield return new object[] { "Anna's house has '{0} and # in the roof' and {NUM_COWS} cows.", - new Dictionary { { "NUM_COWS", 5 } }, + new Dictionary { { "NUM_COWS", 5 } }, "Anna's house has {0} and # in the roof and 5 cows." }; yield return new object[] { "Anna's house has '{'0'} and # in the roof' and {NUM_COWS} cows.", - new Dictionary { { "NUM_COWS", 5 } }, + new Dictionary { { "NUM_COWS", 5 } }, "Anna's house has {0} and # in the roof and 5 cows." }; yield return new object[] { "Anna's house has '{0}' and '# in the roof' and {NUM_COWS} cows.", - new Dictionary { { "NUM_COWS", 5 } }, + new Dictionary { { "NUM_COWS", 5 } }, "Anna's house has {0} and # in the roof and 5 cows." }; yield return new object[] { "Anna's house 'has {NUM_COWS} cows'.", - new Dictionary { { "NUM_COWS", 5 } }, + new Dictionary { { "NUM_COWS", 5 } }, "Anna's house 'has 5 cows'." }; yield return new object[] { "Anna''s house a'{''''b'", - new Dictionary(), + new Dictionary(), "Anna's house a{''b" }; yield return new object[] { "a''{NUM_COWS}'b", - new Dictionary { { "NUM_COWS", 5 } }, + new Dictionary { { "NUM_COWS", 5 } }, "a'5'b" }; yield return new object[] { "a'{NUM_COWS}'b'", - new Dictionary { { "NUM_COWS", 5 } }, + new Dictionary { { "NUM_COWS", 5 } }, "a{NUM_COWS}b'" }; yield return new object[] { "These '{'braces'}' and thoses '{braces}' ain''t not escaped, which makes a total of {braces, plural, one {a single pair} other {'#'# (=#) pairs}} of escaped braces.", - new Dictionary { { "braces", 2 } }, + new Dictionary { { "braces", 2 } }, "These {braces} and thoses {braces} ain't not escaped, which makes a total of #2 (=2) pairs of escaped braces." }; yield return new object[] { "{num, plural, =1 {1} other {'#'{num, plural, =1 {1} other {'{'#'#'#'}'}}}}", - new Dictionary { { "num", 2 } }, + new Dictionary { { "num", 2 } }, "#{2#2}" }; } @@ -194,91 +194,91 @@ public static IEnumerable Tests new object[] { Case1, - new Dictionary { { "gender", "male" }, { "name", "Jeff" } }, + new Dictionary { { "gender", "male" }, { "name", "Jeff" } }, "He - {Jeff} - said: You're pretty cool!" }; yield return new object[] { Case2, - new Dictionary { { "gender", "male" }, { "name", "Jeff" }, { "count", 0 } }, + new Dictionary { { "gender", "male" }, { "name", "Jeff" }, { "count", 0 } }, "He - {Jeff} - said: You have no notifications. Have a nice day!" }; yield return new object[] { Case2, - new Dictionary { { "gender", "female" }, { "name", "Amanda" }, { "count", 1 } }, + new Dictionary { { "gender", "female" }, { "name", "Amanda" }, { "count", 1 } }, "She - {Amanda} - said: You have just one notification. Have a nice day!" }; yield return new object[] { Case2, - new Dictionary { { "gender", "uni" }, { "count", 42 } }, + new Dictionary { { "gender", "uni" }, { "count", 42 } }, "They said: You have a universal amount of notifications. Have a nice day!" }; yield return new object[] { Case3, - new Dictionary { { "count", 5 } }, + new Dictionary { { "count", 5 } }, "You have 5 notifications. Have a nice day!" }; yield return new object[] { Case4, - new Dictionary { { "count", 5 }, { "gender", "male" } }, + new Dictionary { { "count", 5 }, { "gender", "male" } }, "He said: You have 5 notifications. Have a nice day!" }; yield return new object[] { Case5, - new Dictionary { { "count", 5 }, { "gender", "male" }, { "genitals", 0 } }, + new Dictionary { { "count", 5 }, { "gender", "male" }, { "genitals", 0 } }, "He (who has no testicles) said: You have 5 notifications. Have a nice day!" }; yield return new object[] { Case5, - new Dictionary { { "count", 5 }, { "gender", "female" }, { "genitals", 0 } }, + new Dictionary { { "count", 5 }, { "gender", "female" }, { "genitals", 0 } }, "She (who has no boobies) said: You have 5 notifications. Have a nice day!" }; yield return new object[] { Case5, - new Dictionary { { "count", 0 }, { "gender", "female" }, { "genitals", 1 } }, + new Dictionary { { "count", 0 }, { "gender", "female" }, { "genitals", 1 } }, "She (who has just one boob) said: You have no notifications. Have a nice day!" }; yield return new object[] { Case5, - new Dictionary { { "count", 0 }, { "gender", "female" }, { "genitals", 2 } }, + new Dictionary { { "count", 0 }, { "gender", "female" }, { "genitals", 2 } }, "She (who has a pair of lovelies) said: You have no notifications. Have a nice day!" }; yield return new object[] { Case5, - new Dictionary { { "count", 0 }, { "gender", "female" }, { "genitals", 102 } }, + new Dictionary { { "count", 0 }, { "gender", "female" }, { "genitals", 102 } }, "She (who has the freakish amount of 102 boobies) said: You have no notifications. Have a nice day!" }; yield return new object[] { Case5, - new Dictionary { { "count", 42 }, { "gender", "female" }, { "genitals", 102 } }, + new Dictionary { { "count", 42 }, { "gender", "female" }, { "genitals", 102 } }, "She (who has the freakish amount of 102 boobies) said: You have a universal amount of notifications. Have a nice day!" }; yield return new object[] { Case5, - new Dictionary { { "count", 1 }, { "gender", "male" }, { "genitals", 102 } }, + new Dictionary { { "count", 1 }, { "gender", "male" }, { "genitals", 102 } }, "He (who has the insane amount of 102 testicles) said: You have just one notification. Have a nice day!" }; @@ -287,7 +287,7 @@ public static IEnumerable Tests new object[] { "{nbrAttachments, plural, zero {} one {{nbrAttachmentsFmt} attachment} other {{nbrAttachmentsFmt} attachments}}", - new Dictionary { { "nbrAttachments", 0 }, { "nbrAttachmentsFmt", "wut" } }, + new Dictionary { { "nbrAttachments", 0 }, { "nbrAttachmentsFmt", "wut" } }, string.Empty }; @@ -296,42 +296,42 @@ public static IEnumerable Tests new object[] { "{maybeCount}", - new Dictionary { { "maybeCount", null } }, + new Dictionary { { "maybeCount", null } }, string.Empty }; yield return new object[] { "{maybeCount}", - new Dictionary { { "maybeCount", (int?)2 } }, + new Dictionary { { "maybeCount", (int?)2 } }, "2" }; yield return new object[] { Case6, - new Dictionary { { "count", 0 } }, + new Dictionary { { "count", 0 } }, "You didn't add this to your profile." }; yield return new object[] { Case6, - new Dictionary { { "count", 1 } }, + new Dictionary { { "count", 1 } }, "You added this to your profile." }; yield return new object[] { Case6, - new Dictionary { { "count", 2 } }, + new Dictionary { { "count", 2 } }, "You and one other person added this to their profile." }; yield return new object[] { Case6, - new Dictionary { { "count", 3 } }, + new Dictionary { { "count", 3 } }, "You and 2 others added this to their profiles." }; } @@ -355,7 +355,7 @@ public static IEnumerable Tests /// [Theory] [MemberData(nameof(Tests))] - public void FormatMessage(string source, Dictionary args, string expected) + public void FormatMessage(string source, Dictionary args, string expected) { var subject = new MessageFormatter(false); @@ -373,7 +373,7 @@ public void FormatMessage(string source, Dictionary args, string /// [Theory] [MemberData(nameof(EscapingTests))] - public void FormatMessage_escaping(string source, Dictionary args, string expected) + public void FormatMessage_escaping(string source, Dictionary args, string expected) { var subject = new MessageFormatter(false); @@ -398,7 +398,7 @@ public void FormatMessage_debug() other {# notifications} }. Have a nice day!"; const string Expected = "He said: You have 5 notifications. Have a nice day!"; - var args = new Dictionary { { "gender", "male" }, { "count", 5 } }; + var args = new Dictionary { { "gender", "male" }, { "count", 5 } }; var subject = new MessageFormatter(false); string result = subject.FormatMessage(Source, args); @@ -413,7 +413,7 @@ public void FormatMessage_lets_non_ascii_characters_right_through() { const string Input = "中test中国话不用彁字。"; var subject = new MessageFormatter(false); - var actual = subject.FormatMessage(Input, new Dictionary()); + var actual = subject.FormatMessage(Input, new Dictionary()); Assert.Equal(Input, actual); } @@ -476,7 +476,7 @@ public void ReadMe_test_to_make_sure_I_dont_look_like_a_fool() }. Have a nice day, {name}!"; var formatted = mf.FormatMessage( Str, - new Dictionary { { "notifications", 4 }, { "name", "Jeff" } }); + new Dictionary { { "notifications", 4 }, { "name", "Jeff" } }); Assert.Equal("You have 4 notifications. Have a nice day, Jeff!", formatted); } @@ -488,16 +488,16 @@ public void ReadMe_test_to_make_sure_I_dont_look_like_a_fool() one{and one other person added this to their profile} other{and # others added this to their profiles} }."; - var formatted = mf.FormatMessage(Str, new Dictionary { { "NUM_ADDS", 0 } }); + var formatted = mf.FormatMessage(Str, new Dictionary { { "NUM_ADDS", 0 } }); Assert.Equal("You didnt add this to your profile.", formatted); - formatted = mf.FormatMessage(Str, new Dictionary { { "NUM_ADDS", 1 } }); + formatted = mf.FormatMessage(Str, new Dictionary { { "NUM_ADDS", 1 } }); Assert.Equal("You added this to your profile.", formatted); - formatted = mf.FormatMessage(Str, new Dictionary { { "NUM_ADDS", 2 } }); + formatted = mf.FormatMessage(Str, new Dictionary { { "NUM_ADDS", 2 } }); Assert.Equal("You and one other person added this to their profile.", formatted); - formatted = mf.FormatMessage(Str, new Dictionary { { "NUM_ADDS", 3 } }); + formatted = mf.FormatMessage(Str, new Dictionary { { "NUM_ADDS", 3 } }); Assert.Equal("You and 2 others added this to their profiles.", formatted); } @@ -516,7 +516,7 @@ public void ReadMe_test_to_make_sure_I_dont_look_like_a_fool() }."; var formatted = mf.FormatMessage( Str, - new Dictionary + new Dictionary { { "GENDER", "male" }, { "NUM_RESULTS", 1 }, @@ -526,7 +526,7 @@ public void ReadMe_test_to_make_sure_I_dont_look_like_a_fool() formatted = mf.FormatMessage( Str, - new Dictionary + new Dictionary { { "GENDER", "male" }, { "NUM_RESULTS", 1 }, @@ -536,7 +536,7 @@ public void ReadMe_test_to_make_sure_I_dont_look_like_a_fool() formatted = mf.FormatMessage( Str, - new Dictionary + new Dictionary { { "GENDER", "female" }, { "NUM_RESULTS", 2 }, @@ -548,10 +548,10 @@ public void ReadMe_test_to_make_sure_I_dont_look_like_a_fool() { var mf = new MessageFormatter(false); const string Str = @"Your {NUM, plural, one{message} other{messages}} go here."; - var formatted = mf.FormatMessage(Str, new Dictionary { { "NUM", 1 } }); + var formatted = mf.FormatMessage(Str, new Dictionary { { "NUM", 1 } }); Assert.Equal("Your message go here.", formatted); - formatted = mf.FormatMessage(Str, new Dictionary { { "NUM", 3 } }); + formatted = mf.FormatMessage(Str, new Dictionary { { "NUM", 3 } }); Assert.Equal("Your messages go here.", formatted); } @@ -560,29 +560,29 @@ public void ReadMe_test_to_make_sure_I_dont_look_like_a_fool() const string Str = @"His name is {LAST_NAME}... {FIRST_NAME} {LAST_NAME}"; var formatted = mf.FormatMessage( Str, - new Dictionary { { "FIRST_NAME", "James" }, { "LAST_NAME", "Bond" } }); + new Dictionary { { "FIRST_NAME", "James" }, { "LAST_NAME", "Bond" } }); Assert.Equal("His name is Bond... James Bond", formatted); } { var mf = new MessageFormatter(false); const string Str = @"{GENDER, select, male{He} female{She} other{They}} liked this."; - var formatted = mf.FormatMessage(Str, new Dictionary { { "GENDER", "male" } }); + var formatted = mf.FormatMessage(Str, new Dictionary { { "GENDER", "male" } }); Assert.Equal("He liked this.", formatted); - formatted = mf.FormatMessage(Str, new Dictionary { { "GENDER", "female" } }); + formatted = mf.FormatMessage(Str, new Dictionary { { "GENDER", "female" } }); Assert.Equal("She liked this.", formatted); - formatted = mf.FormatMessage(Str, new Dictionary { { "GENDER", "somethingelse" } }); + formatted = mf.FormatMessage(Str, new Dictionary { { "GENDER", "somethingelse" } }); Assert.Equal("They liked this.", formatted); - formatted = mf.FormatMessage(Str, new Dictionary { { "GENDER", null } }); + formatted = mf.FormatMessage(Str, new Dictionary { { "GENDER", null } }); Assert.Equal("They liked this.", formatted); } { var mf = new MessageFormatter(true, "en"); - mf.Pluralizers["en"] = n => { + mf.Pluralizers!["en"] = n => { // ´n´ is the number being pluralized. // ReSharper disable once CompareOfFloatsByEqualityOperator if (n == 0) @@ -606,7 +606,7 @@ public void ReadMe_test_to_make_sure_I_dont_look_like_a_fool() var actual = mf.FormatMessage( "You have {number, plural, thatsalot {a shitload of notifications} other {# notifications}}", - new Dictionary { { "number", 1001 } }); + new Dictionary { { "number", 1001 } }); Assert.Equal("You have a shitload of notifications", actual); } } diff --git a/src/Jeffijoe.MessageFormat.Tests/MessageFormatterTests.cs b/src/Jeffijoe.MessageFormat.Tests/MessageFormatterTests.cs index 594e2b1..5ab43e0 100644 --- a/src/Jeffijoe.MessageFormat.Tests/MessageFormatterTests.cs +++ b/src/Jeffijoe.MessageFormat.Tests/MessageFormatterTests.cs @@ -83,7 +83,7 @@ public void FormatMessage() { const string Pattern = "{name} has {messages, plural, 123}."; const string Expected = "Jeff has 123 messages."; - var args = new Dictionary { { "name", "Jeff" }, { "messages", 1 } }; + var args = new Dictionary { { "name", "Jeff" }, { "messages", 1 } }; var requests = new[] { new FormatterRequest( @@ -139,15 +139,15 @@ public void UnescapeLiterals(string source, string expected) } /// - /// Verifies that format message throws when variables are missing. + /// Verifies that format message throws when variables are missing and the formatter requires it to exist. /// [Fact] - public void VerifyFormatMessageThrowsWhenVariablesAreMissing() + public void VerifyFormatMessageThrowsWhenVariablesAreMissingAndTheFormatterRequiresItToExist() { const string Pattern = "{name} has {messages, plural, 123}."; // Note the missing "name" variable. - var args = new Dictionary { { "messages", 1 } }; + var args = new Dictionary { { "messages", 1 } }; var requests = new[] { new FormatterRequest( @@ -164,7 +164,9 @@ public void VerifyFormatMessageThrowsWhenVariablesAreMissing() this.collectionMock.Setup(x => x.GetEnumerator()).Returns(() => requests.AsEnumerable().GetEnumerator()); this.patternParserMock.Setup(x => x.Parse(It.IsAny())).Returns(this.collectionMock.Object); - + this.formatterMock1.SetupGet(x => x.VariableMustExist).Returns(true); + this.libraryMock.Setup(x => x.GetFormatter(It.IsAny())).Returns(formatterMock1.Object); + // First request, and "name" is 4 chars. this.collectionMock.Setup(x => x.ShiftIndices(0, 4)).Callback( @@ -174,6 +176,37 @@ public void VerifyFormatMessageThrowsWhenVariablesAreMissing() var ex = Assert.Throws(() => this.subject.FormatMessage(Pattern, args)); Assert.Equal("name", ex.MissingVariable); } + + /// + /// Verifies that format message allows non-existent variables when formatter allows it. + /// + [Fact] + public void VerifyFormatMessageAllowsNonExistentVariablesWhenFormatterAllowsIt() + { + const string Pattern = "{name}"; + + // Note the missing "name" variable. + var args = new Dictionary (); + var requests = new[] + { + new FormatterRequest( + new Literal(0, 5, 1, 7, new StringBuilder("name")), + "name", + null, + null), + }; + + this.collectionMock.Setup(x => x.GetEnumerator()).Returns(() => requests.AsEnumerable().GetEnumerator()); + this.patternParserMock.Setup(x => x.Parse(It.IsAny())).Returns(this.collectionMock.Object); + this.libraryMock.Setup(x => x.GetFormatter(It.IsAny())).Returns(formatterMock2.Object); + this.formatterMock2.SetupGet(x => x.VariableMustExist).Returns(false); + this.formatterMock2.Setup(x => x.Format(It.IsAny(), It.IsAny(), + It.IsAny>(), null, It.IsAny())).Returns("formatted"); + + Assert.Equal("formatted",subject.FormatMessage(Pattern, args)); + + this.formatterMock2.VerifyAll(); + } #endregion } diff --git a/src/Jeffijoe.MessageFormat.Tests/MessageFormatterUsingRealParserTests.cs b/src/Jeffijoe.MessageFormat.Tests/MessageFormatterUsingRealParserTests.cs index d5ca35a..a9c5e0d 100644 --- a/src/Jeffijoe.MessageFormat.Tests/MessageFormatterUsingRealParserTests.cs +++ b/src/Jeffijoe.MessageFormat.Tests/MessageFormatterUsingRealParserTests.cs @@ -80,7 +80,7 @@ public void FormatMessage_using_real_parser_and_library_mock(string source, stri mockLibary.Object, false); - var args = new Dictionary(); + var args = new Dictionary(); args.Add("name", "Jeff"); dummyFormatter.Setup(x => x.Format("en", It.IsAny(), args, "Jeff", subject)) .Returns("Jeff"); diff --git a/src/Jeffijoe.MessageFormat/Formatting/BaseFormatter.cs b/src/Jeffijoe.MessageFormat/Formatting/BaseFormatter.cs index 697c43f..e1f97ea 100644 --- a/src/Jeffijoe.MessageFormat/Formatting/BaseFormatter.cs +++ b/src/Jeffijoe.MessageFormat/Formatting/BaseFormatter.cs @@ -4,6 +4,7 @@ // Copyright (C) Jeff Hansen 2014. All rights reserved. using System.Collections.Generic; +using System.Linq; using System.Text; using Jeffijoe.MessageFormat.Parsing; @@ -52,6 +53,12 @@ protected internal ParsedArguments ParseArguments(FormatterRequest request) protected internal IEnumerable ParseExtensions(FormatterRequest request, out int index) { var result = new List(); + if (request.FormatterArguments == null) + { + index = -1; + return Enumerable.Empty(); + } + int length = request.FormatterArguments.Length; index = 0; @@ -130,6 +137,11 @@ protected internal IEnumerable ParseKeyedBlocks(FormatterRequest req var braceBalance = 0; var foundWhitespaceAfterKey = false; var insideEscapeSequence = false; + if (request.FormatterArguments == null) + { + return Enumerable.Empty(); + } + for (int i = startIndex; i < request.FormatterArguments.Length; i++) { var c = request.FormatterArguments[i]; diff --git a/src/Jeffijoe.MessageFormat/Formatting/FormatterRequest.cs b/src/Jeffijoe.MessageFormat/Formatting/FormatterRequest.cs index 38c947d..071dc4e 100644 --- a/src/Jeffijoe.MessageFormat/Formatting/FormatterRequest.cs +++ b/src/Jeffijoe.MessageFormat/Formatting/FormatterRequest.cs @@ -29,7 +29,7 @@ public class FormatterRequest /// /// The formatter arguments. /// - public FormatterRequest(Literal sourceLiteral, string variable, string formatterName, string formatterArguments) + public FormatterRequest(Literal sourceLiteral, string variable, string? formatterName, string? formatterArguments) { this.SourceLiteral = sourceLiteral; this.Variable = variable; @@ -47,7 +47,7 @@ public FormatterRequest(Literal sourceLiteral, string variable, string formatter /// /// The formatter arguments. /// - public string FormatterArguments { get; private set; } + public string? FormatterArguments { get; private set; } /// /// Gets the name of the formatter to use . e.g. 'select', 'plural'. Can be null. @@ -55,7 +55,7 @@ public FormatterRequest(Literal sourceLiteral, string variable, string formatter /// /// The name of the formatter. /// - public string FormatterName { get; private set; } + public string? FormatterName { get; private set; } /// /// Gets the source literal. diff --git a/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralFormatter.cs b/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralFormatter.cs index e69ffc5..eac0e47 100644 --- a/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralFormatter.cs +++ b/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralFormatter.cs @@ -31,6 +31,12 @@ public PluralFormatter() #region Public Properties + /// + /// This formatter requires the input variable to exist. + /// + public bool VariableMustExist => true; + + /// /// Gets the pluralizers dictionary. Key is the locale. /// @@ -82,8 +88,8 @@ public bool CanFormat(FormatterRequest request) public string Format( string locale, FormatterRequest request, - IDictionary args, - object value, + IDictionary args, + object? value, IMessageFormatter messageFormatter) { var arguments = this.ParseArguments(request); @@ -137,7 +143,7 @@ internal string Pluralize(string locale, ParsedArguments arguments, double n, do } var pluralForm = pluralizer(n - offset); - KeyedBlock other = null; + KeyedBlock? other = null; foreach (var keyedBlock in arguments.KeyedBlocks) { if (keyedBlock.Key == OtherKey) @@ -207,6 +213,7 @@ internal string ReplaceNumberLiterals(StringBuilder pluralized, double n) { insideEscapeSequence = false; } + continue; } @@ -229,7 +236,6 @@ internal string ReplaceNumberLiterals(StringBuilder pluralized, double n) sb.Append(nextChar); insideEscapeSequence = true; ++i; - continue; } continue; @@ -271,7 +277,8 @@ private void AddStandardPluralizers() { this.Pluralizers.Add( "en", - n => { + n => + { // ReSharper disable CompareOfFloatsByEqualityOperator if (n == 0) { diff --git a/src/Jeffijoe.MessageFormat/Formatting/Formatters/SelectFormatter.cs b/src/Jeffijoe.MessageFormat/Formatting/Formatters/SelectFormatter.cs index 4074439..5f0c44d 100644 --- a/src/Jeffijoe.MessageFormat/Formatting/Formatters/SelectFormatter.cs +++ b/src/Jeffijoe.MessageFormat/Formatting/Formatters/SelectFormatter.cs @@ -14,7 +14,18 @@ namespace Jeffijoe.MessageFormat.Formatting.Formatters /// public class SelectFormatter : BaseFormatter, IFormatter { + #region Public Properties + + /// + /// This formatter requires the input variable to exist. + /// + public bool VariableMustExist => true; + + #endregion + + #region Public Methods and Operators + /// /// Determines whether this instance can format a message based on the specified parameters. @@ -50,12 +61,12 @@ public bool CanFormat(FormatterRequest request) public string Format( string locale, FormatterRequest request, - IDictionary args, - object value, + IDictionary args, + object? value, IMessageFormatter messageFormatter) { var parsed = this.ParseArguments(request); - KeyedBlock other = null; + KeyedBlock? other = null; foreach (var keyedBlock in parsed.KeyedBlocks) { var str = Convert.ToString(value); diff --git a/src/Jeffijoe.MessageFormat/Formatting/Formatters/VariableFormatter.cs b/src/Jeffijoe.MessageFormat/Formatting/Formatters/VariableFormatter.cs index 37504e5..02e3b35 100644 --- a/src/Jeffijoe.MessageFormat/Formatting/Formatters/VariableFormatter.cs +++ b/src/Jeffijoe.MessageFormat/Formatting/Formatters/VariableFormatter.cs @@ -17,10 +17,19 @@ public class VariableFormatter : IFormatter { #region Fields - private ConcurrentDictionary cultures = new ConcurrentDictionary(); + private readonly ConcurrentDictionary cultures = new ConcurrentDictionary(); #endregion + #region Public Properties + + /// + /// This formatter requires the input variable to exist. + /// + public bool VariableMustExist => true; + + #endregion + #region Public Methods and Operators /// @@ -54,8 +63,8 @@ public bool CanFormat(FormatterRequest request) public string Format( string locale, FormatterRequest request, - IDictionary args, - object value, + IDictionary args, + object? value, IMessageFormatter messageFormatter) { switch (value) diff --git a/src/Jeffijoe.MessageFormat/Formatting/IFormatter.cs b/src/Jeffijoe.MessageFormat/Formatting/IFormatter.cs index e5bae52..49b0d85 100644 --- a/src/Jeffijoe.MessageFormat/Formatting/IFormatter.cs +++ b/src/Jeffijoe.MessageFormat/Formatting/IFormatter.cs @@ -12,6 +12,16 @@ namespace Jeffijoe.MessageFormat.Formatting /// public interface IFormatter { + #region Public Properties + + /// + /// Each Formatter must declare whether or not an input variable is required to exist. + /// Most of the time that is the case. + /// + bool VariableMustExist { get; } + + #endregion + #region Public Methods and Operators /// @@ -42,8 +52,8 @@ public interface IFormatter string Format( string locale, FormatterRequest request, - IDictionary args, - object value, + IDictionary args, + object? value, IMessageFormatter messageFormatter); #endregion diff --git a/src/Jeffijoe.MessageFormat/Formatting/IFormatterLibrary.cs b/src/Jeffijoe.MessageFormat/Formatting/IFormatterLibrary.cs index 82ee28c..11a4957 100644 --- a/src/Jeffijoe.MessageFormat/Formatting/IFormatterLibrary.cs +++ b/src/Jeffijoe.MessageFormat/Formatting/IFormatterLibrary.cs @@ -23,7 +23,7 @@ public interface IFormatterLibrary : IList /// /// The . /// - IFormatter GetFormatter(FormatterRequest request); + IFormatter? GetFormatter(FormatterRequest request); #endregion } diff --git a/src/Jeffijoe.MessageFormat/Helpers/ObjectHelper.cs b/src/Jeffijoe.MessageFormat/Helpers/ObjectHelper.cs index 55b271c..d09a31c 100644 --- a/src/Jeffijoe.MessageFormat/Helpers/ObjectHelper.cs +++ b/src/Jeffijoe.MessageFormat/Helpers/ObjectHelper.cs @@ -54,12 +54,12 @@ internal static IEnumerable GetProperties(object obj) /// /// The . /// - internal static Dictionary ToDictionary(this object obj) + internal static Dictionary ToDictionary(this object obj) { // We want to be able to read the property, and it should not be an indexer. var properties = GetProperties(obj).Where(x => x.CanRead && x.GetIndexParameters().Any() == false); - var result = new Dictionary(); + var result = new Dictionary(); foreach (var propertyInfo in properties) { result[propertyInfo.Name] = propertyInfo.GetValue(obj); diff --git a/src/Jeffijoe.MessageFormat/IMessageFormatter.cs b/src/Jeffijoe.MessageFormat/IMessageFormatter.cs index 694134e..a3947d1 100644 --- a/src/Jeffijoe.MessageFormat/IMessageFormatter.cs +++ b/src/Jeffijoe.MessageFormat/IMessageFormatter.cs @@ -26,7 +26,7 @@ public interface IMessageFormatter /// /// The . /// - string FormatMessage(string pattern, IDictionary argsMap); + string FormatMessage(string pattern, IDictionary argsMap); /// /// Formats the message, and uses reflection to create a dictionary of property values from the specified object. diff --git a/src/Jeffijoe.MessageFormat/Jeffijoe.MessageFormat.csproj b/src/Jeffijoe.MessageFormat/Jeffijoe.MessageFormat.csproj index 8bea7b9..ae3bb46 100644 --- a/src/Jeffijoe.MessageFormat/Jeffijoe.MessageFormat.csproj +++ b/src/Jeffijoe.MessageFormat/Jeffijoe.MessageFormat.csproj @@ -1,7 +1,6 @@  - netstandard1.2;netstandard1.3;netstandard1.4;netstandard1.5;netstandard1.6;netstandard2.0;net45 True MessageFormat.snk MessageFormat @@ -11,6 +10,9 @@ Follow official guidelines for escaping literals (saithis, #15) messageformat,pcl,messageformatter,xamarin,ui,messages,formatting,plural,pluralization,singular,select,strings,stringformat,format https://github.com/jeffijoe/messageformat.net + netstandard1.2;netstandard2.0 + latest + enable diff --git a/src/Jeffijoe.MessageFormat/MessageFormatter.cs b/src/Jeffijoe.MessageFormat/MessageFormatter.cs index a106250..ed56c26 100644 --- a/src/Jeffijoe.MessageFormat/MessageFormatter.cs +++ b/src/Jeffijoe.MessageFormat/MessageFormatter.cs @@ -41,7 +41,7 @@ public class MessageFormatter : IMessageFormatter /// Pattern cache. If enabled, should speed up formatting the same pattern multiple times, /// regardless of arguments. /// - private readonly ConcurrentDictionary cache; + private readonly ConcurrentDictionary? cache; /// /// The formatter library. @@ -92,18 +92,8 @@ internal MessageFormatter( bool useCache, string locale = "en") { - if (patternParser == null) - { - throw new ArgumentNullException("patternParser"); - } - - if (library == null) - { - throw new ArgumentNullException("library"); - } - - this.patternParser = patternParser; - this.library = library; + this.patternParser = patternParser ?? throw new ArgumentNullException("patternParser"); + this.library = library ?? throw new ArgumentNullException("library"); this.Locale = locale; if (useCache) { @@ -143,17 +133,12 @@ public IFormatterLibrary Formatters /// /// The pluralizers, or null if the plural formatter has not been added. /// - public IDictionary Pluralizers + public IDictionary? Pluralizers { get { var pluralFormatter = this.Formatters.OfType().FirstOrDefault(); - if (pluralFormatter == null) - { - return null; - } - - return pluralFormatter.Pluralizers; + return pluralFormatter?.Pluralizers; } } @@ -178,7 +163,7 @@ public IDictionary Pluralizers /// /// The formatted message. /// - public static string Format(string pattern, IDictionary data) + public static string Format(string pattern, IDictionary data) { lock (Lock) { @@ -222,7 +207,7 @@ public static string Format(string pattern, object data) /// /// The . /// - public string FormatMessage(string pattern, IDictionary args) + public string FormatMessage(string pattern, IDictionary args) { /* * We are asuming the formatters are ordered correctly @@ -243,18 +228,17 @@ public string FormatMessage(string pattern, IDictionary args) { var request = requestsEnumerated[i]; - object value; - if (args.TryGetValue(request.Variable, out value) == false) - { - throw new VariableNotFoundException(request.Variable); - } - var formatter = this.Formatters.GetFormatter(request); if (formatter == null) { throw new FormatterNotFoundException(request); } + if (args.TryGetValue(request.Variable, out var value) == false && formatter.VariableMustExist) + { + throw new VariableNotFoundException(request.Variable); + } + // Double dispatch, yeah! var result = formatter.Format(this.Locale, request, args, value, this); diff --git a/src/Jeffijoe.MessageFormat/Parsing/LiteralParser.cs b/src/Jeffijoe.MessageFormat/Parsing/LiteralParser.cs index 7486b28..689855d 100644 --- a/src/Jeffijoe.MessageFormat/Parsing/LiteralParser.cs +++ b/src/Jeffijoe.MessageFormat/Parsing/LiteralParser.cs @@ -44,19 +44,19 @@ public IEnumerable ParseLiterals(StringBuilder sb) var insideEscapeSequence = false; var currentEscapeSequenceLineNumber = 0; var currentEscapeSequenceColumnNumber = 0; - const char CR = '\r'; // Carriage return - const char LF = '\n'; // Line feed + const char Cr = '\r'; // Carriage return + const char Lf = '\n'; // Line feed for (var i = 0; i < sb.Length; i++) { var c = sb[i]; - if (c == LF) + if (c == Lf) { lineNumber++; columnNumber = 0; continue; } - if (c == CR) + if (c == Cr) { continue; } @@ -101,7 +101,6 @@ public IEnumerable ParseLiterals(StringBuilder sb) currentEscapeSequenceLineNumber = lineNumber; currentEscapeSequenceColumnNumber = columnNumber; ++i; - continue; } continue; diff --git a/src/Jeffijoe.MessageFormat/Parsing/MalformedLiteralException.cs b/src/Jeffijoe.MessageFormat/Parsing/MalformedLiteralException.cs index ce7ff3e..9d1415b 100644 --- a/src/Jeffijoe.MessageFormat/Parsing/MalformedLiteralException.cs +++ b/src/Jeffijoe.MessageFormat/Parsing/MalformedLiteralException.cs @@ -30,11 +30,12 @@ internal MalformedLiteralException( string message, int lineNumber = 0, int columnNumber = 0, - string sourceSnippet = null) + string? sourceSnippet = null) : base(BuildMessage(message, lineNumber, columnNumber, sourceSnippet)) { this.LineNumber = lineNumber; this.ColumnNumber = columnNumber; + this.SourceSnippet = sourceSnippet; } #endregion @@ -63,7 +64,7 @@ internal MalformedLiteralException( /// /// The source snippet. /// - public string SourceSnippet { get; private set; } + public string? SourceSnippet { get; private set; } #endregion @@ -87,7 +88,7 @@ internal MalformedLiteralException( /// /// The . /// - private static string BuildMessage(string message, int lineNumber, int columnNumber, string sourceSnippet) + private static string BuildMessage(string message, int lineNumber, int columnNumber, string? sourceSnippet) { var str = message; if (lineNumber != 0 && columnNumber != 0) diff --git a/src/Jeffijoe.MessageFormat/Parsing/PatternParser.cs b/src/Jeffijoe.MessageFormat/Parsing/PatternParser.cs index f99bbdc..da3101a 100644 --- a/src/Jeffijoe.MessageFormat/Parsing/PatternParser.cs +++ b/src/Jeffijoe.MessageFormat/Parsing/PatternParser.cs @@ -6,7 +6,6 @@ using System; using System.Linq; using System.Text; - using Jeffijoe.MessageFormat.Formatting; using Jeffijoe.MessageFormat.Helpers; @@ -70,13 +69,13 @@ public IFormatterRequestCollection Parse(StringBuilder source) { // The first token to follow an opening brace will be the variable name. int lastIndex; - string variableName = ReadLiteralSection(literal, 0, false, out lastIndex); + string variableName = ReadLiteralSection(literal, 0, false, out lastIndex)!; // The next (if any), is the formatter to use. Null is allowed. - string formatterKey = null; + string? formatterKey = null; // The rest of the string is what we pass into the formatter. Can be null. - string formatterArgs = null; + string? formatterArgs = null; if (variableName.Length != literal.InnerText.Length) { formatterKey = ReadLiteralSection(literal, variableName.Length + 1, true, out lastIndex); @@ -119,7 +118,8 @@ public IFormatterRequestCollection Parse(StringBuilder source) /// /// Parsing the variable key yielded an empty string. /// - internal static string ReadLiteralSection(Literal literal, int offset, bool allowEmptyResult, out int lastIndex) + internal static string? ReadLiteralSection(Literal literal, int offset, bool allowEmptyResult, + out int lastIndex) { const char Comma = ','; var sb = new StringBuilder(); @@ -146,7 +146,8 @@ internal static string ReadLiteralSection(Literal literal, int offset, bool allo var msg = string.Format("Invalid literal character '{0}'.", c); // Line number can't have changed. - throw new MalformedLiteralException(msg, literal.SourceLineNumber, column, innerText.ToString()); + throw new MalformedLiteralException(msg, literal.SourceLineNumber, column, + innerText.ToString()); } } else @@ -168,15 +169,23 @@ internal static string ReadLiteralSection(Literal literal, int offset, bool allo StringBuilder trimmed = sb.TrimWhitespace(); if (trimmed.Length == 0) { - return null; + if (allowEmptyResult) + { + return null; + } + + throw new MalformedLiteralException( + "Parsing the literal yielded a string that was pure whitespace.", + literal.SourceLineNumber, + column); } if (trimmed.ContainsWhitespace()) { throw new MalformedLiteralException( - "Parsed literal must not contain whitespace.", - 0, - 0, + "Parsed literal must not contain whitespace.", + 0, + 0, trimmed.ToString()); } @@ -189,8 +198,8 @@ internal static string ReadLiteralSection(Literal literal, int offset, bool allo } throw new MalformedLiteralException( - "Parsing the literal yielded an empty string.", - literal.SourceLineNumber, + "Parsing the literal yielded an empty string.", + literal.SourceLineNumber, column); } diff --git a/src/Jeffijoe.MessageFormat/Properties/AssemblyInfo.cs b/src/Jeffijoe.MessageFormat/Properties/AssemblyInfo.cs index d3b706a..4b7d381 100644 --- a/src/Jeffijoe.MessageFormat/Properties/AssemblyInfo.cs +++ b/src/Jeffijoe.MessageFormat/Properties/AssemblyInfo.cs @@ -15,7 +15,7 @@ [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("")] [assembly: AssemblyProduct("MessageFormatter for .NET")] -[assembly: AssemblyCopyright("Copyright © Jeff Hansen 2018")] +[assembly: AssemblyCopyright("Copyright © Jeff Hansen 2014 to present")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] [assembly: NeutralResourcesLanguage("en")] @@ -29,5 +29,5 @@ // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("3.0.1")] -[assembly: AssemblyFileVersion("3.0.1")] +[assembly: AssemblyVersion("4.0.0")] +[assembly: AssemblyFileVersion("4.0.0")] From 5a2c6008580211c02628f774984194465699d819 Mon Sep 17 00:00:00 2001 From: Jeff Hansen Date: Thu, 3 Dec 2020 21:10:28 -0500 Subject: [PATCH 02/98] Attempt at build with GH Actions --- .github/workflows/ci.yml | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..0d321f2 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,32 @@ +name: .NET Core + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +env: + DOTNET_NOLOGO: true + DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: Setup .NET Core + uses: actions/setup-dotnet@v1 + with: + dotnet-version: 3.1.x + - name: Install dependencies + run: dotnet restore + working-directory: ./src + - name: Build + run: dotnet build --configuration Release --no-restore + working-directory: ./src + - name: Test + run: dotnet test --no-restore --verbosity normal + working-directory: ./src From 65c67de181125d485764a6c078f52670dfa5bfdf Mon Sep 17 00:00:00 2001 From: Jeff Hansen Date: Thu, 3 Dec 2020 22:03:34 -0500 Subject: [PATCH 03/98] Add MinVer --- .github/workflows/ci.yml | 2 ++ src/Jeffijoe.MessageFormat/Jeffijoe.MessageFormat.csproj | 9 ++++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0d321f2..affbf18 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,6 +17,8 @@ jobs: steps: - uses: actions/checkout@v2 + with: + fetch-depth: 100 - name: Setup .NET Core uses: actions/setup-dotnet@v1 with: diff --git a/src/Jeffijoe.MessageFormat/Jeffijoe.MessageFormat.csproj b/src/Jeffijoe.MessageFormat/Jeffijoe.MessageFormat.csproj index ae3bb46..f2c0918 100644 --- a/src/Jeffijoe.MessageFormat/Jeffijoe.MessageFormat.csproj +++ b/src/Jeffijoe.MessageFormat/Jeffijoe.MessageFormat.csproj @@ -10,13 +10,20 @@ Follow official guidelines for escaping literals (saithis, #15) messageformat,pcl,messageformatter,xamarin,ui,messages,formatting,plural,pluralization,singular,select,strings,stringformat,format https://github.com/jeffijoe/messageformat.net - netstandard1.2;netstandard2.0 latest enable + netstandard1.1 bin\Release\netstandard1.1\Jeffijoe.MessageFormat.xml + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + From cf4e52962e2f7a2c95f45dc46195ec54d788598b Mon Sep 17 00:00:00 2001 From: Jeff Hansen Date: Thu, 3 Dec 2020 22:37:17 -0500 Subject: [PATCH 04/98] Simplify AssemblyInfo --- .../.idea/contentModel.xml | 3 -- .../Properties/AssemblyInfo.cs | 39 ------------------- .../Jeffijoe.MessageFormat.csproj | 2 +- .../Properties/AssemblyInfo.cs | 23 ----------- 4 files changed, 1 insertion(+), 66 deletions(-) delete mode 100644 src/Jeffijoe.MessageFormat.Tests/Properties/AssemblyInfo.cs diff --git a/src/.idea/.idea.MessageFormat/.idea/contentModel.xml b/src/.idea/.idea.MessageFormat/.idea/contentModel.xml index 6674849..2eb6084 100644 --- a/src/.idea/.idea.MessageFormat/.idea/contentModel.xml +++ b/src/.idea/.idea.MessageFormat/.idea/contentModel.xml @@ -95,9 +95,6 @@ - - - diff --git a/src/Jeffijoe.MessageFormat.Tests/Properties/AssemblyInfo.cs b/src/Jeffijoe.MessageFormat.Tests/Properties/AssemblyInfo.cs deleted file mode 100644 index cec1201..0000000 --- a/src/Jeffijoe.MessageFormat.Tests/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,39 +0,0 @@ -// MessageFormat for .NET -// - AssemblyInfo.cs -// -// Author: Jeff Hansen -// Copyright (C) Jeff Hansen 2015. All rights reserved. - -using System.Reflection; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("Jeffijoe.MessageFormat.Tests")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("Jeffijoe.MessageFormat.Tests")] -[assembly: AssemblyCopyright("Copyright © 2014")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("1f34c9fb-42dc-432f-8719-689679fce52b")] - -// Version information for an assembly consists of the following four values: -// Major Version -// Minor Version -// Build Number -// Revision -// You can specify all the values or you can default the Build and Revision Numbers -// by using the '*' as shown below: -// [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] \ No newline at end of file diff --git a/src/Jeffijoe.MessageFormat/Jeffijoe.MessageFormat.csproj b/src/Jeffijoe.MessageFormat/Jeffijoe.MessageFormat.csproj index f2c0918..14a9168 100644 --- a/src/Jeffijoe.MessageFormat/Jeffijoe.MessageFormat.csproj +++ b/src/Jeffijoe.MessageFormat/Jeffijoe.MessageFormat.csproj @@ -8,7 +8,7 @@ false Jeff Hansen Follow official guidelines for escaping literals (saithis, #15) - messageformat,pcl,messageformatter,xamarin,ui,messages,formatting,plural,pluralization,singular,select,strings,stringformat,format + messageformat,messageformatter,xamarin,ui,messages,formatting,plural,pluralization,singular,select,strings,stringformat,format https://github.com/jeffijoe/messageformat.net latest enable diff --git a/src/Jeffijoe.MessageFormat/Properties/AssemblyInfo.cs b/src/Jeffijoe.MessageFormat/Properties/AssemblyInfo.cs index 4b7d381..18ae6d2 100644 --- a/src/Jeffijoe.MessageFormat/Properties/AssemblyInfo.cs +++ b/src/Jeffijoe.MessageFormat/Properties/AssemblyInfo.cs @@ -7,27 +7,4 @@ using System.Resources; using System.Runtime.CompilerServices; -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("Jeffijoe.MessageFormat")] -[assembly: AssemblyDescription("ICU Message Format for .NET.\r\n\r\nCheck the README on Github for more information.")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("MessageFormatter for .NET")] -[assembly: AssemblyCopyright("Copyright © Jeff Hansen 2014 to present")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] -[assembly: NeutralResourcesLanguage("en")] [assembly: InternalsVisibleTo("Jeffijoe.MessageFormat.Tests, PublicKey=00240000048000009400000006020000002400005253413100040000010001004f0dc10ad8873751f986416a7d3134e473ad3454a7138e26cf9760eff341709fc50e69d985b4e0ea512b5628079043a31ce1e402f4eaebffb5772eed9ef3a7a9e39f122633f19529a1e9988005289ed09a1e294abe13152afebc59835c2fef2879d8641b564daa7f511298ec2f8a3e9b322819e05cbaea0bfc73fe100615bfc7")] - -// Version information for an assembly consists of the following four values: -// Major Version -// Minor Version -// Build Number -// Revision -// You can specify all the values or you can default the Build and Revision Numbers -// by using the '*' as shown below: -// [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("4.0.0")] -[assembly: AssemblyFileVersion("4.0.0")] From f5e24b7779381aecba0cb837de7b24da7eac08de Mon Sep 17 00:00:00 2001 From: Jeff Hansen Date: Fri, 4 Dec 2020 09:47:11 -0500 Subject: [PATCH 05/98] More cleanup --- .github/workflows/ci.yml | 4 + .gitignore | 1 + .../.idea/contentModel.xml | 8 +- .../Jeffijoe.MessageFormat.csproj | 7 +- src/MessageFormat.nuspec | 50 ------- src/Settings.StyleCop | 134 ------------------ 6 files changed, 14 insertions(+), 190 deletions(-) delete mode 100644 src/MessageFormat.nuspec delete mode 100644 src/Settings.StyleCop diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index affbf18..d703f94 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,16 +19,20 @@ jobs: - uses: actions/checkout@v2 with: fetch-depth: 100 + - name: Setup .NET Core uses: actions/setup-dotnet@v1 with: dotnet-version: 3.1.x + - name: Install dependencies run: dotnet restore working-directory: ./src + - name: Build run: dotnet build --configuration Release --no-restore working-directory: ./src + - name: Test run: dotnet test --no-restore --verbosity normal working-directory: ./src diff --git a/.gitignore b/.gitignore index 6e012ea..2e5faf7 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,4 @@ bin imagecache /src/.vs src/.idea/.idea.MessageFormat/.idea/workspace.xml +.DS_Store diff --git a/src/.idea/.idea.MessageFormat/.idea/contentModel.xml b/src/.idea/.idea.MessageFormat/.idea/contentModel.xml index 2eb6084..b0abb30 100644 --- a/src/.idea/.idea.MessageFormat/.idea/contentModel.xml +++ b/src/.idea/.idea.MessageFormat/.idea/contentModel.xml @@ -38,7 +38,13 @@ - + + + + + + + diff --git a/src/Jeffijoe.MessageFormat/Jeffijoe.MessageFormat.csproj b/src/Jeffijoe.MessageFormat/Jeffijoe.MessageFormat.csproj index 14a9168..3c5df28 100644 --- a/src/Jeffijoe.MessageFormat/Jeffijoe.MessageFormat.csproj +++ b/src/Jeffijoe.MessageFormat/Jeffijoe.MessageFormat.csproj @@ -4,10 +4,8 @@ True MessageFormat.snk MessageFormat - False - false + True Jeff Hansen - Follow official guidelines for escaping literals (saithis, #15) messageformat,messageformatter,xamarin,ui,messages,formatting,plural,pluralization,singular,select,strings,stringformat,format https://github.com/jeffijoe/messageformat.net latest @@ -16,7 +14,7 @@ - bin\Release\netstandard1.1\Jeffijoe.MessageFormat.xml + bin/Release/netstandard1.1/Jeffijoe.MessageFormat.xml @@ -25,5 +23,4 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - diff --git a/src/MessageFormat.nuspec b/src/MessageFormat.nuspec deleted file mode 100644 index e35165f..0000000 --- a/src/MessageFormat.nuspec +++ /dev/null @@ -1,50 +0,0 @@ - - - - MessageFormat - 3.0.1 - MessageFormatter for .NET - Jeff Hansen - Jeff Hansen - false - https://github.com/jeffijoe/messageformat.net - PHP has it. Java has it. Even JavaScript has it. It's time .NET joined in with support for the ICU Message Format. - -Check the README on Github for more information. - An implementation of the ICU MessageFormatter for .NET - write contextual UI messages with proper pluralization, and more. Works with Xamarin! - Fix escaping (#16, @saithis) - Copyright © Jeff Hansen 2017 to present. All rights reserved. - en-US - messageformat,pcl,messageformatter,xamarin,ui,messages,formatting,plural,pluralization,singular,select,strings,stringformat,format - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/Settings.StyleCop b/src/Settings.StyleCop deleted file mode 100644 index 3b35ae0..0000000 --- a/src/Settings.StyleCop +++ /dev/null @@ -1,134 +0,0 @@ - - - - - - - False - - - - - False - - - - - False - - - - - False - - - - - False - - - - - False - - - - - False - - - - - False - - - - - False - - - - - False - - - - - False - - - - - - - - - - False - - - - - False - - - - - - - - - - False - - - - - - - - - - False - - - - - False - - - - - False - - - - - False - - - - - - - - - - False - - - - - False - - - - - False - - - - - - - \ No newline at end of file From b285778b792fb8960cde9692432e6cca4011227f Mon Sep 17 00:00:00 2001 From: Jeff Hansen Date: Fri, 4 Dec 2020 16:43:27 -0500 Subject: [PATCH 06/98] Add publish step --- .github/workflows/ci.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d703f94..c957b53 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,6 +5,8 @@ on: branches: [ master ] pull_request: branches: [ master ] + release: + action: released env: DOTNET_NOLOGO: true @@ -36,3 +38,8 @@ jobs: - name: Test run: dotnet test --no-restore --verbosity normal working-directory: ./src + + - name: Publish to NuGet + if: github.event_name == 'release' # Only publish on Release creation. + run: | + dotnet nuget push Jeffijoe.MessageFormat/**/*.nupkg --api-key ${{ secrets.NUGET_JEFFIJOE_KEY }} --source https://api.nuget.org/v3/index.json \ No newline at end of file From 4dbf9a05ecb9ddf4671e9897003b9cb2ea5b6704 Mon Sep 17 00:00:00 2001 From: Jeff Hansen Date: Sat, 5 Dec 2020 00:33:52 -0500 Subject: [PATCH 07/98] Add pack step to build --- .github/workflows/ci.yml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c957b53..d8b24f3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,16 +28,18 @@ jobs: dotnet-version: 3.1.x - name: Install dependencies - run: dotnet restore working-directory: ./src + run: dotnet restore - name: Build - run: dotnet build --configuration Release --no-restore working-directory: ./src + run: | + dotnet build --configuration Release --no-restore + dotnet pack -c Release Jeffijoe.MessageFormat/Jeffijoe.MessageFormat.csproj - name: Test - run: dotnet test --no-restore --verbosity normal working-directory: ./src + run: dotnet test --no-restore --verbosity normal - name: Publish to NuGet if: github.event_name == 'release' # Only publish on Release creation. From 2582af44af9471465f3b573317b72f0cb932d08e Mon Sep 17 00:00:00 2001 From: Jeff Hansen Date: Sat, 5 Dec 2020 00:45:08 -0500 Subject: [PATCH 08/98] Add working-directory option to publish step --- .github/workflows/ci.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d8b24f3..59f8551 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -43,5 +43,6 @@ jobs: - name: Publish to NuGet if: github.event_name == 'release' # Only publish on Release creation. + working-directory: ./src run: | - dotnet nuget push Jeffijoe.MessageFormat/**/*.nupkg --api-key ${{ secrets.NUGET_JEFFIJOE_KEY }} --source https://api.nuget.org/v3/index.json \ No newline at end of file + dotnet nuget push Jeffijoe.MessageFormat/**/*.nupkg --api-key ${{ secrets.NUGET_JEFFIJOE_KEY }} --source https://api.nuget.org/v3/index.json From d595c50bffd6d1cd84ab4769435e6ed38436d13c Mon Sep 17 00:00:00 2001 From: Jeff Hansen Date: Sat, 5 Dec 2020 00:48:51 -0500 Subject: [PATCH 09/98] I SAID ONLY ON RELEASE! --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 59f8551..c1ef719 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,7 +6,7 @@ on: pull_request: branches: [ master ] release: - action: released + types: [released] env: DOTNET_NOLOGO: true From 0ac587c8f418768509ea8378d0dba4313a60f640 Mon Sep 17 00:00:00 2001 From: Jeff Hansen Date: Sat, 5 Dec 2020 01:07:57 -0500 Subject: [PATCH 10/98] Update readme --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 4a45314..2f902c3 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ #### - better UI strings. - [![Build status](https://ci.appveyor.com/api/projects/status/9g7dplst1vyibc3e?svg=true)](https://ci.appveyor.com/project/jeffijoe/messageformat-net) [![Join the chat at https://gitter.im/jeffijoe/messageformat.net](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/jeffijoe/messageformat.net?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) +![Build](https://github.com/jeffijoe/messageformat.net/workflows/.NET%20Core/badge.svg) This is an implementation of the ICU Message Format in .NET. For official information about the format, go to: http://userguide.icu-project.org/formatparse/messages @@ -60,7 +60,7 @@ Install-Package MessageFormat ## Features * **It's fast.** Everything is hand-written; no parser-generators, *not even regular expressions*. -* **It's portable.** The library is a PCL, and has just a single dependency ([Portable.ConcurrentDictionary](https://www.nuget.org/packages/Portable.ConcurrentDictionary/) for thread safety) - other than that the only reference is the standard `.NET` in PCL's. +* **It's portable.** The library is targeting **.NET Standard 1.1**. * **It's compatible with other implementations.** I've been peeking a bit at the [MessageFormat.js][0] library to make sure the results would be the same. * **It's (relatively) small**. For a .NET library, ~25kb is not a lot. @@ -68,7 +68,7 @@ Install-Package MessageFormat * **Nesting is supported.** You can nest your blocks as you please, there's no special structure required to do this, just ensure your braces match. * **Adding your own formatters.** I don't know why you would need to, but if you want, you can add your own formatters, and take advantage of the code in my base classes to help you parse patterns. Look at the source, this is how I implemented the built-in formatters. -* **Exceptions make atleast a little sense.** When exceptions are thrown due to a bad pattern, the exception should include useful information. +* **Exceptions make at least a little sense.** When exceptions are thrown due to a bad pattern, the exception should include useful information. * **There are unit tests.** Run them yourself if you want, they're using XUnit. * **Built-in cache.** If you are formatting messages in a tight loop, with different data for each iteration, and if you are reusing the same instance of `MessageFormatter`, the formatter will cache the tokens of each pattern (nested, too), From 446c27be227f59ef3ea99007e5fcaf3946ac342b Mon Sep 17 00:00:00 2001 From: Kostiantyn Sharovarskyi Date: Tue, 30 Mar 2021 19:34:12 +0300 Subject: [PATCH 11/98] Add source generator. Parse simple rules with absolute value and visible digits number --- .../Jeffijoe.MessageFormat.Tests.csproj | 1 + .../MetadataGenerator/ParserTests.cs | 126 +++++++++ .../Formatters/PluralRulesMetadata.cs | 24 ++ .../Jeffijoe.MessageFormat.csproj | 4 + ...joe.MessageFormat.MetadataGenerator.csproj | 20 ++ .../Plural/Parsing/Condition.cs | 21 ++ .../Plural/Parsing/OperandSymbol.cs | 8 + .../Plural/Parsing/Operation.cs | 18 ++ .../Plural/Parsing/OrCondition.cs | 12 + .../Plural/Parsing/PluralParser.cs | 139 ++++++++++ .../Plural/Parsing/PluralRule.cs | 15 ++ .../Plural/Parsing/Relation.cs | 7 + .../Plural/PluralLanguagesGenerator.cs | 18 ++ .../data/plurals.xml | 242 ++++++++++++++++++ src/MessageFormat.sln | 17 +- 15 files changed, 668 insertions(+), 4 deletions(-) create mode 100644 src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/ParserTests.cs create mode 100644 src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralRulesMetadata.cs create mode 100644 src/Jeffjoe.MessageFormat.MetadataGenerator/Jeffjoe.MessageFormat.MetadataGenerator.csproj create mode 100644 src/Jeffjoe.MessageFormat.MetadataGenerator/Plural/Parsing/Condition.cs create mode 100644 src/Jeffjoe.MessageFormat.MetadataGenerator/Plural/Parsing/OperandSymbol.cs create mode 100644 src/Jeffjoe.MessageFormat.MetadataGenerator/Plural/Parsing/Operation.cs create mode 100644 src/Jeffjoe.MessageFormat.MetadataGenerator/Plural/Parsing/OrCondition.cs create mode 100644 src/Jeffjoe.MessageFormat.MetadataGenerator/Plural/Parsing/PluralParser.cs create mode 100644 src/Jeffjoe.MessageFormat.MetadataGenerator/Plural/Parsing/PluralRule.cs create mode 100644 src/Jeffjoe.MessageFormat.MetadataGenerator/Plural/Parsing/Relation.cs create mode 100644 src/Jeffjoe.MessageFormat.MetadataGenerator/Plural/PluralLanguagesGenerator.cs create mode 100644 src/Jeffjoe.MessageFormat.MetadataGenerator/data/plurals.xml diff --git a/src/Jeffijoe.MessageFormat.Tests/Jeffijoe.MessageFormat.Tests.csproj b/src/Jeffijoe.MessageFormat.Tests/Jeffijoe.MessageFormat.Tests.csproj index c522346..898d89b 100644 --- a/src/Jeffijoe.MessageFormat.Tests/Jeffijoe.MessageFormat.Tests.csproj +++ b/src/Jeffijoe.MessageFormat.Tests/Jeffijoe.MessageFormat.Tests.csproj @@ -26,6 +26,7 @@ + diff --git a/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/ParserTests.cs b/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/ParserTests.cs new file mode 100644 index 0000000..79c9675 --- /dev/null +++ b/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/ParserTests.cs @@ -0,0 +1,126 @@ +using Jeffjoe.MessageFormat.MetadataGenerator; + +using System.Collections.Generic; +using System.Xml; + +using Xunit; + +namespace Jeffijoe.MessageFormat.Tests.MetadataGenerator +{ + public class ParserTests + { + [Fact] + public void CanParseLocales() + { + var rules = ParseRules(@" + + + + + + +"); + + var rule = Assert.Single(rules); + var expected = new[] + { + "am", "as", "bn", "doi", "fa", "gu", "hi", "kn", "pcm", "zu" + }; + var actual = rule.Locales; + Assert.Equal(actual, expected); + } + + [Fact] + public void OtherCountIsIgnored() + { + var rules = ParseRules(@" + + + + @integer 2~17, 100, 1000, 10000, 100000, 1000000, … @decimal 1.1~2.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, … + + + +"); + var rule = Assert.Single(rules); + Assert.Empty(rule.Conditions); + } + + [Fact] + public void CanParseSingleCount_RuleDescription_WithoutRelations() + { + var rules = ParseRules(@" + + + + @integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, … + + + +"); + var rule = Assert.Single(rules); + var condition = Assert.Single(rule.Conditions); + var expected = "@integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, …"; + Assert.Equal(expected, condition.RuleDescription); + } + + [Fact] + public void CanParseSingleCount_VisibleDigitsNumber() + { + var rules = ParseRules(@" + + + + v = 0 @integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, … + + + +"); + var rule = Assert.Single(rules); + var condition = Assert.Single(rule.Conditions); + var orCondition = Assert.Single(condition.OrConditions); + var actual = Assert.Single(orCondition.AndConditions); + var expected = new Operation(OperandSymbol.VisibleFractionDigitNumber, Relation.Equals, new[] { 0 }); + + AssertOperationEqual(expected, actual); + } + + [Fact] + public void CanParseSingleCount_AbsoluteNumber() + { + var rules = ParseRules(@" + + + + n = 1 @integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, … + + + +"); + var rule = Assert.Single(rules); + var condition = Assert.Single(rule.Conditions); + var orCondition = Assert.Single(condition.OrConditions); + var actual = Assert.Single(orCondition.AndConditions); + var expected = new Operation(OperandSymbol.AbsoluteValue, Relation.Equals, new[] { 1 }); + + AssertOperationEqual(expected, actual); + } + + private static void AssertOperationEqual(Operation expected, Operation actual) + { + Assert.Equal(expected.OperandLeft, actual.OperandLeft); + Assert.Equal(expected.Relation, actual.Relation); + Assert.Equal(expected.OperandRight, actual.OperandRight); + } + + private static IEnumerable ParseRules(string xmlText) + { + var xml = new XmlDocument(); + xml.LoadXml(xmlText); + + var parser = new PluralParser(xml); + + return parser.Parse(); + } + } +} diff --git a/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralRulesMetadata.cs b/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralRulesMetadata.cs new file mode 100644 index 0000000..b989134 --- /dev/null +++ b/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralRulesMetadata.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Jeffijoe.MessageFormat.Formatting.Formatters +{ + public static partial class PluralRulesMetadata + { + public static string DefaultPluralRule(double number) + { + if(number == 0) + { + return "zero"; + } + + if(number == 1) + { + return "one"; + } + + return "other"; + } + } +} diff --git a/src/Jeffijoe.MessageFormat/Jeffijoe.MessageFormat.csproj b/src/Jeffijoe.MessageFormat/Jeffijoe.MessageFormat.csproj index 3c5df28..573ac9e 100644 --- a/src/Jeffijoe.MessageFormat/Jeffijoe.MessageFormat.csproj +++ b/src/Jeffijoe.MessageFormat/Jeffijoe.MessageFormat.csproj @@ -23,4 +23,8 @@ runtime; build; native; contentfiles; analyzers; buildtransitive + + + + diff --git a/src/Jeffjoe.MessageFormat.MetadataGenerator/Jeffjoe.MessageFormat.MetadataGenerator.csproj b/src/Jeffjoe.MessageFormat.MetadataGenerator/Jeffjoe.MessageFormat.MetadataGenerator.csproj new file mode 100644 index 0000000..e19b86f --- /dev/null +++ b/src/Jeffjoe.MessageFormat.MetadataGenerator/Jeffjoe.MessageFormat.MetadataGenerator.csproj @@ -0,0 +1,20 @@ + + + + netstandard2.1 + 8 + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + diff --git a/src/Jeffjoe.MessageFormat.MetadataGenerator/Plural/Parsing/Condition.cs b/src/Jeffjoe.MessageFormat.MetadataGenerator/Plural/Parsing/Condition.cs new file mode 100644 index 0000000..e69c4c7 --- /dev/null +++ b/src/Jeffjoe.MessageFormat.MetadataGenerator/Plural/Parsing/Condition.cs @@ -0,0 +1,21 @@ +using System.Diagnostics; + +namespace Jeffjoe.MessageFormat.MetadataGenerator +{ + [DebuggerDisplay("{{RuleDescription}}")] + public class Condition + { + public Condition(string count, string ruleDescription, OrCondition[] orConditions) + { + Count = count; + RuleDescription = ruleDescription; + OrConditions = orConditions; + } + + public string Count { get; } + + public string RuleDescription { get; } + + public OrCondition[] OrConditions { get; } + } +} diff --git a/src/Jeffjoe.MessageFormat.MetadataGenerator/Plural/Parsing/OperandSymbol.cs b/src/Jeffjoe.MessageFormat.MetadataGenerator/Plural/Parsing/OperandSymbol.cs new file mode 100644 index 0000000..e6ab4b5 --- /dev/null +++ b/src/Jeffjoe.MessageFormat.MetadataGenerator/Plural/Parsing/OperandSymbol.cs @@ -0,0 +1,8 @@ +namespace Jeffjoe.MessageFormat.MetadataGenerator +{ + public enum OperandSymbol + { + AbsoluteValue, + VisibleFractionDigitNumber + } +} diff --git a/src/Jeffjoe.MessageFormat.MetadataGenerator/Plural/Parsing/Operation.cs b/src/Jeffjoe.MessageFormat.MetadataGenerator/Plural/Parsing/Operation.cs new file mode 100644 index 0000000..640e9e0 --- /dev/null +++ b/src/Jeffjoe.MessageFormat.MetadataGenerator/Plural/Parsing/Operation.cs @@ -0,0 +1,18 @@ +namespace Jeffjoe.MessageFormat.MetadataGenerator +{ + public class Operation + { + public Operation(OperandSymbol operandLeft, Relation relation, int[] operandRight) + { + OperandLeft = operandLeft; + Relation = relation; + OperandRight = operandRight; + } + + public OperandSymbol OperandLeft { get; } + + public Relation Relation { get; } + + public int[] OperandRight { get; } + } +} diff --git a/src/Jeffjoe.MessageFormat.MetadataGenerator/Plural/Parsing/OrCondition.cs b/src/Jeffjoe.MessageFormat.MetadataGenerator/Plural/Parsing/OrCondition.cs new file mode 100644 index 0000000..e75576d --- /dev/null +++ b/src/Jeffjoe.MessageFormat.MetadataGenerator/Plural/Parsing/OrCondition.cs @@ -0,0 +1,12 @@ +namespace Jeffjoe.MessageFormat.MetadataGenerator +{ + public class OrCondition + { + public OrCondition(Operation[] andConditions) + { + AndConditions = andConditions; + } + + public Operation[] AndConditions { get; } + } +} diff --git a/src/Jeffjoe.MessageFormat.MetadataGenerator/Plural/Parsing/PluralParser.cs b/src/Jeffjoe.MessageFormat.MetadataGenerator/Plural/Parsing/PluralParser.cs new file mode 100644 index 0000000..0b2cb16 --- /dev/null +++ b/src/Jeffjoe.MessageFormat.MetadataGenerator/Plural/Parsing/PluralParser.cs @@ -0,0 +1,139 @@ +using System; +using System.Collections.Generic; +using System.Xml; + +namespace Jeffjoe.MessageFormat.MetadataGenerator +{ + public class PluralParser + { + private readonly XmlDocument _rulesDocument; + + public PluralParser(XmlDocument rulesDocument) + { + _rulesDocument = rulesDocument; + } + + public IEnumerable Parse() + { + var root = _rulesDocument.DocumentElement; + + foreach(XmlNode dataElement in root.ChildNodes) + { + if(dataElement.Name == "plurals") + { + foreach (XmlNode rule in dataElement.ChildNodes) + { + if(rule.Name == "pluralRules") + { + yield return ParseSingleRule(rule); + } + } + } + } + } + + private PluralRule ParseSingleRule(XmlNode rule) + { + var locales = rule.Attributes["locales"].Value.Split(' '); + + var conditions = new List(); + foreach (XmlNode condition in rule.ChildNodes) + { + if(condition.Name == "pluralRule") + { + var count = condition.Attributes["count"].Value; + + // Ignore other, because other is basically everything else except for the conditions present + if (count == "other") + continue; + + var ruleContent = condition.InnerText; + + var orConditions = ParseRuleContent(ruleContent); + + conditions.Add(new Condition(count, ruleContent, orConditions)); + } + } + + return new PluralRule(locales, conditions.ToArray()); + } + + private ReadOnlySpan AdvanceWhitespace(ReadOnlySpan characters) + { + while (!characters.IsEmpty && char.IsWhiteSpace(characters[0])) + { + characters = characters.Slice(1); + } + + return characters; + } + + private ReadOnlySpan AdvanceAndSkipWhitespace(ReadOnlySpan characters, int skipCharacters) + { + characters = characters.Slice(skipCharacters); + characters = AdvanceWhitespace(characters); + + return characters; + } + + private OrCondition[] ParseRuleContent(string ruleContent) + { + var conditions = new List(); + + var currentParsing = ruleContent.AsSpan(); + + while(!currentParsing.IsEmpty) + { + currentParsing = AdvanceWhitespace(currentParsing); + if (currentParsing.IsEmpty) continue; + + // Samples section + if(currentParsing[0] == '@') + { + break; + } + + var operandSymbol = currentParsing[0] switch + { + 'v' => OperandSymbol.VisibleFractionDigitNumber, + 'n' => OperandSymbol.AbsoluteValue, + var otherCharacter => throw new ArgumentException($"Invalid format, do not recognise character '{otherCharacter}'") + }; + currentParsing = AdvanceAndSkipWhitespace(currentParsing, 1); + if (currentParsing.IsEmpty) continue; + + var relation = currentParsing[0] switch + { + '=' => Relation.Equals, + var otherCharacter => throw new ArgumentException($"Invalid format, do not recognise character '{otherCharacter}'") + }; + + currentParsing = AdvanceAndSkipWhitespace(currentParsing, 1); + if (currentParsing.IsEmpty) continue; + + var (number, numberSize) = ParseNumber(currentParsing); + currentParsing = AdvanceAndSkipWhitespace(currentParsing, numberSize); + + conditions.Add(new OrCondition(new[] + { + new Operation(operandSymbol, relation, new[] { number }) + })); + } + + return conditions.ToArray(); + } + + private static (int number, int numberSize) ParseNumber(ReadOnlySpan currentParsing) + { + int numbersCount = 0; + while (currentParsing.Length > numbersCount && char.IsNumber(currentParsing[numbersCount])) + { + numbersCount++; + } + + var number = int.Parse(currentParsing.Slice(0, numbersCount)); + + return (number, numbersCount); + } + } +} diff --git a/src/Jeffjoe.MessageFormat.MetadataGenerator/Plural/Parsing/PluralRule.cs b/src/Jeffjoe.MessageFormat.MetadataGenerator/Plural/Parsing/PluralRule.cs new file mode 100644 index 0000000..5e57bda --- /dev/null +++ b/src/Jeffjoe.MessageFormat.MetadataGenerator/Plural/Parsing/PluralRule.cs @@ -0,0 +1,15 @@ +namespace Jeffjoe.MessageFormat.MetadataGenerator +{ + public class PluralRule + { + public PluralRule(string[] locales, Condition[] conditions) + { + Locales = locales; + Conditions = conditions; + } + + public string[] Locales { get; } + + public Condition[] Conditions { get; } + } +} diff --git a/src/Jeffjoe.MessageFormat.MetadataGenerator/Plural/Parsing/Relation.cs b/src/Jeffjoe.MessageFormat.MetadataGenerator/Plural/Parsing/Relation.cs new file mode 100644 index 0000000..69817aa --- /dev/null +++ b/src/Jeffjoe.MessageFormat.MetadataGenerator/Plural/Parsing/Relation.cs @@ -0,0 +1,7 @@ +namespace Jeffjoe.MessageFormat.MetadataGenerator +{ + public enum Relation + { + Equals + } +} diff --git a/src/Jeffjoe.MessageFormat.MetadataGenerator/Plural/PluralLanguagesGenerator.cs b/src/Jeffjoe.MessageFormat.MetadataGenerator/Plural/PluralLanguagesGenerator.cs new file mode 100644 index 0000000..f58aeb8 --- /dev/null +++ b/src/Jeffjoe.MessageFormat.MetadataGenerator/Plural/PluralLanguagesGenerator.cs @@ -0,0 +1,18 @@ +using Microsoft.CodeAnalysis; + +using System; + +namespace Jeffjoe.MessageFormat.MetadataGenerator +{ + public class PluralLanguagesGenerator : ISourceGenerator + { + public void Execute(GeneratorExecutionContext context) + { + } + + public void Initialize(GeneratorInitializationContext context) + { + + } + } +} diff --git a/src/Jeffjoe.MessageFormat.MetadataGenerator/data/plurals.xml b/src/Jeffjoe.MessageFormat.MetadataGenerator/data/plurals.xml new file mode 100644 index 0000000..e118c2e --- /dev/null +++ b/src/Jeffjoe.MessageFormat.MetadataGenerator/data/plurals.xml @@ -0,0 +1,242 @@ + + + + + + + + + + + + @integer 0~15, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, … + + + + + + i = 0 or n = 1 @integer 0, 1 @decimal 0.0~1.0, 0.00~0.04 + @integer 2~17, 100, 1000, 10000, 100000, 1000000, … @decimal 1.1~2.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, … + + + i = 0,1 @integer 0, 1 @decimal 0.0~1.5 + @integer 2~17, 100, 1000, 10000, 100000, 1000000, … @decimal 2.0~3.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, … + + + i = 0..1 @integer 0, 1 @decimal 0.0~1.5 + @integer 2~17, 100, 1000, 10000, 100000, 1000000, … @decimal 2.0~3.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, … + + + i = 1 and v = 0 @integer 1 + @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, … + + + n = 0,1 or i = 0 and f = 1 @integer 0, 1 @decimal 0.0, 0.1, 1.0, 0.00, 0.01, 1.00, 0.000, 0.001, 1.000, 0.0000, 0.0001, 1.0000 + @integer 2~17, 100, 1000, 10000, 100000, 1000000, … @decimal 0.2~0.9, 1.1~1.8, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, … + + + n = 0..1 @integer 0, 1 @decimal 0.0, 1.0, 0.00, 1.00, 0.000, 1.000, 0.0000, 1.0000 + @integer 2~17, 100, 1000, 10000, 100000, 1000000, … @decimal 0.1~0.9, 1.1~1.7, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, … + + + n = 0..1 or n = 11..99 @integer 0, 1, 11~24 @decimal 0.0, 1.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0, 17.0, 18.0, 19.0, 20.0, 21.0, 22.0, 23.0, 24.0 + @integer 2~10, 100~106, 1000, 10000, 100000, 1000000, … @decimal 0.1~0.9, 1.1~1.7, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, … + + + n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000 + @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, … + + + n = 1 or t != 0 and i = 0,1 @integer 1 @decimal 0.1~1.6 + @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0, 2.0~3.4, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, … + + + t = 0 and i % 10 = 1 and i % 100 != 11 or t != 0 @integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, … @decimal 0.1~1.6, 10.1, 100.1, 1000.1, … + @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, … + + + v = 0 and i % 10 = 1 and i % 100 != 11 or f % 10 = 1 and f % 100 != 11 @integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, … @decimal 0.1, 1.1, 2.1, 3.1, 4.1, 5.1, 6.1, 7.1, 10.1, 100.1, 1000.1, … + @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0, 0.2~1.0, 1.2~1.7, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, … + + + v = 0 and i = 1,2,3 or v = 0 and i % 10 != 4,6,9 or v != 0 and f % 10 != 4,6,9 @integer 0~3, 5, 7, 8, 10~13, 15, 17, 18, 20, 21, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.3, 0.5, 0.7, 0.8, 1.0~1.3, 1.5, 1.7, 1.8, 2.0, 2.1, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, … + @integer 4, 6, 9, 14, 16, 19, 24, 26, 104, 1004, … @decimal 0.4, 0.6, 0.9, 1.4, 1.6, 1.9, 2.4, 2.6, 10.4, 100.4, 1000.4, … + + + + + + n % 10 = 0 or n % 100 = 11..19 or v = 2 and f % 100 = 11..19 @integer 0, 10~20, 30, 40, 50, 60, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0, 10.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, … + n % 10 = 1 and n % 100 != 11 or v = 2 and f % 10 = 1 and f % 100 != 11 or v != 2 and f % 10 = 1 @integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, … @decimal 0.1, 1.0, 1.1, 2.1, 3.1, 4.1, 5.1, 6.1, 7.1, 10.1, 100.1, 1000.1, … + @integer 2~9, 22~29, 102, 1002, … @decimal 0.2~0.9, 1.2~1.9, 10.2, 100.2, 1000.2, … + + + n = 0 @integer 0 @decimal 0.0, 0.00, 0.000, 0.0000 + i = 0,1 and n != 0 @integer 1 @decimal 0.1~1.6 + @integer 2~17, 100, 1000, 10000, 100000, 1000000, … @decimal 2.0~3.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, … + + + n = 0 @integer 0 @decimal 0.0, 0.00, 0.000, 0.0000 + n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000 + @integer 2~17, 100, 1000, 10000, 100000, 1000000, … @decimal 0.1~0.9, 1.1~1.7, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, … + + + + + + n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000 + n = 2 @integer 2 @decimal 2.0, 2.00, 2.000, 2.0000 + @integer 0, 3~17, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, … + + + + + + i = 0 or n = 1 @integer 0, 1 @decimal 0.0~1.0, 0.00~0.04 + n = 2..10 @integer 2~10 @decimal 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 2.00, 3.00, 4.00, 5.00, 6.00, 7.00, 8.00 + @integer 11~26, 100, 1000, 10000, 100000, 1000000, … @decimal 1.1~1.9, 2.1~2.7, 10.1, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, … + + + i = 1 and v = 0 @integer 1 + v != 0 or n = 0 or n % 100 = 2..19 @integer 0, 2~16, 102, 1002, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, … + @integer 20~35, 100, 1000, 10000, 100000, 1000000, … + + + v = 0 and i % 10 = 1 and i % 100 != 11 or f % 10 = 1 and f % 100 != 11 @integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, … @decimal 0.1, 1.1, 2.1, 3.1, 4.1, 5.1, 6.1, 7.1, 10.1, 100.1, 1000.1, … + v = 0 and i % 10 = 2..4 and i % 100 != 12..14 or f % 10 = 2..4 and f % 100 != 12..14 @integer 2~4, 22~24, 32~34, 42~44, 52~54, 62, 102, 1002, … @decimal 0.2~0.4, 1.2~1.4, 2.2~2.4, 3.2~3.4, 4.2~4.4, 5.2, 10.2, 100.2, 1000.2, … + @integer 0, 5~19, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0, 0.5~1.0, 1.5~2.0, 2.5~2.7, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, … + + + + + + i = 0,1 @integer 0, 1 @decimal 0.0~1.5 + e = 0 and i != 0 and i % 1000000 = 0 and v = 0 or e != 0..5 @integer 1000000, 1e6, 2e6, 3e6, 4e6, 5e6, 6e6, … @decimal 1.0000001e6, 1.1e6, 2.0000001e6, 2.1e6, 3.0000001e6, 3.1e6, … + @integer 2~17, 100, 1000, 10000, 100000, 1e3, 2e3, 3e3, 4e3, 5e3, 6e3, … @decimal 2.0~3.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, 1.0001e3, 1.1e3, 2.0001e3, 2.1e3, 3.0001e3, 3.1e3, … + + + + + + n = 1,11 @integer 1, 11 @decimal 1.0, 11.0, 1.00, 11.00, 1.000, 11.000, 1.0000 + n = 2,12 @integer 2, 12 @decimal 2.0, 12.0, 2.00, 12.00, 2.000, 12.000, 2.0000 + n = 3..10,13..19 @integer 3~10, 13~19 @decimal 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 13.0, 14.0, 15.0, 16.0, 17.0, 18.0, 19.0, 3.00 + @integer 0, 20~34, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.1, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, … + + + v = 0 and i % 100 = 1 @integer 1, 101, 201, 301, 401, 501, 601, 701, 1001, … + v = 0 and i % 100 = 2 @integer 2, 102, 202, 302, 402, 502, 602, 702, 1002, … + v = 0 and i % 100 = 3..4 or v != 0 @integer 3, 4, 103, 104, 203, 204, 303, 304, 403, 404, 503, 504, 603, 604, 703, 704, 1003, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, … + @integer 0, 5~19, 100, 1000, 10000, 100000, 1000000, … + + + v = 0 and i % 100 = 1 or f % 100 = 1 @integer 1, 101, 201, 301, 401, 501, 601, 701, 1001, … @decimal 0.1, 1.1, 2.1, 3.1, 4.1, 5.1, 6.1, 7.1, 10.1, 100.1, 1000.1, … + v = 0 and i % 100 = 2 or f % 100 = 2 @integer 2, 102, 202, 302, 402, 502, 602, 702, 1002, … @decimal 0.2, 1.2, 2.2, 3.2, 4.2, 5.2, 6.2, 7.2, 10.2, 100.2, 1000.2, … + v = 0 and i % 100 = 3..4 or f % 100 = 3..4 @integer 3, 4, 103, 104, 203, 204, 303, 304, 403, 404, 503, 504, 603, 604, 703, 704, 1003, … @decimal 0.3, 0.4, 1.3, 1.4, 2.3, 2.4, 3.3, 3.4, 4.3, 4.4, 5.3, 5.4, 6.3, 6.4, 7.3, 7.4, 10.3, 100.3, 1000.3, … + @integer 0, 5~19, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0, 0.5~1.0, 1.5~2.0, 2.5~2.7, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, … + + + + + + i = 1 and v = 0 @integer 1 + i = 2 and v = 0 @integer 2 + v = 0 and n != 0..10 and n % 10 = 0 @integer 20, 30, 40, 50, 60, 70, 80, 90, 100, 1000, 10000, 100000, 1000000, … + @integer 0, 3~17, 101, 1001, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, … + + + + + + i = 1 and v = 0 @integer 1 + i = 2..4 and v = 0 @integer 2~4 + v != 0 @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, … + @integer 0, 5~19, 100, 1000, 10000, 100000, 1000000, … + + + i = 1 and v = 0 @integer 1 + v = 0 and i % 10 = 2..4 and i % 100 != 12..14 @integer 2~4, 22~24, 32~34, 42~44, 52~54, 62, 102, 1002, … + v = 0 and i != 1 and i % 10 = 0..1 or v = 0 and i % 10 = 5..9 or v = 0 and i % 100 = 12..14 @integer 0, 5~19, 100, 1000, 10000, 100000, 1000000, … + @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, … + + + n % 10 = 1 and n % 100 != 11 @integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, … @decimal 1.0, 21.0, 31.0, 41.0, 51.0, 61.0, 71.0, 81.0, 101.0, 1001.0, … + n % 10 = 2..4 and n % 100 != 12..14 @integer 2~4, 22~24, 32~34, 42~44, 52~54, 62, 102, 1002, … @decimal 2.0, 3.0, 4.0, 22.0, 23.0, 24.0, 32.0, 33.0, 102.0, 1002.0, … + n % 10 = 0 or n % 10 = 5..9 or n % 100 = 11..14 @integer 0, 5~19, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, … + @decimal 0.1~0.9, 1.1~1.7, 10.1, 100.1, 1000.1, … + + + n % 10 = 1 and n % 100 != 11..19 @integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, … @decimal 1.0, 21.0, 31.0, 41.0, 51.0, 61.0, 71.0, 81.0, 101.0, 1001.0, … + n % 10 = 2..9 and n % 100 != 11..19 @integer 2~9, 22~29, 102, 1002, … @decimal 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 22.0, 102.0, 1002.0, … + f != 0 @decimal 0.1~0.9, 1.1~1.7, 10.1, 100.1, 1000.1, … + @integer 0, 10~20, 30, 40, 50, 60, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0, 10.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, … + + + n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000 + n = 0 or n % 100 = 2..10 @integer 0, 2~10, 102~107, 1002, … @decimal 0.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 10.0, 102.0, 1002.0, … + n % 100 = 11..19 @integer 11~19, 111~117, 1011, … @decimal 11.0, 12.0, 13.0, 14.0, 15.0, 16.0, 17.0, 18.0, 111.0, 1011.0, … + @integer 20~35, 100, 1000, 10000, 100000, 1000000, … @decimal 0.1~0.9, 1.1~1.7, 10.1, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, … + + + v = 0 and i % 10 = 1 and i % 100 != 11 @integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, … + v = 0 and i % 10 = 2..4 and i % 100 != 12..14 @integer 2~4, 22~24, 32~34, 42~44, 52~54, 62, 102, 1002, … + v = 0 and i % 10 = 0 or v = 0 and i % 10 = 5..9 or v = 0 and i % 100 = 11..14 @integer 0, 5~19, 100, 1000, 10000, 100000, 1000000, … + @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, … + + + + + + n % 10 = 1 and n % 100 != 11,71,91 @integer 1, 21, 31, 41, 51, 61, 81, 101, 1001, … @decimal 1.0, 21.0, 31.0, 41.0, 51.0, 61.0, 81.0, 101.0, 1001.0, … + n % 10 = 2 and n % 100 != 12,72,92 @integer 2, 22, 32, 42, 52, 62, 82, 102, 1002, … @decimal 2.0, 22.0, 32.0, 42.0, 52.0, 62.0, 82.0, 102.0, 1002.0, … + n % 10 = 3..4,9 and n % 100 != 10..19,70..79,90..99 @integer 3, 4, 9, 23, 24, 29, 33, 34, 39, 43, 44, 49, 103, 1003, … @decimal 3.0, 4.0, 9.0, 23.0, 24.0, 29.0, 33.0, 34.0, 103.0, 1003.0, … + n != 0 and n % 1000000 = 0 @integer 1000000, … @decimal 1000000.0, 1000000.00, 1000000.000, 1000000.0000, … + @integer 0, 5~8, 10~20, 100, 1000, 10000, 100000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, … + + + n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000 + n = 2 @integer 2 @decimal 2.0, 2.00, 2.000, 2.0000 + n = 3..6 @integer 3~6 @decimal 3.0, 4.0, 5.0, 6.0, 3.00, 4.00, 5.00, 6.00, 3.000, 4.000, 5.000, 6.000, 3.0000, 4.0000, 5.0000, 6.0000 + n = 7..10 @integer 7~10 @decimal 7.0, 8.0, 9.0, 10.0, 7.00, 8.00, 9.00, 10.00, 7.000, 8.000, 9.000, 10.000, 7.0000, 8.0000, 9.0000, 10.0000 + @integer 0, 11~25, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.1, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, … + + + v = 0 and i % 10 = 1 @integer 1, 11, 21, 31, 41, 51, 61, 71, 101, 1001, … + v = 0 and i % 10 = 2 @integer 2, 12, 22, 32, 42, 52, 62, 72, 102, 1002, … + v = 0 and i % 100 = 0,20,40,60,80 @integer 0, 20, 40, 60, 80, 100, 120, 140, 1000, 10000, 100000, 1000000, … + v != 0 @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, … + @integer 3~10, 13~19, 23, 103, 1003, … + + + + + + n = 0 @integer 0 @decimal 0.0, 0.00, 0.000, 0.0000 + n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000 + n % 100 = 2,22,42,62,82 or n % 1000 = 0 and n % 100000 = 1000..20000,40000,60000,80000 or n != 0 and n % 1000000 = 100000 @integer 2, 22, 42, 62, 82, 102, 122, 142, 1000, 10000, 100000, … @decimal 2.0, 22.0, 42.0, 62.0, 82.0, 102.0, 122.0, 142.0, 1000.0, 10000.0, 100000.0, … + n % 100 = 3,23,43,63,83 @integer 3, 23, 43, 63, 83, 103, 123, 143, 1003, … @decimal 3.0, 23.0, 43.0, 63.0, 83.0, 103.0, 123.0, 143.0, 1003.0, … + n != 1 and n % 100 = 1,21,41,61,81 @integer 21, 41, 61, 81, 101, 121, 141, 161, 1001, … @decimal 21.0, 41.0, 61.0, 81.0, 101.0, 121.0, 141.0, 161.0, 1001.0, … + @integer 4~19, 100, 1004, 1000000, … @decimal 0.1~0.9, 1.1~1.7, 10.0, 100.0, 1000.1, 1000000.0, … + + + n = 0 @integer 0 @decimal 0.0, 0.00, 0.000, 0.0000 + n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000 + n = 2 @integer 2 @decimal 2.0, 2.00, 2.000, 2.0000 + n % 100 = 3..10 @integer 3~10, 103~110, 1003, … @decimal 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 103.0, 1003.0, … + n % 100 = 11..99 @integer 11~26, 111, 1011, … @decimal 11.0, 12.0, 13.0, 14.0, 15.0, 16.0, 17.0, 18.0, 111.0, 1011.0, … + @integer 100~102, 200~202, 300~302, 400~402, 500~502, 600, 1000, 10000, 100000, 1000000, … @decimal 0.1~0.9, 1.1~1.7, 10.1, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, … + + + n = 0 @integer 0 @decimal 0.0, 0.00, 0.000, 0.0000 + n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000 + n = 2 @integer 2 @decimal 2.0, 2.00, 2.000, 2.0000 + n = 3 @integer 3 @decimal 3.0, 3.00, 3.000, 3.0000 + n = 6 @integer 6 @decimal 6.0, 6.00, 6.000, 6.0000 + @integer 4, 5, 7~20, 100, 1000, 10000, 100000, 1000000, … @decimal 0.1~0.9, 1.1~1.7, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, … + + + diff --git a/src/MessageFormat.sln b/src/MessageFormat.sln index e2bd321..a0a7563 100644 --- a/src/MessageFormat.sln +++ b/src/MessageFormat.sln @@ -1,11 +1,13 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 15 -VisualStudioVersion = 15.0.26403.7 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.31112.23 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jeffijoe.MessageFormat", "Jeffijoe.MessageFormat\Jeffijoe.MessageFormat.csproj", "{7D16B114-A482-4FC4-A055-8E96573BE2A3}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jeffijoe.MessageFormat", "Jeffijoe.MessageFormat\Jeffijoe.MessageFormat.csproj", "{7D16B114-A482-4FC4-A055-8E96573BE2A3}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jeffijoe.MessageFormat.Tests", "Jeffijoe.MessageFormat.Tests\Jeffijoe.MessageFormat.Tests.csproj", "{F1AC744E-9031-468E-A397-6F44AA19EBA1}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jeffijoe.MessageFormat.Tests", "Jeffijoe.MessageFormat.Tests\Jeffijoe.MessageFormat.Tests.csproj", "{F1AC744E-9031-468E-A397-6F44AA19EBA1}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jeffjoe.MessageFormat.MetadataGenerator", "Jeffjoe.MessageFormat.MetadataGenerator\Jeffjoe.MessageFormat.MetadataGenerator.csproj", "{5431C848-23D1-4752-A9B0-5159E5B2F92E}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -21,8 +23,15 @@ Global {F1AC744E-9031-468E-A397-6F44AA19EBA1}.Debug|Any CPU.Build.0 = Debug|Any CPU {F1AC744E-9031-468E-A397-6F44AA19EBA1}.Release|Any CPU.ActiveCfg = Release|Any CPU {F1AC744E-9031-468E-A397-6F44AA19EBA1}.Release|Any CPU.Build.0 = Release|Any CPU + {5431C848-23D1-4752-A9B0-5159E5B2F92E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5431C848-23D1-4752-A9B0-5159E5B2F92E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5431C848-23D1-4752-A9B0-5159E5B2F92E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5431C848-23D1-4752-A9B0-5159E5B2F92E}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {A7BC8D54-673A-497B-A89E-B5D1BC8CD506} + EndGlobalSection EndGlobal From e611ed6e54648aba2f5707841626f3a104b83a27 Mon Sep 17 00:00:00 2001 From: Kostiantyn Sharovarskyi Date: Wed, 31 Mar 2021 22:11:47 +0300 Subject: [PATCH 12/98] Fix assembly name. Refactor rule parsing to a class --- ...oe.MessageFormat.MetadataGenerator.csproj} | 0 .../Plural/Parsing/Condition.cs | 2 +- .../Parsing/InvalidCharacterException.cs | 11 ++ .../Plural/Parsing/OperandSymbol.cs | 2 +- .../Plural/Parsing/Operation.cs | 2 +- .../Plural/Parsing/OrCondition.cs | 2 +- .../Plural/Parsing/PluralParser.cs | 64 ++++++++ .../Plural/Parsing/PluralRule.cs | 2 +- .../Plural/Parsing/Relation.cs | 7 + .../Plural/Parsing/RuleParser.cs | 145 ++++++++++++++++++ .../Plural/PluralLanguagesGenerator.cs | 2 +- .../data/plurals.xml | 0 .../Jeffijoe.MessageFormat.Tests.csproj | 2 +- .../MetadataGenerator/ParserTests.cs | 25 ++- .../Plural/Parsing/PluralParser.cs | 139 ----------------- .../Plural/Parsing/Relation.cs | 7 - src/MessageFormat.sln | 2 +- 17 files changed, 259 insertions(+), 155 deletions(-) rename src/{Jeffjoe.MessageFormat.MetadataGenerator/Jeffjoe.MessageFormat.MetadataGenerator.csproj => Jeffijoe.MessageFormat.MetadataGenerator/Jeffijoe.MessageFormat.MetadataGenerator.csproj} (100%) rename src/{Jeffjoe.MessageFormat.MetadataGenerator => Jeffijoe.MessageFormat.MetadataGenerator}/Plural/Parsing/Condition.cs (87%) create mode 100644 src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/InvalidCharacterException.cs rename src/{Jeffjoe.MessageFormat.MetadataGenerator => Jeffijoe.MessageFormat.MetadataGenerator}/Plural/Parsing/OperandSymbol.cs (60%) rename src/{Jeffjoe.MessageFormat.MetadataGenerator => Jeffijoe.MessageFormat.MetadataGenerator}/Plural/Parsing/Operation.cs (85%) rename src/{Jeffjoe.MessageFormat.MetadataGenerator => Jeffijoe.MessageFormat.MetadataGenerator}/Plural/Parsing/OrCondition.cs (75%) create mode 100644 src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/PluralParser.cs rename src/{Jeffjoe.MessageFormat.MetadataGenerator => Jeffijoe.MessageFormat.MetadataGenerator}/Plural/Parsing/PluralRule.cs (80%) create mode 100644 src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/Relation.cs create mode 100644 src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/RuleParser.cs rename src/{Jeffjoe.MessageFormat.MetadataGenerator => Jeffijoe.MessageFormat.MetadataGenerator}/Plural/PluralLanguagesGenerator.cs (86%) rename src/{Jeffjoe.MessageFormat.MetadataGenerator => Jeffijoe.MessageFormat.MetadataGenerator}/data/plurals.xml (100%) delete mode 100644 src/Jeffjoe.MessageFormat.MetadataGenerator/Plural/Parsing/PluralParser.cs delete mode 100644 src/Jeffjoe.MessageFormat.MetadataGenerator/Plural/Parsing/Relation.cs diff --git a/src/Jeffjoe.MessageFormat.MetadataGenerator/Jeffjoe.MessageFormat.MetadataGenerator.csproj b/src/Jeffijoe.MessageFormat.MetadataGenerator/Jeffijoe.MessageFormat.MetadataGenerator.csproj similarity index 100% rename from src/Jeffjoe.MessageFormat.MetadataGenerator/Jeffjoe.MessageFormat.MetadataGenerator.csproj rename to src/Jeffijoe.MessageFormat.MetadataGenerator/Jeffijoe.MessageFormat.MetadataGenerator.csproj diff --git a/src/Jeffjoe.MessageFormat.MetadataGenerator/Plural/Parsing/Condition.cs b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/Condition.cs similarity index 87% rename from src/Jeffjoe.MessageFormat.MetadataGenerator/Plural/Parsing/Condition.cs rename to src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/Condition.cs index e69c4c7..a4d5dcf 100644 --- a/src/Jeffjoe.MessageFormat.MetadataGenerator/Plural/Parsing/Condition.cs +++ b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/Condition.cs @@ -1,6 +1,6 @@ using System.Diagnostics; -namespace Jeffjoe.MessageFormat.MetadataGenerator +namespace Jeffijoe.MessageFormat.MetadataGenerator.Plural.Parsing { [DebuggerDisplay("{{RuleDescription}}")] public class Condition diff --git a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/InvalidCharacterException.cs b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/InvalidCharacterException.cs new file mode 100644 index 0000000..dc03017 --- /dev/null +++ b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/InvalidCharacterException.cs @@ -0,0 +1,11 @@ +using System; + +namespace Jeffijoe.MessageFormat.MetadataGenerator.Plural.Parsing +{ + public class InvalidCharacterException : ArgumentException + { + public InvalidCharacterException(char character) : base($"Invalid format, do not recognise character '{character}'") + { + } + } +} diff --git a/src/Jeffjoe.MessageFormat.MetadataGenerator/Plural/Parsing/OperandSymbol.cs b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/OperandSymbol.cs similarity index 60% rename from src/Jeffjoe.MessageFormat.MetadataGenerator/Plural/Parsing/OperandSymbol.cs rename to src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/OperandSymbol.cs index e6ab4b5..cd973c2 100644 --- a/src/Jeffjoe.MessageFormat.MetadataGenerator/Plural/Parsing/OperandSymbol.cs +++ b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/OperandSymbol.cs @@ -1,4 +1,4 @@ -namespace Jeffjoe.MessageFormat.MetadataGenerator +namespace Jeffijoe.MessageFormat.MetadataGenerator.Plural.Parsing { public enum OperandSymbol { diff --git a/src/Jeffjoe.MessageFormat.MetadataGenerator/Plural/Parsing/Operation.cs b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/Operation.cs similarity index 85% rename from src/Jeffjoe.MessageFormat.MetadataGenerator/Plural/Parsing/Operation.cs rename to src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/Operation.cs index 640e9e0..ff15d87 100644 --- a/src/Jeffjoe.MessageFormat.MetadataGenerator/Plural/Parsing/Operation.cs +++ b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/Operation.cs @@ -1,4 +1,4 @@ -namespace Jeffjoe.MessageFormat.MetadataGenerator +namespace Jeffijoe.MessageFormat.MetadataGenerator.Plural.Parsing { public class Operation { diff --git a/src/Jeffjoe.MessageFormat.MetadataGenerator/Plural/Parsing/OrCondition.cs b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/OrCondition.cs similarity index 75% rename from src/Jeffjoe.MessageFormat.MetadataGenerator/Plural/Parsing/OrCondition.cs rename to src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/OrCondition.cs index e75576d..915ac38 100644 --- a/src/Jeffjoe.MessageFormat.MetadataGenerator/Plural/Parsing/OrCondition.cs +++ b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/OrCondition.cs @@ -1,4 +1,4 @@ -namespace Jeffjoe.MessageFormat.MetadataGenerator +namespace Jeffijoe.MessageFormat.MetadataGenerator.Plural.Parsing { public class OrCondition { diff --git a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/PluralParser.cs b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/PluralParser.cs new file mode 100644 index 0000000..ddf798c --- /dev/null +++ b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/PluralParser.cs @@ -0,0 +1,64 @@ +using System; +using System.Collections.Generic; +using System.Xml; + +namespace Jeffijoe.MessageFormat.MetadataGenerator.Plural.Parsing +{ + public class PluralParser + { + private readonly XmlDocument _rulesDocument; + + public PluralParser(XmlDocument rulesDocument) + { + _rulesDocument = rulesDocument; + } + + public IEnumerable Parse() + { + var root = _rulesDocument.DocumentElement; + + foreach(XmlNode dataElement in root.ChildNodes) + { + if(dataElement.Name == "plurals") + { + foreach (XmlNode rule in dataElement.ChildNodes) + { + if(rule.Name == "pluralRules") + { + yield return ParseSingleRule(rule); + } + } + } + } + } + + private PluralRule ParseSingleRule(XmlNode rule) + { + var locales = rule.Attributes["locales"].Value.Split(' '); + + var conditions = new List(); + foreach (XmlNode condition in rule.ChildNodes) + { + if(condition.Name == "pluralRule") + { + var count = condition.Attributes["count"].Value; + + // Ignore other, because other is basically everything else except for the conditions present + if (count == "other") + continue; + + var ruleContent = condition.InnerText; + + var ruleParser = new RuleParser(ruleContent); + var orConditions = ruleParser.ParseRuleContent(); + + conditions.Add(new Condition(count, ruleContent, orConditions)); + } + } + + return new PluralRule(locales, conditions.ToArray()); + } + + + } +} diff --git a/src/Jeffjoe.MessageFormat.MetadataGenerator/Plural/Parsing/PluralRule.cs b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/PluralRule.cs similarity index 80% rename from src/Jeffjoe.MessageFormat.MetadataGenerator/Plural/Parsing/PluralRule.cs rename to src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/PluralRule.cs index 5e57bda..a50a8c1 100644 --- a/src/Jeffjoe.MessageFormat.MetadataGenerator/Plural/Parsing/PluralRule.cs +++ b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/PluralRule.cs @@ -1,4 +1,4 @@ -namespace Jeffjoe.MessageFormat.MetadataGenerator +namespace Jeffijoe.MessageFormat.MetadataGenerator.Plural.Parsing { public class PluralRule { diff --git a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/Relation.cs b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/Relation.cs new file mode 100644 index 0000000..9eb0c9d --- /dev/null +++ b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/Relation.cs @@ -0,0 +1,7 @@ +namespace Jeffijoe.MessageFormat.MetadataGenerator.Plural.Parsing +{ + public enum Relation + { + Equals, NotEquals + } +} diff --git a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/RuleParser.cs b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/RuleParser.cs new file mode 100644 index 0000000..87d0aea --- /dev/null +++ b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/RuleParser.cs @@ -0,0 +1,145 @@ +using System; +using System.Collections.Generic; + +namespace Jeffijoe.MessageFormat.MetadataGenerator.Plural.Parsing +{ + public class RuleParser + { + private string _ruleText; + private int _position; + + public RuleParser(string ruleText) + { + _ruleText = ruleText; + } + + private char PeekCurrentChar => + _position < _ruleText.Length + ? _ruleText[_position] + : '0'; + + private char PeekNextChar => + _position + 1 < _ruleText.Length + ? _ruleText[_position + 1] + : '0'; + + private char PeekAt(int delta) + { + if (_position + delta > _ruleText.Length) + return '0'; + + return _ruleText[_position + delta]; + } + + private ReadOnlySpan ConsumeCharacters(int count) + { + if (_position + count > _ruleText.Length) + { + var characters = _ruleText.AsSpan(_position, _ruleText.Length - _position); + _position = _ruleText.Length; + return characters; + } + else + { + var characters = _ruleText.AsSpan(_position, count); + _position += count; + return characters; + } + } + + private char ConsumeChar() + { + if (IsEnd) + return '0'; + + var character = PeekCurrentChar; + _position++; + return character; + } + + private bool IsEnd => _position >= _ruleText.Length; + + private void AdvanceWhitespace() + { + while (!IsEnd && char.IsWhiteSpace(PeekCurrentChar)) + { + ConsumeChar(); + } + } + + private OrCondition ParseOrCondition() + { + AdvanceWhitespace(); + var operandSymbol = ConsumeChar() switch + { + 'v' => OperandSymbol.VisibleFractionDigitNumber, + 'n' => OperandSymbol.AbsoluteValue, + var otherCharacter => throw new InvalidCharacterException(otherCharacter) + }; + + AdvanceWhitespace(); + var firstRelationCharacter = ConsumeChar(); + var relation = firstRelationCharacter switch + { + '=' => Relation.Equals, + '!' when ConsumeChar() == '=' + => Relation.NotEquals, + var otherCharacter => throw new InvalidCharacterException(otherCharacter) + }; + + AdvanceWhitespace(); + var number = ParseNumber(); + return new OrCondition(new[] { new Operation(operandSymbol, relation, new[] { number })}); + } + + private int ParseNumber() + { + int numbersCount = 0; + while (!IsEnd && char.IsNumber(PeekAt(numbersCount))) + { + numbersCount++; + } + + var numberSpan = ConsumeCharacters(numbersCount); + var number = int.Parse(numberSpan); + + return number; + } + + public OrCondition[] ParseRuleContent() + { + var conditions = new List(); + + while (!IsEnd) + { + if (PeekCurrentChar == '@') + { + return conditions.ToArray(); + } + + var condition = ParseOrCondition(); + conditions.Add(condition); + + AdvanceWhitespace(); + + var character = ConsumeChar(); + var characterNext = ConsumeChar(); + + if (character == '@') + { + return conditions.ToArray(); + } + else if(character == 'o' && characterNext == 'r') + { + continue; + } + else + { + throw new InvalidCharacterException(character); + } + } + + return conditions.ToArray(); + } + } +} diff --git a/src/Jeffjoe.MessageFormat.MetadataGenerator/Plural/PluralLanguagesGenerator.cs b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/PluralLanguagesGenerator.cs similarity index 86% rename from src/Jeffjoe.MessageFormat.MetadataGenerator/Plural/PluralLanguagesGenerator.cs rename to src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/PluralLanguagesGenerator.cs index f58aeb8..0eb96ec 100644 --- a/src/Jeffjoe.MessageFormat.MetadataGenerator/Plural/PluralLanguagesGenerator.cs +++ b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/PluralLanguagesGenerator.cs @@ -2,7 +2,7 @@ using System; -namespace Jeffjoe.MessageFormat.MetadataGenerator +namespace Jeffijoe.MessageFormat.MetadataGenerator { public class PluralLanguagesGenerator : ISourceGenerator { diff --git a/src/Jeffjoe.MessageFormat.MetadataGenerator/data/plurals.xml b/src/Jeffijoe.MessageFormat.MetadataGenerator/data/plurals.xml similarity index 100% rename from src/Jeffjoe.MessageFormat.MetadataGenerator/data/plurals.xml rename to src/Jeffijoe.MessageFormat.MetadataGenerator/data/plurals.xml diff --git a/src/Jeffijoe.MessageFormat.Tests/Jeffijoe.MessageFormat.Tests.csproj b/src/Jeffijoe.MessageFormat.Tests/Jeffijoe.MessageFormat.Tests.csproj index 898d89b..d187690 100644 --- a/src/Jeffijoe.MessageFormat.Tests/Jeffijoe.MessageFormat.Tests.csproj +++ b/src/Jeffijoe.MessageFormat.Tests/Jeffijoe.MessageFormat.Tests.csproj @@ -26,7 +26,7 @@ - + diff --git a/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/ParserTests.cs b/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/ParserTests.cs index 79c9675..8af2394 100644 --- a/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/ParserTests.cs +++ b/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/ParserTests.cs @@ -1,4 +1,4 @@ -using Jeffjoe.MessageFormat.MetadataGenerator; +using Jeffijoe.MessageFormat.MetadataGenerator.Plural.Parsing; using System.Collections.Generic; using System.Xml; @@ -106,6 +106,29 @@ public void CanParseSingleCount_AbsoluteNumber() AssertOperationEqual(expected, actual); } + [Theory] + [InlineData("n = 2 @integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, …", Relation.Equals)] + [InlineData("n != 2 @integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, …", Relation.NotEquals)] + public void CanParseVariousRelations(string ruleText, Relation expectedRelation) + { + var rules = ParseRules($@" + + + + {ruleText} + + + +"); + var rule = Assert.Single(rules); + var condition = Assert.Single(rule.Conditions); + var orCondition = Assert.Single(condition.OrConditions); + var actual = Assert.Single(orCondition.AndConditions); + var expected = new Operation(OperandSymbol.AbsoluteValue, expectedRelation, new[] { 2 }); + + AssertOperationEqual(expected, actual); + } + private static void AssertOperationEqual(Operation expected, Operation actual) { Assert.Equal(expected.OperandLeft, actual.OperandLeft); diff --git a/src/Jeffjoe.MessageFormat.MetadataGenerator/Plural/Parsing/PluralParser.cs b/src/Jeffjoe.MessageFormat.MetadataGenerator/Plural/Parsing/PluralParser.cs deleted file mode 100644 index 0b2cb16..0000000 --- a/src/Jeffjoe.MessageFormat.MetadataGenerator/Plural/Parsing/PluralParser.cs +++ /dev/null @@ -1,139 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Xml; - -namespace Jeffjoe.MessageFormat.MetadataGenerator -{ - public class PluralParser - { - private readonly XmlDocument _rulesDocument; - - public PluralParser(XmlDocument rulesDocument) - { - _rulesDocument = rulesDocument; - } - - public IEnumerable Parse() - { - var root = _rulesDocument.DocumentElement; - - foreach(XmlNode dataElement in root.ChildNodes) - { - if(dataElement.Name == "plurals") - { - foreach (XmlNode rule in dataElement.ChildNodes) - { - if(rule.Name == "pluralRules") - { - yield return ParseSingleRule(rule); - } - } - } - } - } - - private PluralRule ParseSingleRule(XmlNode rule) - { - var locales = rule.Attributes["locales"].Value.Split(' '); - - var conditions = new List(); - foreach (XmlNode condition in rule.ChildNodes) - { - if(condition.Name == "pluralRule") - { - var count = condition.Attributes["count"].Value; - - // Ignore other, because other is basically everything else except for the conditions present - if (count == "other") - continue; - - var ruleContent = condition.InnerText; - - var orConditions = ParseRuleContent(ruleContent); - - conditions.Add(new Condition(count, ruleContent, orConditions)); - } - } - - return new PluralRule(locales, conditions.ToArray()); - } - - private ReadOnlySpan AdvanceWhitespace(ReadOnlySpan characters) - { - while (!characters.IsEmpty && char.IsWhiteSpace(characters[0])) - { - characters = characters.Slice(1); - } - - return characters; - } - - private ReadOnlySpan AdvanceAndSkipWhitespace(ReadOnlySpan characters, int skipCharacters) - { - characters = characters.Slice(skipCharacters); - characters = AdvanceWhitespace(characters); - - return characters; - } - - private OrCondition[] ParseRuleContent(string ruleContent) - { - var conditions = new List(); - - var currentParsing = ruleContent.AsSpan(); - - while(!currentParsing.IsEmpty) - { - currentParsing = AdvanceWhitespace(currentParsing); - if (currentParsing.IsEmpty) continue; - - // Samples section - if(currentParsing[0] == '@') - { - break; - } - - var operandSymbol = currentParsing[0] switch - { - 'v' => OperandSymbol.VisibleFractionDigitNumber, - 'n' => OperandSymbol.AbsoluteValue, - var otherCharacter => throw new ArgumentException($"Invalid format, do not recognise character '{otherCharacter}'") - }; - currentParsing = AdvanceAndSkipWhitespace(currentParsing, 1); - if (currentParsing.IsEmpty) continue; - - var relation = currentParsing[0] switch - { - '=' => Relation.Equals, - var otherCharacter => throw new ArgumentException($"Invalid format, do not recognise character '{otherCharacter}'") - }; - - currentParsing = AdvanceAndSkipWhitespace(currentParsing, 1); - if (currentParsing.IsEmpty) continue; - - var (number, numberSize) = ParseNumber(currentParsing); - currentParsing = AdvanceAndSkipWhitespace(currentParsing, numberSize); - - conditions.Add(new OrCondition(new[] - { - new Operation(operandSymbol, relation, new[] { number }) - })); - } - - return conditions.ToArray(); - } - - private static (int number, int numberSize) ParseNumber(ReadOnlySpan currentParsing) - { - int numbersCount = 0; - while (currentParsing.Length > numbersCount && char.IsNumber(currentParsing[numbersCount])) - { - numbersCount++; - } - - var number = int.Parse(currentParsing.Slice(0, numbersCount)); - - return (number, numbersCount); - } - } -} diff --git a/src/Jeffjoe.MessageFormat.MetadataGenerator/Plural/Parsing/Relation.cs b/src/Jeffjoe.MessageFormat.MetadataGenerator/Plural/Parsing/Relation.cs deleted file mode 100644 index 69817aa..0000000 --- a/src/Jeffjoe.MessageFormat.MetadataGenerator/Plural/Parsing/Relation.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Jeffjoe.MessageFormat.MetadataGenerator -{ - public enum Relation - { - Equals - } -} diff --git a/src/MessageFormat.sln b/src/MessageFormat.sln index a0a7563..3a62c07 100644 --- a/src/MessageFormat.sln +++ b/src/MessageFormat.sln @@ -7,7 +7,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jeffijoe.MessageFormat", "J EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jeffijoe.MessageFormat.Tests", "Jeffijoe.MessageFormat.Tests\Jeffijoe.MessageFormat.Tests.csproj", "{F1AC744E-9031-468E-A397-6F44AA19EBA1}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jeffjoe.MessageFormat.MetadataGenerator", "Jeffjoe.MessageFormat.MetadataGenerator\Jeffjoe.MessageFormat.MetadataGenerator.csproj", "{5431C848-23D1-4752-A9B0-5159E5B2F92E}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jeffijoe.MessageFormat.MetadataGenerator", "Jeffijoe.MessageFormat.MetadataGenerator\Jeffijoe.MessageFormat.MetadataGenerator.csproj", "{5431C848-23D1-4752-A9B0-5159E5B2F92E}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution From e2afb05fbc45a9de2a2d299310f0dfb53d2de2ce Mon Sep 17 00:00:00 2001 From: Kostiantyn Sharovarskyi Date: Wed, 31 Mar 2021 22:26:52 +0300 Subject: [PATCH 13/98] Add 'and' rule parsing --- .../Plural/Parsing/RuleParser.cs | 45 +++++++- .../MetadataGenerator/ParserTests.cs | 101 +++++++++++------- 2 files changed, 107 insertions(+), 39 deletions(-) diff --git a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/RuleParser.cs b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/RuleParser.cs index 87d0aea..ef3ae1c 100644 --- a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/RuleParser.cs +++ b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/RuleParser.cs @@ -67,7 +67,7 @@ private void AdvanceWhitespace() } } - private OrCondition ParseOrCondition() + private Operation ParseAndCondition() { AdvanceWhitespace(); var operandSymbol = ConsumeChar() switch @@ -82,14 +82,53 @@ private OrCondition ParseOrCondition() var relation = firstRelationCharacter switch { '=' => Relation.Equals, - '!' when ConsumeChar() == '=' + '!' when ConsumeChar() == '=' => Relation.NotEquals, var otherCharacter => throw new InvalidCharacterException(otherCharacter) }; AdvanceWhitespace(); var number = ParseNumber(); - return new OrCondition(new[] { new Operation(operandSymbol, relation, new[] { number })}); + return new Operation(operandSymbol, relation, new[] { number }); + } + + private OrCondition ParseOrCondition() + { + var andConditions = new List(); + while (!IsEnd) + { + var operation = ParseAndCondition(); + andConditions.Add(operation); + + AdvanceWhitespace(); + + if (PeekCurrentChar == 'a') + { + var andWord = ConsumeCharacters(3); + Span andWordExpected = stackalloc char[3] + { + 'a', + 'n', + 'd' + }; + + + if (andWord.SequenceEqual(andWordExpected)) + { + continue; + } + else + { + throw new InvalidCharacterException(andWord[0]); + } + } + else + { + return new OrCondition(andConditions.ToArray()); + } + } + + return new OrCondition(andConditions.ToArray()); } private int ParseNumber() diff --git a/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/ParserTests.cs b/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/ParserTests.cs index 8af2394..5790cb9 100644 --- a/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/ParserTests.cs +++ b/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/ParserTests.cs @@ -49,15 +49,8 @@ public void OtherCountIsIgnored() [Fact] public void CanParseSingleCount_RuleDescription_WithoutRelations() { - var rules = ParseRules(@" - - - - @integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, … - - - -"); + var rules = ParseRules(GenerateXmlWithRuleContent("@integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, …")); + var rule = Assert.Single(rules); var condition = Assert.Single(rule.Conditions); var expected = "@integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, …"; @@ -67,15 +60,8 @@ public void CanParseSingleCount_RuleDescription_WithoutRelations() [Fact] public void CanParseSingleCount_VisibleDigitsNumber() { - var rules = ParseRules(@" - - - - v = 0 @integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, … - - - -"); + var rules = ParseRules( + GenerateXmlWithRuleContent(@"v = 0 @integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, …")); var rule = Assert.Single(rules); var condition = Assert.Single(rule.Conditions); var orCondition = Assert.Single(condition.OrConditions); @@ -88,15 +74,8 @@ public void CanParseSingleCount_VisibleDigitsNumber() [Fact] public void CanParseSingleCount_AbsoluteNumber() { - var rules = ParseRules(@" - - - - n = 1 @integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, … - - - -"); + var rules = ParseRules( + GenerateXmlWithRuleContent("n = 1 @integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, …")); var rule = Assert.Single(rules); var condition = Assert.Single(rule.Conditions); var orCondition = Assert.Single(condition.OrConditions); @@ -111,15 +90,7 @@ public void CanParseSingleCount_AbsoluteNumber() [InlineData("n != 2 @integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, …", Relation.NotEquals)] public void CanParseVariousRelations(string ruleText, Relation expectedRelation) { - var rules = ParseRules($@" - - - - {ruleText} - - - -"); + var rules = ParseRules(GenerateXmlWithRuleContent(ruleText)); var rule = Assert.Single(rules); var condition = Assert.Single(rule.Conditions); var orCondition = Assert.Single(condition.OrConditions); @@ -129,6 +100,64 @@ public void CanParseVariousRelations(string ruleText, Relation expectedRelation) AssertOperationEqual(expected, actual); } + [Fact] + public void CanParseOrRules() + { + var rules = ParseRules(GenerateXmlWithRuleContent("n = 2 or n = 1 or n = 0 @integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, …")); + var rule = Assert.Single(rules); + var condition = Assert.Single(rule.Conditions); + + Assert.Equal(3, condition.OrConditions.Length); + + var actualFirst = Assert.Single(condition.OrConditions[0].AndConditions); + var expectedFirst = new Operation(OperandSymbol.AbsoluteValue, Relation.Equals, new[] { 2 }); + AssertOperationEqual(expectedFirst, actualFirst); + + var actualSecond = Assert.Single(condition.OrConditions[1].AndConditions); + var expectedSecond = new Operation(OperandSymbol.AbsoluteValue, Relation.Equals, new[] { 1 }); + AssertOperationEqual(expectedSecond, actualSecond); + + var actualThird = Assert.Single(condition.OrConditions[2].AndConditions); + var expectedThird = new Operation(OperandSymbol.AbsoluteValue, Relation.Equals, new[] { 0 }); + AssertOperationEqual(expectedThird, actualThird); + } + + [Fact] + public void CanParseAndRules() + { + var rules = ParseRules(GenerateXmlWithRuleContent("n = 2 and n = 1 and n = 0 @integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, …")); + var rule = Assert.Single(rules); + var condition = Assert.Single(rule.Conditions); + + var orCondition = Assert.Single(condition.OrConditions); + Assert.Equal(3, orCondition.AndConditions.Length); + + var actualFirst = orCondition.AndConditions[0]; + var expectedFirst = new Operation(OperandSymbol.AbsoluteValue, Relation.Equals, new[] { 2 }); + AssertOperationEqual(expectedFirst, actualFirst); + + var actualSecond = orCondition.AndConditions[1]; + var expectedSecond = new Operation(OperandSymbol.AbsoluteValue, Relation.Equals, new[] { 1 }); + AssertOperationEqual(expectedSecond, actualSecond); + + var actualThird = orCondition.AndConditions[2]; + var expectedThird = new Operation(OperandSymbol.AbsoluteValue, Relation.Equals, new[] { 0 }); + AssertOperationEqual(expectedThird, actualThird); + } + + private static string GenerateXmlWithRuleContent(string ruleText) + { + return $@" + + + + {ruleText} + + + +"; + } + private static void AssertOperationEqual(Operation expected, Operation actual) { Assert.Equal(expected.OperandLeft, actual.OperandLeft); From 05ca43083895eaf2a2acf912fcdf7429df7c6cd8 Mon Sep 17 00:00:00 2001 From: Kostiantyn Sharovarskyi Date: Wed, 31 Mar 2021 22:50:23 +0300 Subject: [PATCH 14/98] Add modulo support --- .../Plural/Parsing/LeftOperand.cs | 28 +++++ .../Plural/Parsing/Operation.cs | 4 +- .../Plural/Parsing/RuleParser.cs | 100 +++++++++++------- .../MetadataGenerator/ParserTests.cs | 55 ++++++++-- 4 files changed, 135 insertions(+), 52 deletions(-) create mode 100644 src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/LeftOperand.cs diff --git a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/LeftOperand.cs b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/LeftOperand.cs new file mode 100644 index 0000000..f66dcb7 --- /dev/null +++ b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/LeftOperand.cs @@ -0,0 +1,28 @@ +namespace Jeffijoe.MessageFormat.MetadataGenerator.Plural.Parsing +{ + public interface ILeftOperand + { + } + + public class VariableOperand : ILeftOperand + { + public VariableOperand(OperandSymbol operand) + { + Operand = operand; + } + + public OperandSymbol Operand { get; } + } + + public class ModuloOperand : ILeftOperand + { + public ModuloOperand(OperandSymbol operandSymbol, int modValue) + { + Operand = operandSymbol; + ModValue = modValue; + } + + public OperandSymbol Operand { get; } + public int ModValue { get; } + } +} diff --git a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/Operation.cs b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/Operation.cs index ff15d87..7490528 100644 --- a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/Operation.cs +++ b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/Operation.cs @@ -2,14 +2,14 @@ { public class Operation { - public Operation(OperandSymbol operandLeft, Relation relation, int[] operandRight) + public Operation(ILeftOperand operandLeft, Relation relation, int[] operandRight) { OperandLeft = operandLeft; Relation = relation; OperandRight = operandRight; } - public OperandSymbol OperandLeft { get; } + public ILeftOperand OperandLeft { get; } public Relation Relation { get; } diff --git a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/RuleParser.cs b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/RuleParser.cs index ef3ae1c..856578b 100644 --- a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/RuleParser.cs +++ b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/RuleParser.cs @@ -13,6 +13,42 @@ public RuleParser(string ruleText) _ruleText = ruleText; } + public OrCondition[] ParseRuleContent() + { + var conditions = new List(); + + while (!IsEnd) + { + if (PeekCurrentChar == '@') + { + return conditions.ToArray(); + } + + var condition = ParseOrCondition(); + conditions.Add(condition); + + AdvanceWhitespace(); + + var character = ConsumeChar(); + var characterNext = ConsumeChar(); + + if (character == '@') + { + return conditions.ToArray(); + } + else if (character == 'o' && characterNext == 'r') + { + continue; + } + else + { + throw new InvalidCharacterException(character); + } + } + + return conditions.ToArray(); + } + private char PeekCurrentChar => _position < _ruleText.Length ? _ruleText[_position] @@ -67,9 +103,8 @@ private void AdvanceWhitespace() } } - private Operation ParseAndCondition() + private ILeftOperand ParseLeftOperand() { - AdvanceWhitespace(); var operandSymbol = ConsumeChar() switch { 'v' => OperandSymbol.VisibleFractionDigitNumber, @@ -77,6 +112,27 @@ private Operation ParseAndCondition() var otherCharacter => throw new InvalidCharacterException(otherCharacter) }; + AdvanceWhitespace(); + + if(PeekCurrentChar == '%') + { + ConsumeChar(); + AdvanceWhitespace(); + + var number = ParseNumber(); + + return new ModuloOperand(operandSymbol, number); + } + + return new VariableOperand(operandSymbol); + } + + private Operation ParseAndCondition() + { + AdvanceWhitespace(); + var leftOperand = ParseLeftOperand(); + + AdvanceWhitespace(); var firstRelationCharacter = ConsumeChar(); var relation = firstRelationCharacter switch @@ -89,7 +145,7 @@ private Operation ParseAndCondition() AdvanceWhitespace(); var number = ParseNumber(); - return new Operation(operandSymbol, relation, new[] { number }); + return new Operation(leftOperand, relation, new[] { number }); } private OrCondition ParseOrCondition() @@ -105,7 +161,7 @@ private OrCondition ParseOrCondition() if (PeekCurrentChar == 'a') { var andWord = ConsumeCharacters(3); - Span andWordExpected = stackalloc char[3] + ReadOnlySpan andWordExpected = stackalloc char[3] { 'a', 'n', @@ -144,41 +200,5 @@ private int ParseNumber() return number; } - - public OrCondition[] ParseRuleContent() - { - var conditions = new List(); - - while (!IsEnd) - { - if (PeekCurrentChar == '@') - { - return conditions.ToArray(); - } - - var condition = ParseOrCondition(); - conditions.Add(condition); - - AdvanceWhitespace(); - - var character = ConsumeChar(); - var characterNext = ConsumeChar(); - - if (character == '@') - { - return conditions.ToArray(); - } - else if(character == 'o' && characterNext == 'r') - { - continue; - } - else - { - throw new InvalidCharacterException(character); - } - } - - return conditions.ToArray(); - } } } diff --git a/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/ParserTests.cs b/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/ParserTests.cs index 5790cb9..1b3c16b 100644 --- a/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/ParserTests.cs +++ b/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/ParserTests.cs @@ -66,7 +66,7 @@ public void CanParseSingleCount_VisibleDigitsNumber() var condition = Assert.Single(rule.Conditions); var orCondition = Assert.Single(condition.OrConditions); var actual = Assert.Single(orCondition.AndConditions); - var expected = new Operation(OperandSymbol.VisibleFractionDigitNumber, Relation.Equals, new[] { 0 }); + var expected = new Operation(new VariableOperand(OperandSymbol.VisibleFractionDigitNumber), Relation.Equals, new[] { 0 }); AssertOperationEqual(expected, actual); } @@ -80,7 +80,7 @@ public void CanParseSingleCount_AbsoluteNumber() var condition = Assert.Single(rule.Conditions); var orCondition = Assert.Single(condition.OrConditions); var actual = Assert.Single(orCondition.AndConditions); - var expected = new Operation(OperandSymbol.AbsoluteValue, Relation.Equals, new[] { 1 }); + var expected = new Operation(new VariableOperand(OperandSymbol.AbsoluteValue), Relation.Equals, new[] { 1 }); AssertOperationEqual(expected, actual); } @@ -95,7 +95,7 @@ public void CanParseVariousRelations(string ruleText, Relation expectedRelation) var condition = Assert.Single(rule.Conditions); var orCondition = Assert.Single(condition.OrConditions); var actual = Assert.Single(orCondition.AndConditions); - var expected = new Operation(OperandSymbol.AbsoluteValue, expectedRelation, new[] { 2 }); + var expected = new Operation(new VariableOperand(OperandSymbol.AbsoluteValue), expectedRelation, new[] { 2 }); AssertOperationEqual(expected, actual); } @@ -110,15 +110,15 @@ public void CanParseOrRules() Assert.Equal(3, condition.OrConditions.Length); var actualFirst = Assert.Single(condition.OrConditions[0].AndConditions); - var expectedFirst = new Operation(OperandSymbol.AbsoluteValue, Relation.Equals, new[] { 2 }); + var expectedFirst = new Operation(new VariableOperand(OperandSymbol.AbsoluteValue), Relation.Equals, new[] { 2 }); AssertOperationEqual(expectedFirst, actualFirst); var actualSecond = Assert.Single(condition.OrConditions[1].AndConditions); - var expectedSecond = new Operation(OperandSymbol.AbsoluteValue, Relation.Equals, new[] { 1 }); + var expectedSecond = new Operation(new VariableOperand(OperandSymbol.AbsoluteValue), Relation.Equals, new[] { 1 }); AssertOperationEqual(expectedSecond, actualSecond); var actualThird = Assert.Single(condition.OrConditions[2].AndConditions); - var expectedThird = new Operation(OperandSymbol.AbsoluteValue, Relation.Equals, new[] { 0 }); + var expectedThird = new Operation(new VariableOperand(OperandSymbol.AbsoluteValue), Relation.Equals, new[] { 0 }); AssertOperationEqual(expectedThird, actualThird); } @@ -133,18 +133,33 @@ public void CanParseAndRules() Assert.Equal(3, orCondition.AndConditions.Length); var actualFirst = orCondition.AndConditions[0]; - var expectedFirst = new Operation(OperandSymbol.AbsoluteValue, Relation.Equals, new[] { 2 }); + var expectedFirst = new Operation(new VariableOperand(OperandSymbol.AbsoluteValue), Relation.Equals, new[] { 2 }); AssertOperationEqual(expectedFirst, actualFirst); var actualSecond = orCondition.AndConditions[1]; - var expectedSecond = new Operation(OperandSymbol.AbsoluteValue, Relation.Equals, new[] { 1 }); + var expectedSecond = new Operation(new VariableOperand(OperandSymbol.AbsoluteValue), Relation.Equals, new[] { 1 }); AssertOperationEqual(expectedSecond, actualSecond); var actualThird = orCondition.AndConditions[2]; - var expectedThird = new Operation(OperandSymbol.AbsoluteValue, Relation.Equals, new[] { 0 }); + var expectedThird = new Operation(new VariableOperand(OperandSymbol.AbsoluteValue), Relation.Equals, new[] { 0 }); AssertOperationEqual(expectedThird, actualThird); } + [Fact] + public void CanParseModuloInLeftOperator() + { + var rules = ParseRules( + GenerateXmlWithRuleContent("n % 5 = 3 @integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, …")); + var rule = Assert.Single(rules); + var condition = Assert.Single(rule.Conditions); + var orCondition = Assert.Single(condition.OrConditions); + var actual = Assert.Single(orCondition.AndConditions); + var modulo = new ModuloOperand(OperandSymbol.AbsoluteValue, 5); + var expected = new Operation(modulo, Relation.Equals, new[] { 3 }); + + AssertOperationEqual(expected, actual); + } + private static string GenerateXmlWithRuleContent(string ruleText) { return $@" @@ -160,11 +175,31 @@ private static string GenerateXmlWithRuleContent(string ruleText) private static void AssertOperationEqual(Operation expected, Operation actual) { - Assert.Equal(expected.OperandLeft, actual.OperandLeft); + AssertLeftOperandEqual(expected.OperandLeft, actual.OperandLeft); Assert.Equal(expected.Relation, actual.Relation); Assert.Equal(expected.OperandRight, actual.OperandRight); } + private static void AssertLeftOperandEqual(ILeftOperand expectedOperand, ILeftOperand actualOperand) + { + switch (expectedOperand, actualOperand) + { + case (VariableOperand expected, VariableOperand actual): + { + Assert.Equal(expected.Operand, actual.Operand); + } break; + case (ModuloOperand expected, ModuloOperand actual): + { + Assert.Equal(expected.Operand, actual.Operand); + Assert.Equal(expected.ModValue, actual.ModValue); + } break; + default: + { + Assert.False(true, $"Received unexpected operand types expected={expectedOperand.GetType()} actual={actualOperand.GetType()}"); + } break; + } + } + private static IEnumerable ParseRules(string xmlText) { var xml = new XmlDocument(); From b21c0a864e0a620d1273ac607fdad2e81ea5c2b8 Mon Sep 17 00:00:00 2001 From: Kostiantyn Sharovarskyi Date: Wed, 31 Mar 2021 23:03:53 +0300 Subject: [PATCH 15/98] Add support for range operators in the right part --- .../Plural/Parsing/RuleParser.cs | 51 ++++++++++++++++++- .../MetadataGenerator/ParserTests.cs | 45 ++++++++++++++++ 2 files changed, 94 insertions(+), 2 deletions(-) diff --git a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/RuleParser.cs b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/RuleParser.cs index 856578b..c1051f2 100644 --- a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/RuleParser.cs +++ b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/RuleParser.cs @@ -144,8 +144,55 @@ private Operation ParseAndCondition() }; AdvanceWhitespace(); - var number = ParseNumber(); - return new Operation(leftOperand, relation, new[] { number }); + var rightOperand = ParseRightOperand(); + return new Operation(leftOperand, relation, rightOperand); + } + + private int[] ParseRightOperand() + { + var numbers = new List(); + + while (!IsEnd) + { + AdvanceWhitespace(); + + var number = ParseNumber(); + if (PeekCurrentChar == '.') + { + if (PeekNextChar == '.') + { + ConsumeCharacters(2); + AdvanceWhitespace(); + + var nextNumber = ParseNumber(); + for (var i = number; i <= nextNumber; i++) + { + numbers.Add(i); + } + } + else + { + throw new InvalidCharacterException(PeekCurrentChar); + } + } + else + { + numbers.Add(number); + } + + if (PeekCurrentChar == ',') + { + ConsumeChar(); + AdvanceWhitespace(); + continue; + } + else + { + break; + } + } + + return numbers.ToArray(); } private OrCondition ParseOrCondition() diff --git a/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/ParserTests.cs b/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/ParserTests.cs index 1b3c16b..4e47443 100644 --- a/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/ParserTests.cs +++ b/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/ParserTests.cs @@ -160,6 +160,51 @@ public void CanParseModuloInLeftOperator() AssertOperationEqual(expected, actual); } + [Fact] + public void CanParseRangeInRightOperator() + { + var rules = ParseRules( + GenerateXmlWithRuleContent("n = 3..5 @integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, …")); + var rule = Assert.Single(rules); + var condition = Assert.Single(rule.Conditions); + var orCondition = Assert.Single(condition.OrConditions); + var actual = Assert.Single(orCondition.AndConditions); + var range = new[] { 3, 4, 5 }; + var expected = new Operation(new VariableOperand(OperandSymbol.AbsoluteValue), Relation.Equals, range); + + AssertOperationEqual(expected, actual); + } + + [Fact] + public void CanParseCommaSeparatedInRightOperator() + { + var rules = ParseRules( + GenerateXmlWithRuleContent("n = 3,5,8, 10 @integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, …")); + var rule = Assert.Single(rules); + var condition = Assert.Single(rule.Conditions); + var orCondition = Assert.Single(condition.OrConditions); + var actual = Assert.Single(orCondition.AndConditions); + var range = new[] { 3, 5, 8, 10 }; + var expected = new Operation(new VariableOperand(OperandSymbol.AbsoluteValue), Relation.Equals, range); + + AssertOperationEqual(expected, actual); + } + + [Fact] + public void CanParseMixedCommaSeparatedAndRangeInRightOperator() + { + var rules = ParseRules( + GenerateXmlWithRuleContent("n = 3,5..7,12,15 @integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, …")); + var rule = Assert.Single(rules); + var condition = Assert.Single(rule.Conditions); + var orCondition = Assert.Single(condition.OrConditions); + var actual = Assert.Single(orCondition.AndConditions); + var range = new[] { 3, 5, 6, 7, 12, 15 }; + var expected = new Operation(new VariableOperand(OperandSymbol.AbsoluteValue), Relation.Equals, range); + + AssertOperationEqual(expected, actual); + } + private static string GenerateXmlWithRuleContent(string ruleText) { return $@" From df8bec310f8d843103cd5967a375ef630efe0c88 Mon Sep 17 00:00:00 2001 From: Kostiantyn Sharovarskyi Date: Thu, 1 Apr 2021 00:32:34 +0300 Subject: [PATCH 16/98] Add source generation for a single rule --- .../Plural/Parsing/OperandSymbol.cs | 7 + .../Plural/SourceGeneration/RuleGenerator.cs | 162 ++++++++++ .../SourceGeneratingTests.cs | 289 ++++++++++++++++++ 3 files changed, 458 insertions(+) create mode 100644 src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/SourceGeneration/RuleGenerator.cs create mode 100644 src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/SourceGeneratingTests.cs diff --git a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/OperandSymbol.cs b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/OperandSymbol.cs index cd973c2..4439335 100644 --- a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/OperandSymbol.cs +++ b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/OperandSymbol.cs @@ -2,7 +2,14 @@ { public enum OperandSymbol { + /// + /// n - absolute value of the source number. + /// AbsoluteValue, + + /// + /// + /// VisibleFractionDigitNumber } } diff --git a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/SourceGeneration/RuleGenerator.cs b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/SourceGeneration/RuleGenerator.cs new file mode 100644 index 0000000..32bf790 --- /dev/null +++ b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/SourceGeneration/RuleGenerator.cs @@ -0,0 +1,162 @@ +using Jeffijoe.MessageFormat.MetadataGenerator.Plural.Parsing; + +using System; +using System.Collections.Generic; +using System.Text; + +namespace Jeffijoe.MessageFormat.MetadataGenerator.Plural.SourceGeneration +{ + public class RuleGenerator + { + private PluralRule _rule; + private int _conditionNumber; + private List _initializedSymbols; + private int _innerIndent; + + public RuleGenerator(PluralRule rule) + { + _rule = rule; + _conditionNumber = 0; + HasNext = true; + _initializedSymbols = new List(); + _innerIndent = 0; + } + + public bool HasNext { get; private set; } + + public void WriteNext(StringBuilder builder, int indent) + { + if(_conditionNumber > _rule.Conditions.Length - 1) + { + if(_rule.Conditions.Length > 0) + WriteLine(builder, string.Empty, indent); + + WriteLine(builder, "return \"other\";", indent); + HasNext = false; + return; + } + + var condition = _rule.Conditions[_conditionNumber]; + foreach (var operand in GetAllLeftOperands(condition.OrConditions)) + { + if(!_initializedSymbols.Contains(operand)) + { + var line = InitializeValue(operand); + WriteLine(builder, line, indent); + _initializedSymbols.Add(operand); + } + } + + WriteLine(builder, string.Empty, indent); + + if(condition.OrConditions.Length > 0) + { + builder.Append("if ("); + + for (int orIdx = 0; orIdx < condition.OrConditions.Length; orIdx++) + { + OrCondition orCondition = condition.OrConditions[orIdx]; + var orIsLast = orIdx == condition.OrConditions.Length - 1; + + WriteOrCondition(builder, orCondition); + + if (!orIsLast) + { + builder.Append(" || "); + } + } + + builder.AppendLine(")"); + + _innerIndent += 4; + WriteLine(builder, $"return \"{condition.Count}\";", indent); + _innerIndent -= 4; + } + else + { + throw new InvalidOperationException("Expected to have at least one or condition, but got none"); + } + + + _conditionNumber++; + } + + private void WriteOrCondition(StringBuilder builder, OrCondition orCondition) + { + for (int andIdx = 0; andIdx < orCondition.AndConditions.Length; andIdx++) + { + var andIsLast = andIdx == orCondition.AndConditions.Length - 1; + Operation andCondition = orCondition.AndConditions[andIdx]; + builder.Append('('); + var csharpOperator = andCondition.Relation == Relation.Equals ? "==" : "!="; + + for (int innerOrIdx = 0; innerOrIdx < andCondition.OperandRight.Length; innerOrIdx++) + { + var isLast = innerOrIdx == andCondition.OperandRight.Length - 1; + + int number = andCondition.OperandRight[innerOrIdx]; + if (andCondition.OperandLeft is VariableOperand op) + { + var variable = OperandToVariable(op.Operand); + builder.Append($"{variable} {csharpOperator} {number}"); + } + + if (!isLast) + { + builder.Append(" || "); + } + } + builder.Append(')'); + + if (!andIsLast) + { + builder.Append(" && "); + } + } + } + + private char OperandToVariable(OperandSymbol operand) + { + return operand switch + { + OperandSymbol.AbsoluteValue => 'n', + OperandSymbol.VisibleFractionDigitNumber => 'v', + _ => throw new InvalidOperationException($"Unknown variable {operand}") + }; + } + + private string InitializeValue(OperandSymbol operand) + { + return operand switch + { + OperandSymbol.AbsoluteValue => "var n = Math.Abs(value);", + OperandSymbol.VisibleFractionDigitNumber => "var v = (int)value == value ? 0 : 1;", + var otherSymbol => throw new InvalidOperationException($"Unknown operand symbol {otherSymbol}") + }; + } + + private IEnumerable GetAllLeftOperands(OrCondition[] conditions) + { + foreach (var condition in conditions) + { + foreach(var operation in condition.AndConditions) + { + var operand = operation.OperandLeft switch + { + VariableOperand op => op.Operand, + ModuloOperand op => op.Operand, + var op => throw new InvalidOperationException($"Unexpected operand {op.GetType()}") + }; + + yield return operand; + } + } + } + + private void WriteLine(StringBuilder builder, string value, int indent) + { + builder.Append(' ', indent + _innerIndent); + builder.AppendLine(value); + } + } +} diff --git a/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/SourceGeneratingTests.cs b/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/SourceGeneratingTests.cs new file mode 100644 index 0000000..9a0552f --- /dev/null +++ b/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/SourceGeneratingTests.cs @@ -0,0 +1,289 @@ +using Jeffijoe.MessageFormat.MetadataGenerator.Plural.Parsing; +using Jeffijoe.MessageFormat.MetadataGenerator.Plural.SourceGeneration; + +using System; +using System.Text; + +using Xunit; + +namespace Jeffijoe.MessageFormat.Tests.MetadataGenerator +{ + public class SourceGeneratingTests + { + [Fact] + public void CanGenerateEmptyRule() + { + var generator = new RuleGenerator(new PluralRule(new[] { "en" }, Array.Empty())); + + var actual = GenerateText(generator); + var expected = $"return \"other\";{Environment.NewLine}"; + Assert.Equal(expected, actual); + } + + [Fact] + public void CanGenerateRuleForFractionNumberEquals() + { + var generator = new RuleGenerator(new PluralRule(new[] { "en" }, new[] + { + new Condition("one", string.Empty, new[] + { + new OrCondition(new [] + { + new Operation(new VariableOperand(OperandSymbol.VisibleFractionDigitNumber), Relation.Equals, new[] { 0 }) + }) + }) + })); + + var actual = GenerateText(generator); + var expected = @$" +var v = (int)value == value ? 0 : 1; + +if ((v == 0)) + return ""one""; + +return ""other""; +".TrimStart(); + Assert.Equal(expected, actual); + } + + [Fact] + public void CanGenerateRuleForNumberEquals() + { + var generator = new RuleGenerator(new PluralRule(new[] { "en" }, new[] + { + new Condition("one", string.Empty, new[] + { + new OrCondition(new [] + { + new Operation(new VariableOperand(OperandSymbol.AbsoluteValue), Relation.Equals, new[] { 5 }) + }) + }) + })); + + var actual = GenerateText(generator); + var expected = @$" +var n = Math.Abs(value); + +if ((n == 5)) + return ""one""; + +return ""other""; +".TrimStart(); + Assert.Equal(expected, actual); + } + + [Fact] + public void CanGenerateRuleForNumberNotEquals() + { + var generator = new RuleGenerator(new PluralRule(new[] { "en" }, new[] + { + new Condition("one", string.Empty, new[] + { + new OrCondition(new [] + { + new Operation(new VariableOperand(OperandSymbol.AbsoluteValue), Relation.NotEquals, new[] { 5 }) + }) + }) + })); + + var actual = GenerateText(generator); + var expected = @$" +var n = Math.Abs(value); + +if ((n != 5)) + return ""one""; + +return ""other""; +".TrimStart(); + Assert.Equal(expected, actual); + } + + [Fact] + public void CanGenerateRuleForNumberRange() + { + var generator = new RuleGenerator(new PluralRule(new[] { "en" }, new[] + { + new Condition("one", string.Empty, new[] + { + new OrCondition(new [] + { + new Operation(new VariableOperand(OperandSymbol.AbsoluteValue), Relation.Equals, new[] { 5, 6, 10 }) + }) + }) + })); + + var actual = GenerateText(generator); + var expected = @$" +var n = Math.Abs(value); + +if ((n == 5 || n == 6 || n == 10)) + return ""one""; + +return ""other""; +".TrimStart(); + Assert.Equal(expected, actual); + } + + [Fact] + public void CanGenerateRuleForAndRules() + { + var generator = new RuleGenerator(new PluralRule(new[] { "en" }, new[] + { + new Condition("one", string.Empty, new[] + { + new OrCondition(new [] + { + new Operation(new VariableOperand(OperandSymbol.AbsoluteValue), Relation.Equals, new[] { 4 }), + new Operation(new VariableOperand(OperandSymbol.VisibleFractionDigitNumber), Relation.Equals, new[] { 0 }) + }) + }) + })); + + var actual = GenerateText(generator); + var expected = @$" +var n = Math.Abs(value); +var v = (int)value == value ? 0 : 1; + +if ((n == 4) && (v == 0)) + return ""one""; + +return ""other""; +".TrimStart(); + Assert.Equal(expected, actual); + } + + [Fact] + public void CanGenerateRuleForMixedRangeAndNumberRangeRules() + { + var generator = new RuleGenerator(new PluralRule(new[] { "en" }, new[] + { + new Condition("one", string.Empty, new[] + { + new OrCondition(new [] + { + new Operation(new VariableOperand(OperandSymbol.AbsoluteValue), Relation.Equals, new[] { 4, 5 }), + new Operation(new VariableOperand(OperandSymbol.VisibleFractionDigitNumber), Relation.Equals, new[] { 0 }) + }) + }) + })); + + var actual = GenerateText(generator); + var expected = @$" +var n = Math.Abs(value); +var v = (int)value == value ? 0 : 1; + +if ((n == 4 || n == 5) && (v == 0)) + return ""one""; + +return ""other""; +".TrimStart(); + Assert.Equal(expected, actual); + } + + [Fact] + public void CanGenerateRuleForMultipleOrRules() + { + var generator = new RuleGenerator(new PluralRule(new[] { "en" }, new[] + { + new Condition("one", string.Empty, new[] + { + new OrCondition(new [] + { + new Operation(new VariableOperand(OperandSymbol.AbsoluteValue), Relation.Equals, new[] { 4 }) + }), + new OrCondition(new [] + { + new Operation(new VariableOperand(OperandSymbol.VisibleFractionDigitNumber), Relation.Equals, new[] { 0 }) + }) + }) + })); + + var actual = GenerateText(generator); + var expected = @$" +var n = Math.Abs(value); +var v = (int)value == value ? 0 : 1; + +if ((n == 4) || (v == 0)) + return ""one""; + +return ""other""; +".TrimStart(); + Assert.Equal(expected, actual); + } + + [Fact] + public void CanGenerateRuleForMixedAndOrRules() + { + var generator = new RuleGenerator(new PluralRule(new[] { "en" }, new[] + { + new Condition("one", string.Empty, new[] + { + new OrCondition(new [] + { + new Operation(new VariableOperand(OperandSymbol.AbsoluteValue), Relation.Equals, new[] { 4 }), + new Operation(new VariableOperand(OperandSymbol.VisibleFractionDigitNumber), Relation.NotEquals, new[] { 0 }) + }), + new OrCondition(new [] + { + new Operation(new VariableOperand(OperandSymbol.VisibleFractionDigitNumber), Relation.Equals, new[] { 0 }) + }) + }) + })); + + var actual = GenerateText(generator); + var expected = @$" +var n = Math.Abs(value); +var v = (int)value == value ? 0 : 1; + +if ((n == 4) && (v != 0) || (v == 0)) + return ""one""; + +return ""other""; +".TrimStart(); + Assert.Equal(expected, actual); + } + + [Fact] + public void CanGenerateRuleForMixedAndOrRangeRules() + { + var generator = new RuleGenerator(new PluralRule(new[] { "en" }, new[] + { + new Condition("one", string.Empty, new[] + { + new OrCondition(new [] + { + new Operation(new VariableOperand(OperandSymbol.AbsoluteValue), Relation.Equals, new[] { 4, 5 }), + new Operation(new VariableOperand(OperandSymbol.VisibleFractionDigitNumber), Relation.NotEquals, new[] { 0 }) + }), + new OrCondition(new [] + { + new Operation(new VariableOperand(OperandSymbol.VisibleFractionDigitNumber), Relation.Equals, new[] { 0 }) + }) + }) + })); + + var actual = GenerateText(generator); + var expected = @$" +var n = Math.Abs(value); +var v = (int)value == value ? 0 : 1; + +if ((n == 4 || n == 5) && (v != 0) || (v == 0)) + return ""one""; + +return ""other""; +".TrimStart(); + Assert.Equal(expected, actual); + } + + private string GenerateText(RuleGenerator generator) + { + var sb = new StringBuilder(); + while(generator.HasNext) + { + generator.WriteNext(sb, 0); + } + + return sb.ToString(); + } + } +} From 662768dc638208ef261d0f40fca1c3a52a1cf985 Mon Sep 17 00:00:00 2001 From: Kostiantyn Sharovarskyi Date: Thu, 1 Apr 2021 02:50:06 +0300 Subject: [PATCH 17/98] Add code generation --- ...joe.MessageFormat.MetadataGenerator.csproj | 6 +- .../Plural/Parsing/OperandSymbol.cs | 9 +- .../Plural/Parsing/Operation.cs | 4 +- .../Plural/Parsing/PluralParser.cs | 22 ++- .../Plural/Parsing/RightOperand.cs | 54 ++++++++ .../Plural/Parsing/RuleParser.cs | 16 +-- .../Plural/PluralLanguagesGenerator.cs | 43 +++++- .../PluralRulesMetadataGenerator.cs | 91 +++++++++++++ .../Plural/SourceGeneration/RuleGenerator.cs | 62 +++++---- .../MetadataGenerator/ParserTests.cs | 43 ++++-- .../SourceGeneratingTests.cs | 125 ++++++++++++++---- .../Formatting/Formatters/PluralFormatter.cs | 2 + .../Formatters/PluralRulesMetadata.cs | 2 + .../Jeffijoe.MessageFormat.csproj | 15 ++- 14 files changed, 408 insertions(+), 86 deletions(-) create mode 100644 src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/RightOperand.cs create mode 100644 src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/SourceGeneration/PluralRulesMetadataGenerator.cs diff --git a/src/Jeffijoe.MessageFormat.MetadataGenerator/Jeffijoe.MessageFormat.MetadataGenerator.csproj b/src/Jeffijoe.MessageFormat.MetadataGenerator/Jeffijoe.MessageFormat.MetadataGenerator.csproj index e19b86f..77cd2e4 100644 --- a/src/Jeffijoe.MessageFormat.MetadataGenerator/Jeffijoe.MessageFormat.MetadataGenerator.csproj +++ b/src/Jeffijoe.MessageFormat.MetadataGenerator/Jeffijoe.MessageFormat.MetadataGenerator.csproj @@ -1,12 +1,13 @@  - netstandard2.1 + netstandard2.0 8 + enable - + @@ -17,4 +18,5 @@ + diff --git a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/OperandSymbol.cs b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/OperandSymbol.cs index 4439335..6920149 100644 --- a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/OperandSymbol.cs +++ b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/OperandSymbol.cs @@ -8,8 +8,13 @@ public enum OperandSymbol AbsoluteValue, /// - /// + /// v - number of visible fraction digits in n, with trailing zeros. /// - VisibleFractionDigitNumber + VisibleFractionDigitNumber, + + /// + /// i - integer digits of n. + /// + IntegerDigits, } } diff --git a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/Operation.cs b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/Operation.cs index 7490528..7a1c9da 100644 --- a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/Operation.cs +++ b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/Operation.cs @@ -2,7 +2,7 @@ { public class Operation { - public Operation(ILeftOperand operandLeft, Relation relation, int[] operandRight) + public Operation(ILeftOperand operandLeft, Relation relation, IRightOperand[] operandRight) { OperandLeft = operandLeft; Relation = relation; @@ -13,6 +13,6 @@ public Operation(ILeftOperand operandLeft, Relation relation, int[] operandRight public Relation Relation { get; } - public int[] OperandRight { get; } + public IRightOperand[] OperandRight { get; } } } diff --git a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/PluralParser.cs b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/PluralParser.cs index ddf798c..6d706e6 100644 --- a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/PluralParser.cs +++ b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/PluralParser.cs @@ -1,16 +1,19 @@ using System; using System.Collections.Generic; using System.Xml; +using System.Linq; namespace Jeffijoe.MessageFormat.MetadataGenerator.Plural.Parsing { public class PluralParser { private readonly XmlDocument _rulesDocument; + private readonly HashSet _excludedLocales; - public PluralParser(XmlDocument rulesDocument) + public PluralParser(XmlDocument rulesDocument, string[] excludedLocales) { _rulesDocument = rulesDocument; + _excludedLocales = new HashSet(excludedLocales); } public IEnumerable Parse() @@ -25,21 +28,30 @@ public IEnumerable Parse() { if(rule.Name == "pluralRules") { - yield return ParseSingleRule(rule); + var parsed = ParseSingleRule(rule); + if (parsed != null) + { + yield return parsed; + } } } } } } - private PluralRule ParseSingleRule(XmlNode rule) + private PluralRule? ParseSingleRule(XmlNode rule) { var locales = rule.Attributes["locales"].Value.Split(' '); + if (locales.All(l => _excludedLocales.Contains(l))) + { + return null; + } + var conditions = new List(); foreach (XmlNode condition in rule.ChildNodes) { - if(condition.Name == "pluralRule") + if (condition.Name == "pluralRule") { var count = condition.Attributes["count"].Value; @@ -58,7 +70,5 @@ private PluralRule ParseSingleRule(XmlNode rule) return new PluralRule(locales, conditions.ToArray()); } - - } } diff --git a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/RightOperand.cs b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/RightOperand.cs new file mode 100644 index 0000000..7882f0c --- /dev/null +++ b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/RightOperand.cs @@ -0,0 +1,54 @@ +namespace Jeffijoe.MessageFormat.MetadataGenerator.Plural.Parsing +{ + public interface IRightOperand + { + } + + public class NumberOperand : IRightOperand + { + public NumberOperand(int number) + { + Number = number; + } + + public int Number { get; } + + public override bool Equals(object obj) + { + if (obj is NumberOperand n) + return n.Number == Number; + + return base.Equals(obj); + } + + public override int GetHashCode() + { + return Number.GetHashCode(); + } + } + + public class RangeOperand : IRightOperand + { + public RangeOperand(int start, int end) + { + Start = start; + End = end; + } + + public int Start { get; } + public int End { get; } + + public override bool Equals(object obj) + { + if (obj is RangeOperand n) + return n.Start == Start && n.End == End; + + return base.Equals(obj); + } + + public override int GetHashCode() + { + return Start.GetHashCode() + End.GetHashCode(); + } + } +} diff --git a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/RuleParser.cs b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/RuleParser.cs index c1051f2..0a6f8ce 100644 --- a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/RuleParser.cs +++ b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/RuleParser.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; namespace Jeffijoe.MessageFormat.MetadataGenerator.Plural.Parsing { @@ -109,6 +110,7 @@ private ILeftOperand ParseLeftOperand() { 'v' => OperandSymbol.VisibleFractionDigitNumber, 'n' => OperandSymbol.AbsoluteValue, + 'i' => OperandSymbol.IntegerDigits, var otherCharacter => throw new InvalidCharacterException(otherCharacter) }; @@ -148,9 +150,9 @@ private Operation ParseAndCondition() return new Operation(leftOperand, relation, rightOperand); } - private int[] ParseRightOperand() + private IRightOperand[] ParseRightOperand() { - var numbers = new List(); + var numbers = new List(); while (!IsEnd) { @@ -165,10 +167,7 @@ private int[] ParseRightOperand() AdvanceWhitespace(); var nextNumber = ParseNumber(); - for (var i = number; i <= nextNumber; i++) - { - numbers.Add(i); - } + numbers.Add(new RangeOperand(number, nextNumber)); } else { @@ -177,7 +176,7 @@ private int[] ParseRightOperand() } else { - numbers.Add(number); + numbers.Add(new NumberOperand(number)); } if (PeekCurrentChar == ',') @@ -243,7 +242,8 @@ private int ParseNumber() } var numberSpan = ConsumeCharacters(numbersCount); - var number = int.Parse(numberSpan); + + var number = int.Parse(new string(numberSpan.ToArray())); return number; } diff --git a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/PluralLanguagesGenerator.cs b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/PluralLanguagesGenerator.cs index 0eb96ec..dc95d0e 100644 --- a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/PluralLanguagesGenerator.cs +++ b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/PluralLanguagesGenerator.cs @@ -1,13 +1,54 @@ -using Microsoft.CodeAnalysis; +using Jeffijoe.MessageFormat.MetadataGenerator.Plural.Parsing; +using Jeffijoe.MessageFormat.MetadataGenerator.Plural.SourceGeneration; + +using Microsoft.CodeAnalysis; using System; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Xml; namespace Jeffijoe.MessageFormat.MetadataGenerator { + [Generator] public class PluralLanguagesGenerator : ISourceGenerator { public void Execute(GeneratorExecutionContext context) { + var excludeLocales = ReadExcludeLocales(context); + var rules = GetRules(excludeLocales); + var generator = new PluralRulesMetadataGenerator(rules); + var sourceCode = generator.GenerateClass(); + + context.AddSource("PluralRulesMetadata.Generated.cs", sourceCode); + } + + private string[] ReadExcludeLocales(GeneratorExecutionContext context) + { + if(context.AnalyzerConfigOptions.GlobalOptions.TryGetValue($"build_property.PluralLanguagesMetadataExcludeLocales", out var value)) + { + var locales = value.Trim().Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); + return locales; + } + + return Array.Empty(); + } + + private PluralRule[] GetRules(string[] excludedLocales) + { + using var rulesStream = GetRulesContentStream(); + var xml = new XmlDocument(); + xml.Load(rulesStream); + + var parser = new PluralParser(xml, excludedLocales); + return parser.Parse().ToArray(); + } + + + private Stream GetRulesContentStream() + { + return typeof(PluralLanguagesGenerator).Assembly.GetManifestResourceStream("Jeffijoe.MessageFormat.MetadataGenerator.data.plurals.xml"); } public void Initialize(GeneratorInitializationContext context) diff --git a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/SourceGeneration/PluralRulesMetadataGenerator.cs b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/SourceGeneration/PluralRulesMetadataGenerator.cs new file mode 100644 index 0000000..2990d28 --- /dev/null +++ b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/SourceGeneration/PluralRulesMetadataGenerator.cs @@ -0,0 +1,91 @@ +using Jeffijoe.MessageFormat.MetadataGenerator.Plural.Parsing; + +using System.Text; + +namespace Jeffijoe.MessageFormat.MetadataGenerator.Plural.SourceGeneration +{ + public class PluralRulesMetadataGenerator + { + private readonly PluralRule[] _rules; + private readonly StringBuilder _sb; + private int _indent; + + public PluralRulesMetadataGenerator(PluralRule[] rules) + { + _rules = rules; + _sb = new StringBuilder(); + } + + public string GenerateClass() + { + Write("using System;"); + Write("using System.Collections.Generic;"); + + Write("namespace Jeffijoe.MessageFormat.Formatting.Formatters"); + Write("{"); + _indent += 4; + + Write("public static partial class PluralRulesMetadata"); + Write("{"); + _indent += 4; + + for(var ruleIdx = 0; ruleIdx < _rules.Length; ruleIdx++) + { + var rule = _rules[ruleIdx]; + + var ruleGenerator = new RuleGenerator(rule); + + foreach(var locale in rule.Locales) + { + Write($"private static string Locale_{locale.ToUpper()}(double value)"); + Write("{"); + _indent += 4; + + Write($"return Rule{ruleIdx}(value);"); + + _indent -= 4; + Write("}"); + } + + Write($"private static string Rule{ruleIdx}(double value)"); + Write("{"); + _indent += 4; + ruleGenerator.WriteTo(_sb, _indent); + _indent -= 4; + Write("}"); + } + + Write("public static partial void AddAllRules(IDictionary pluralizers)"); + Write("{"); + _indent += 4; + + for (int ruleIdx = 0; ruleIdx < _rules.Length; ruleIdx++) + { + PluralRule rule = _rules[ruleIdx]; + foreach (var locale in rule.Locales) + { + Write($"pluralizers.Add(\"{locale}\", (Pluralizer) Rule{ruleIdx});"); + } + + Write(string.Empty); + } + + _indent -= 4; + Write("}"); + + _indent -= 4; + Write("}"); + + _indent -= 4; + Write("}"); + + return _sb.ToString(); + } + + private void Write(string line) + { + _sb.Append(' ', _indent); + _sb.AppendLine(line); + } + } +} diff --git a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/SourceGeneration/RuleGenerator.cs b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/SourceGeneration/RuleGenerator.cs index 32bf790..99d3cf8 100644 --- a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/SourceGeneration/RuleGenerator.cs +++ b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/SourceGeneration/RuleGenerator.cs @@ -9,34 +9,36 @@ namespace Jeffijoe.MessageFormat.MetadataGenerator.Plural.SourceGeneration public class RuleGenerator { private PluralRule _rule; - private int _conditionNumber; private List _initializedSymbols; private int _innerIndent; public RuleGenerator(PluralRule rule) { _rule = rule; - _conditionNumber = 0; - HasNext = true; _initializedSymbols = new List(); _innerIndent = 0; } - public bool HasNext { get; private set; } - - public void WriteNext(StringBuilder builder, int indent) + public void WriteTo(StringBuilder builder, int indent) { - if(_conditionNumber > _rule.Conditions.Length - 1) + foreach(var condition in _rule.Conditions) { - if(_rule.Conditions.Length > 0) - WriteLine(builder, string.Empty, indent); - - WriteLine(builder, "return \"other\";", indent); - HasNext = false; - return; + WriteNext(condition, builder, indent); } - var condition = _rule.Conditions[_conditionNumber]; + WriteOther(builder, indent); + } + + private void WriteOther(StringBuilder builder, int indent) + { + if (_rule.Conditions.Length > 0) + WriteLine(builder, string.Empty, indent); + + WriteLine(builder, "return \"other\";", indent); + } + + private void WriteNext(Condition condition, StringBuilder builder, int indent) + { foreach (var operand in GetAllLeftOperands(condition.OrConditions)) { if(!_initializedSymbols.Contains(operand)) @@ -51,6 +53,7 @@ public void WriteNext(StringBuilder builder, int indent) if(condition.OrConditions.Length > 0) { + builder.Append(' ', _innerIndent + indent); builder.Append("if ("); for (int orIdx = 0; orIdx < condition.OrConditions.Length; orIdx++) @@ -76,9 +79,6 @@ public void WriteNext(StringBuilder builder, int indent) { throw new InvalidOperationException("Expected to have at least one or condition, but got none"); } - - - _conditionNumber++; } private void WriteOrCondition(StringBuilder builder, OrCondition orCondition) @@ -88,22 +88,34 @@ private void WriteOrCondition(StringBuilder builder, OrCondition orCondition) var andIsLast = andIdx == orCondition.AndConditions.Length - 1; Operation andCondition = orCondition.AndConditions[andIdx]; builder.Append('('); - var csharpOperator = andCondition.Relation == Relation.Equals ? "==" : "!="; for (int innerOrIdx = 0; innerOrIdx < andCondition.OperandRight.Length; innerOrIdx++) { var isLast = innerOrIdx == andCondition.OperandRight.Length - 1; - int number = andCondition.OperandRight[innerOrIdx]; - if (andCondition.OperandLeft is VariableOperand op) + var leftVariable = andCondition.OperandLeft switch { - var variable = OperandToVariable(op.Operand); - builder.Append($"{variable} {csharpOperator} {number}"); - } + VariableOperand op => OperandToVariable(op.Operand).ToString(), + ModuloOperand op => $"{OperandToVariable(op.Operand)} % {op.ModValue}", + var otherOp => throw new InvalidOperationException($"Unknown operation {otherOp.GetType()}") + }; + + var line = andCondition.OperandRight[innerOrIdx] switch + { + RangeOperand range => andCondition.Relation == Relation.Equals + ? $"{leftVariable} >= {range.Start} && {leftVariable} <= {range.End}" + : $"({leftVariable} < {range.Start} || {leftVariable} > {range.End})", + NumberOperand number => andCondition.Relation == Relation.Equals + ? $"{leftVariable} == {number.Number}" + : $"{leftVariable} != {number.Number}", + var otherOperand => throw new InvalidOperationException($"Unknown right operand {otherOperand.GetType()}") + }; + + builder.Append(line); if (!isLast) { - builder.Append(" || "); + builder.Append(andCondition.Relation == Relation.Equals ? " || " : " && "); } } builder.Append(')'); @@ -121,6 +133,7 @@ private char OperandToVariable(OperandSymbol operand) { OperandSymbol.AbsoluteValue => 'n', OperandSymbol.VisibleFractionDigitNumber => 'v', + OperandSymbol.IntegerDigits => 'i', _ => throw new InvalidOperationException($"Unknown variable {operand}") }; } @@ -131,6 +144,7 @@ private string InitializeValue(OperandSymbol operand) { OperandSymbol.AbsoluteValue => "var n = Math.Abs(value);", OperandSymbol.VisibleFractionDigitNumber => "var v = (int)value == value ? 0 : 1;", + OperandSymbol.IntegerDigits => "var i = (int)value;", var otherSymbol => throw new InvalidOperationException($"Unknown operand symbol {otherSymbol}") }; } diff --git a/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/ParserTests.cs b/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/ParserTests.cs index 4e47443..cdfae96 100644 --- a/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/ParserTests.cs +++ b/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/ParserTests.cs @@ -1,5 +1,6 @@ using Jeffijoe.MessageFormat.MetadataGenerator.Plural.Parsing; +using System; using System.Collections.Generic; using System.Xml; @@ -66,7 +67,21 @@ public void CanParseSingleCount_VisibleDigitsNumber() var condition = Assert.Single(rule.Conditions); var orCondition = Assert.Single(condition.OrConditions); var actual = Assert.Single(orCondition.AndConditions); - var expected = new Operation(new VariableOperand(OperandSymbol.VisibleFractionDigitNumber), Relation.Equals, new[] { 0 }); + var expected = new Operation(new VariableOperand(OperandSymbol.VisibleFractionDigitNumber), Relation.Equals, new[] { new NumberOperand(0) }); + + AssertOperationEqual(expected, actual); + } + + [Fact] + public void CanParseSingleCount_IntegerDigits() + { + var rules = ParseRules( + GenerateXmlWithRuleContent(@"i = 0 @integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, …")); + var rule = Assert.Single(rules); + var condition = Assert.Single(rule.Conditions); + var orCondition = Assert.Single(condition.OrConditions); + var actual = Assert.Single(orCondition.AndConditions); + var expected = new Operation(new VariableOperand(OperandSymbol.IntegerDigits), Relation.Equals, new[] { new NumberOperand(0) }); AssertOperationEqual(expected, actual); } @@ -80,7 +95,7 @@ public void CanParseSingleCount_AbsoluteNumber() var condition = Assert.Single(rule.Conditions); var orCondition = Assert.Single(condition.OrConditions); var actual = Assert.Single(orCondition.AndConditions); - var expected = new Operation(new VariableOperand(OperandSymbol.AbsoluteValue), Relation.Equals, new[] { 1 }); + var expected = new Operation(new VariableOperand(OperandSymbol.AbsoluteValue), Relation.Equals, new[] {new NumberOperand(1) }); AssertOperationEqual(expected, actual); } @@ -95,7 +110,7 @@ public void CanParseVariousRelations(string ruleText, Relation expectedRelation) var condition = Assert.Single(rule.Conditions); var orCondition = Assert.Single(condition.OrConditions); var actual = Assert.Single(orCondition.AndConditions); - var expected = new Operation(new VariableOperand(OperandSymbol.AbsoluteValue), expectedRelation, new[] { 2 }); + var expected = new Operation(new VariableOperand(OperandSymbol.AbsoluteValue), expectedRelation, new[] { new NumberOperand(2) }); AssertOperationEqual(expected, actual); } @@ -110,15 +125,15 @@ public void CanParseOrRules() Assert.Equal(3, condition.OrConditions.Length); var actualFirst = Assert.Single(condition.OrConditions[0].AndConditions); - var expectedFirst = new Operation(new VariableOperand(OperandSymbol.AbsoluteValue), Relation.Equals, new[] { 2 }); + var expectedFirst = new Operation(new VariableOperand(OperandSymbol.AbsoluteValue), Relation.Equals, new[] { new NumberOperand(2) }); AssertOperationEqual(expectedFirst, actualFirst); var actualSecond = Assert.Single(condition.OrConditions[1].AndConditions); - var expectedSecond = new Operation(new VariableOperand(OperandSymbol.AbsoluteValue), Relation.Equals, new[] { 1 }); + var expectedSecond = new Operation(new VariableOperand(OperandSymbol.AbsoluteValue), Relation.Equals, new[] { new NumberOperand(1) }); AssertOperationEqual(expectedSecond, actualSecond); var actualThird = Assert.Single(condition.OrConditions[2].AndConditions); - var expectedThird = new Operation(new VariableOperand(OperandSymbol.AbsoluteValue), Relation.Equals, new[] { 0 }); + var expectedThird = new Operation(new VariableOperand(OperandSymbol.AbsoluteValue), Relation.Equals, new[] { new NumberOperand(0) }); AssertOperationEqual(expectedThird, actualThird); } @@ -133,15 +148,15 @@ public void CanParseAndRules() Assert.Equal(3, orCondition.AndConditions.Length); var actualFirst = orCondition.AndConditions[0]; - var expectedFirst = new Operation(new VariableOperand(OperandSymbol.AbsoluteValue), Relation.Equals, new[] { 2 }); + var expectedFirst = new Operation(new VariableOperand(OperandSymbol.AbsoluteValue), Relation.Equals, new[] { new NumberOperand(2) }); AssertOperationEqual(expectedFirst, actualFirst); var actualSecond = orCondition.AndConditions[1]; - var expectedSecond = new Operation(new VariableOperand(OperandSymbol.AbsoluteValue), Relation.Equals, new[] { 1 }); + var expectedSecond = new Operation(new VariableOperand(OperandSymbol.AbsoluteValue), Relation.Equals, new[] { new NumberOperand(1) }); AssertOperationEqual(expectedSecond, actualSecond); var actualThird = orCondition.AndConditions[2]; - var expectedThird = new Operation(new VariableOperand(OperandSymbol.AbsoluteValue), Relation.Equals, new[] { 0 }); + var expectedThird = new Operation(new VariableOperand(OperandSymbol.AbsoluteValue), Relation.Equals, new[] { new NumberOperand(0) }); AssertOperationEqual(expectedThird, actualThird); } @@ -155,7 +170,7 @@ public void CanParseModuloInLeftOperator() var orCondition = Assert.Single(condition.OrConditions); var actual = Assert.Single(orCondition.AndConditions); var modulo = new ModuloOperand(OperandSymbol.AbsoluteValue, 5); - var expected = new Operation(modulo, Relation.Equals, new[] { 3 }); + var expected = new Operation(modulo, Relation.Equals, new[] { new NumberOperand(3) }); AssertOperationEqual(expected, actual); } @@ -169,7 +184,7 @@ public void CanParseRangeInRightOperator() var condition = Assert.Single(rule.Conditions); var orCondition = Assert.Single(condition.OrConditions); var actual = Assert.Single(orCondition.AndConditions); - var range = new[] { 3, 4, 5 }; + var range = new[] { new RangeOperand(3, 5) }; var expected = new Operation(new VariableOperand(OperandSymbol.AbsoluteValue), Relation.Equals, range); AssertOperationEqual(expected, actual); @@ -184,7 +199,7 @@ public void CanParseCommaSeparatedInRightOperator() var condition = Assert.Single(rule.Conditions); var orCondition = Assert.Single(condition.OrConditions); var actual = Assert.Single(orCondition.AndConditions); - var range = new[] { 3, 5, 8, 10 }; + var range = new[] { new NumberOperand(3), new NumberOperand(5), new NumberOperand(8), new NumberOperand(10) }; var expected = new Operation(new VariableOperand(OperandSymbol.AbsoluteValue), Relation.Equals, range); AssertOperationEqual(expected, actual); @@ -199,7 +214,7 @@ public void CanParseMixedCommaSeparatedAndRangeInRightOperator() var condition = Assert.Single(rule.Conditions); var orCondition = Assert.Single(condition.OrConditions); var actual = Assert.Single(orCondition.AndConditions); - var range = new[] { 3, 5, 6, 7, 12, 15 }; + var range = new IRightOperand[] { new NumberOperand(3), new RangeOperand(5, 7), new NumberOperand(12), new NumberOperand(15) }; var expected = new Operation(new VariableOperand(OperandSymbol.AbsoluteValue), Relation.Equals, range); AssertOperationEqual(expected, actual); @@ -250,7 +265,7 @@ private static IEnumerable ParseRules(string xmlText) var xml = new XmlDocument(); xml.LoadXml(xmlText); - var parser = new PluralParser(xml); + var parser = new PluralParser(xml, Array.Empty()); return parser.Parse(); } diff --git a/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/SourceGeneratingTests.cs b/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/SourceGeneratingTests.cs index 9a0552f..9f6a550 100644 --- a/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/SourceGeneratingTests.cs +++ b/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/SourceGeneratingTests.cs @@ -1,4 +1,5 @@ -using Jeffijoe.MessageFormat.MetadataGenerator.Plural.Parsing; +using Jeffijoe.MessageFormat.Formatting.Formatters; +using Jeffijoe.MessageFormat.MetadataGenerator.Plural.Parsing; using Jeffijoe.MessageFormat.MetadataGenerator.Plural.SourceGeneration; using System; @@ -29,7 +30,7 @@ public void CanGenerateRuleForFractionNumberEquals() { new OrCondition(new [] { - new Operation(new VariableOperand(OperandSymbol.VisibleFractionDigitNumber), Relation.Equals, new[] { 0 }) + new Operation(new VariableOperand(OperandSymbol.VisibleFractionDigitNumber), Relation.Equals, new[] { new NumberOperand(0) }) }) }) })); @@ -41,6 +42,58 @@ public void CanGenerateRuleForFractionNumberEquals() if ((v == 0)) return ""one""; +return ""other""; +".TrimStart(); + Assert.Equal(expected, actual); + } + + [Fact] + public void CanGenerateRuleForIntegerDigitsEquals() + { + var generator = new RuleGenerator(new PluralRule(new[] { "en" }, new[] + { + new Condition("one", string.Empty, new[] + { + new OrCondition(new [] + { + new Operation(new VariableOperand(OperandSymbol.IntegerDigits), Relation.Equals, new[] { new NumberOperand(1) }) + }) + }) + })); + + var actual = GenerateText(generator); + var expected = @$" +var i = (int)value; + +if ((i == 1)) + return ""one""; + +return ""other""; +".TrimStart(); + Assert.Equal(expected, actual); + } + + [Fact] + public void CanGenerateRuleForModuloEquals() + { + var generator = new RuleGenerator(new PluralRule(new[] { "en" }, new[] + { + new Condition("one", string.Empty, new[] + { + new OrCondition(new [] + { + new Operation(new ModuloOperand(OperandSymbol.IntegerDigits, 5), Relation.Equals, new[] { new NumberOperand(1) }) + }) + }) + })); + + var actual = GenerateText(generator); + var expected = @$" +var i = (int)value; + +if ((i % 5 == 1)) + return ""one""; + return ""other""; ".TrimStart(); Assert.Equal(expected, actual); @@ -55,7 +108,7 @@ public void CanGenerateRuleForNumberEquals() { new OrCondition(new [] { - new Operation(new VariableOperand(OperandSymbol.AbsoluteValue), Relation.Equals, new[] { 5 }) + new Operation(new VariableOperand(OperandSymbol.AbsoluteValue), Relation.Equals, new[] { new NumberOperand(5) }) }) }) })); @@ -81,7 +134,7 @@ public void CanGenerateRuleForNumberNotEquals() { new OrCondition(new [] { - new Operation(new VariableOperand(OperandSymbol.AbsoluteValue), Relation.NotEquals, new[] { 5 }) + new Operation(new VariableOperand(OperandSymbol.AbsoluteValue), Relation.NotEquals, new[] { new NumberOperand(5) }) }) }) })); @@ -107,7 +160,33 @@ public void CanGenerateRuleForNumberRange() { new OrCondition(new [] { - new Operation(new VariableOperand(OperandSymbol.AbsoluteValue), Relation.Equals, new[] { 5, 6, 10 }) + new Operation(new VariableOperand(OperandSymbol.AbsoluteValue), Relation.Equals, new IRightOperand[] { new RangeOperand(5, 6), new NumberOperand(10) }) + }) + }) + })); + + var actual = GenerateText(generator); + var expected = @$" +var n = Math.Abs(value); + +if ((n >= 5 && n <= 6 || n == 10)) + return ""one""; + +return ""other""; +".TrimStart(); + Assert.Equal(expected, actual); + } + + [Fact] + public void CanGenerateRuleForNegativeNumberRange() + { + var generator = new RuleGenerator(new PluralRule(new[] { "en" }, new[] + { + new Condition("one", string.Empty, new[] + { + new OrCondition(new [] + { + new Operation(new VariableOperand(OperandSymbol.AbsoluteValue), Relation.NotEquals, new IRightOperand[] { new RangeOperand(5, 6), new NumberOperand(10) }) }) }) })); @@ -116,7 +195,7 @@ public void CanGenerateRuleForNumberRange() var expected = @$" var n = Math.Abs(value); -if ((n == 5 || n == 6 || n == 10)) +if (((n < 5 || n > 6) && n != 10)) return ""one""; return ""other""; @@ -133,8 +212,8 @@ public void CanGenerateRuleForAndRules() { new OrCondition(new [] { - new Operation(new VariableOperand(OperandSymbol.AbsoluteValue), Relation.Equals, new[] { 4 }), - new Operation(new VariableOperand(OperandSymbol.VisibleFractionDigitNumber), Relation.Equals, new[] { 0 }) + new Operation(new VariableOperand(OperandSymbol.AbsoluteValue), Relation.Equals, new[] { new NumberOperand(4) }), + new Operation(new VariableOperand(OperandSymbol.VisibleFractionDigitNumber), Relation.Equals, new[] { new NumberOperand(0) }) }) }) })); @@ -161,8 +240,8 @@ public void CanGenerateRuleForMixedRangeAndNumberRangeRules() { new OrCondition(new [] { - new Operation(new VariableOperand(OperandSymbol.AbsoluteValue), Relation.Equals, new[] { 4, 5 }), - new Operation(new VariableOperand(OperandSymbol.VisibleFractionDigitNumber), Relation.Equals, new[] { 0 }) + new Operation(new VariableOperand(OperandSymbol.AbsoluteValue), Relation.Equals, new[] { new RangeOperand(4, 5) }), + new Operation(new VariableOperand(OperandSymbol.VisibleFractionDigitNumber), Relation.Equals, new[] { new NumberOperand(0) }) }) }) })); @@ -172,7 +251,7 @@ public void CanGenerateRuleForMixedRangeAndNumberRangeRules() var n = Math.Abs(value); var v = (int)value == value ? 0 : 1; -if ((n == 4 || n == 5) && (v == 0)) +if ((n >= 4 && n <= 5) && (v == 0)) return ""one""; return ""other""; @@ -189,11 +268,11 @@ public void CanGenerateRuleForMultipleOrRules() { new OrCondition(new [] { - new Operation(new VariableOperand(OperandSymbol.AbsoluteValue), Relation.Equals, new[] { 4 }) + new Operation(new VariableOperand(OperandSymbol.AbsoluteValue), Relation.Equals, new[] { new NumberOperand(4) }) }), new OrCondition(new [] { - new Operation(new VariableOperand(OperandSymbol.VisibleFractionDigitNumber), Relation.Equals, new[] { 0 }) + new Operation(new VariableOperand(OperandSymbol.VisibleFractionDigitNumber), Relation.Equals, new[] { new NumberOperand(0) }) }) }) })); @@ -220,12 +299,12 @@ public void CanGenerateRuleForMixedAndOrRules() { new OrCondition(new [] { - new Operation(new VariableOperand(OperandSymbol.AbsoluteValue), Relation.Equals, new[] { 4 }), - new Operation(new VariableOperand(OperandSymbol.VisibleFractionDigitNumber), Relation.NotEquals, new[] { 0 }) + new Operation(new VariableOperand(OperandSymbol.AbsoluteValue), Relation.Equals, new[] { new NumberOperand(4) }), + new Operation(new VariableOperand(OperandSymbol.VisibleFractionDigitNumber), Relation.NotEquals, new[] { new NumberOperand(0) }) }), new OrCondition(new [] { - new Operation(new VariableOperand(OperandSymbol.VisibleFractionDigitNumber), Relation.Equals, new[] { 0 }) + new Operation(new VariableOperand(OperandSymbol.VisibleFractionDigitNumber), Relation.Equals, new[] { new NumberOperand(0) }) }) }) })); @@ -252,12 +331,12 @@ public void CanGenerateRuleForMixedAndOrRangeRules() { new OrCondition(new [] { - new Operation(new VariableOperand(OperandSymbol.AbsoluteValue), Relation.Equals, new[] { 4, 5 }), - new Operation(new VariableOperand(OperandSymbol.VisibleFractionDigitNumber), Relation.NotEquals, new[] { 0 }) + new Operation(new VariableOperand(OperandSymbol.AbsoluteValue), Relation.Equals, new[] { new RangeOperand(4, 5) }), + new Operation(new VariableOperand(OperandSymbol.VisibleFractionDigitNumber), Relation.NotEquals, new[] { new NumberOperand(0) }) }), new OrCondition(new [] { - new Operation(new VariableOperand(OperandSymbol.VisibleFractionDigitNumber), Relation.Equals, new[] { 0 }) + new Operation(new VariableOperand(OperandSymbol.VisibleFractionDigitNumber), Relation.Equals, new[] { new NumberOperand(0) }) }) }) })); @@ -267,7 +346,7 @@ public void CanGenerateRuleForMixedAndOrRangeRules() var n = Math.Abs(value); var v = (int)value == value ? 0 : 1; -if ((n == 4 || n == 5) && (v != 0) || (v == 0)) +if ((n >= 4 && n <= 5) && (v != 0) || (v == 0)) return ""one""; return ""other""; @@ -278,10 +357,8 @@ public void CanGenerateRuleForMixedAndOrRangeRules() private string GenerateText(RuleGenerator generator) { var sb = new StringBuilder(); - while(generator.HasNext) - { - generator.WriteNext(sb, 0); - } + + generator.WriteTo(sb, 0); return sb.ToString(); } diff --git a/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralFormatter.cs b/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralFormatter.cs index eac0e47..10a38ed 100644 --- a/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralFormatter.cs +++ b/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralFormatter.cs @@ -293,6 +293,8 @@ private void AddStandardPluralizers() // ReSharper restore CompareOfFloatsByEqualityOperator return "other"; }); + + PluralRulesMetadata.AddAllRules(this.Pluralizers); } #endregion diff --git a/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralRulesMetadata.cs b/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralRulesMetadata.cs index b989134..0a20c8f 100644 --- a/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralRulesMetadata.cs +++ b/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralRulesMetadata.cs @@ -20,5 +20,7 @@ public static string DefaultPluralRule(double number) return "other"; } + + public static partial void AddAllRules(IDictionary pluralizers); } } diff --git a/src/Jeffijoe.MessageFormat/Jeffijoe.MessageFormat.csproj b/src/Jeffijoe.MessageFormat/Jeffijoe.MessageFormat.csproj index 573ac9e..e7d12a3 100644 --- a/src/Jeffijoe.MessageFormat/Jeffijoe.MessageFormat.csproj +++ b/src/Jeffijoe.MessageFormat/Jeffijoe.MessageFormat.csproj @@ -10,11 +10,11 @@ https://github.com/jeffijoe/messageformat.net latest enable - netstandard1.1 + netstandard2.0 - bin/Release/netstandard1.1/Jeffijoe.MessageFormat.xml + bin/Release/netstandard2.0/Jeffijoe.MessageFormat.xml @@ -25,6 +25,15 @@ - + + + + si da is mk ceb fil tl lv prg bs hr sh sr fr dsb hsb lt + + + + + + From 1f75545eafbb147ee5cd420d2691a66e6338ed3e Mon Sep 17 00:00:00 2001 From: Kostiantyn Sharovarskyi Date: Thu, 1 Apr 2021 03:06:55 +0300 Subject: [PATCH 18/98] Make locale functions public --- .../Plural/SourceGeneration/PluralRulesMetadataGenerator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/SourceGeneration/PluralRulesMetadataGenerator.cs b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/SourceGeneration/PluralRulesMetadataGenerator.cs index 2990d28..f237ac2 100644 --- a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/SourceGeneration/PluralRulesMetadataGenerator.cs +++ b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/SourceGeneration/PluralRulesMetadataGenerator.cs @@ -37,7 +37,7 @@ public string GenerateClass() foreach(var locale in rule.Locales) { - Write($"private static string Locale_{locale.ToUpper()}(double value)"); + Write($"public static string Locale_{locale.ToUpper()}(double value)"); Write("{"); _indent += 4; From d5433776adc26e1f5ea830b789e8e05fca5d01fd Mon Sep 17 00:00:00 2001 From: Kostiantyn Sharovarskyi Date: Thu, 1 Apr 2021 17:15:21 +0300 Subject: [PATCH 19/98] Extract helpers for indents --- .../PluralRulesMetadataGenerator.cs | 78 +++++++++++-------- .../Formatting/Formatters/PluralFormatter.cs | 7 +- .../Formatters/PluralRulesMetadata.cs | 2 +- 3 files changed, 49 insertions(+), 38 deletions(-) diff --git a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/SourceGeneration/PluralRulesMetadataGenerator.cs b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/SourceGeneration/PluralRulesMetadataGenerator.cs index f237ac2..3c92ba8 100644 --- a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/SourceGeneration/PluralRulesMetadataGenerator.cs +++ b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/SourceGeneration/PluralRulesMetadataGenerator.cs @@ -18,18 +18,18 @@ public PluralRulesMetadataGenerator(PluralRule[] rules) public string GenerateClass() { - Write("using System;"); - Write("using System.Collections.Generic;"); + WriteLine("using System;"); + WriteLine("using System.Collections.Generic;"); - Write("namespace Jeffijoe.MessageFormat.Formatting.Formatters"); - Write("{"); - _indent += 4; + WriteLine("namespace Jeffijoe.MessageFormat.Formatting.Formatters"); + WriteLine("{"); + AddIndent(); - Write("public static partial class PluralRulesMetadata"); - Write("{"); - _indent += 4; + WriteLine("public static partial class PluralRulesMetadata"); + WriteLine("{"); + AddIndent(); - for(var ruleIdx = 0; ruleIdx < _rules.Length; ruleIdx++) + for (var ruleIdx = 0; ruleIdx < _rules.Length; ruleIdx++) { var rule = _rules[ruleIdx]; @@ -37,52 +37,62 @@ public string GenerateClass() foreach(var locale in rule.Locales) { - Write($"public static string Locale_{locale.ToUpper()}(double value)"); - Write("{"); - _indent += 4; - - Write($"return Rule{ruleIdx}(value);"); - - _indent -= 4; - Write("}"); + WriteLine($"public static string Locale_{locale.ToUpper()}(double value) => Rule{ruleIdx}(value);"); + WriteLine(string.Empty); } - Write($"private static string Rule{ruleIdx}(double value)"); - Write("{"); - _indent += 4; + WriteLine($"private static string Rule{ruleIdx}(double value)"); + WriteLine("{"); + AddIndent(); + ruleGenerator.WriteTo(_sb, _indent); - _indent -= 4; - Write("}"); + + DecreaseIndent(); + WriteLine("}"); + WriteLine(string.Empty); } - Write("public static partial void AddAllRules(IDictionary pluralizers)"); - Write("{"); - _indent += 4; + WriteLine("private static readonly Dictionary Pluralizers = new Dictionary()"); + WriteLine("{"); + AddIndent(); for (int ruleIdx = 0; ruleIdx < _rules.Length; ruleIdx++) { PluralRule rule = _rules[ruleIdx]; foreach (var locale in rule.Locales) { - Write($"pluralizers.Add(\"{locale}\", (Pluralizer) Rule{ruleIdx});"); + WriteLine($"{{\"{locale}\", (Pluralizer) Rule{ruleIdx}}},"); } - Write(string.Empty); + WriteLine(string.Empty); } - _indent -= 4; - Write("}"); + DecreaseIndent(); + WriteLine("};"); + WriteLine(string.Empty); - _indent -= 4; - Write("}"); + WriteLine("public static partial bool TryGetRuleByLocale(string locale, out Pluralizer pluralizer)"); + WriteLine("{"); + AddIndent(); - _indent -= 4; - Write("}"); + WriteLine("return Pluralizers.TryGetValue(locale, out pluralizer);"); + + DecreaseIndent(); + WriteLine("}"); + + DecreaseIndent(); + WriteLine("}"); + + DecreaseIndent(); + WriteLine("}"); return _sb.ToString(); } - private void Write(string line) + private void AddIndent() => _indent += 4; + private void DecreaseIndent() => _indent -= 4; + + private void WriteLine(string line) { _sb.Append(' ', _indent); _sb.AppendLine(line); diff --git a/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralFormatter.cs b/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralFormatter.cs index 10a38ed..e2f74c8 100644 --- a/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralFormatter.cs +++ b/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralFormatter.cs @@ -139,7 +139,10 @@ internal string Pluralize(string locale, ParsedArguments arguments, double n, do Pluralizer pluralizer; if (this.Pluralizers.TryGetValue(locale, out pluralizer) == false) { - pluralizer = this.Pluralizers["en"]; + if(PluralRulesMetadata.TryGetRuleByLocale(locale, out pluralizer) == false) + { + pluralizer = this.Pluralizers["en"]; + } } var pluralForm = pluralizer(n - offset); @@ -293,8 +296,6 @@ private void AddStandardPluralizers() // ReSharper restore CompareOfFloatsByEqualityOperator return "other"; }); - - PluralRulesMetadata.AddAllRules(this.Pluralizers); } #endregion diff --git a/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralRulesMetadata.cs b/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralRulesMetadata.cs index 0a20c8f..226d123 100644 --- a/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralRulesMetadata.cs +++ b/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralRulesMetadata.cs @@ -21,6 +21,6 @@ public static string DefaultPluralRule(double number) return "other"; } - public static partial void AddAllRules(IDictionary pluralizers); + public static partial bool TryGetRuleByLocale(string locale, out Pluralizer pluralizer); } } From afe7e0eba9ee6bb2844d5429f5b982b497c24f30 Mon Sep 17 00:00:00 2001 From: Kostiantyn Sharovarskyi Date: Thu, 1 Apr 2021 17:17:50 +0300 Subject: [PATCH 20/98] Move equals logic to left operand --- .../Plural/Parsing/LeftOperand.cs | 26 +++++++++++++++++++ .../MetadataGenerator/ParserTests.cs | 22 +--------------- 2 files changed, 27 insertions(+), 21 deletions(-) diff --git a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/LeftOperand.cs b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/LeftOperand.cs index f66dcb7..3325cc8 100644 --- a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/LeftOperand.cs +++ b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/LeftOperand.cs @@ -12,6 +12,19 @@ public VariableOperand(OperandSymbol operand) } public OperandSymbol Operand { get; } + + public override bool Equals(object obj) + { + if (obj is VariableOperand op) + return op.Operand == Operand; + + return base.Equals(obj); + } + + public override int GetHashCode() + { + return Operand.GetHashCode(); + } } public class ModuloOperand : ILeftOperand @@ -24,5 +37,18 @@ public ModuloOperand(OperandSymbol operandSymbol, int modValue) public OperandSymbol Operand { get; } public int ModValue { get; } + + public override bool Equals(object obj) + { + if (obj is ModuloOperand op) + return op.Operand == Operand && op.ModValue == ModValue; + + return base.Equals(obj); + } + + public override int GetHashCode() + { + return Operand.GetHashCode() + ModValue.GetHashCode(); + } } } diff --git a/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/ParserTests.cs b/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/ParserTests.cs index cdfae96..b21c233 100644 --- a/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/ParserTests.cs +++ b/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/ParserTests.cs @@ -235,31 +235,11 @@ private static string GenerateXmlWithRuleContent(string ruleText) private static void AssertOperationEqual(Operation expected, Operation actual) { - AssertLeftOperandEqual(expected.OperandLeft, actual.OperandLeft); + Assert.Equal(expected.OperandLeft, actual.OperandLeft); Assert.Equal(expected.Relation, actual.Relation); Assert.Equal(expected.OperandRight, actual.OperandRight); } - private static void AssertLeftOperandEqual(ILeftOperand expectedOperand, ILeftOperand actualOperand) - { - switch (expectedOperand, actualOperand) - { - case (VariableOperand expected, VariableOperand actual): - { - Assert.Equal(expected.Operand, actual.Operand); - } break; - case (ModuloOperand expected, ModuloOperand actual): - { - Assert.Equal(expected.Operand, actual.Operand); - Assert.Equal(expected.ModValue, actual.ModValue); - } break; - default: - { - Assert.False(true, $"Received unexpected operand types expected={expectedOperand.GetType()} actual={actualOperand.GetType()}"); - } break; - } - } - private static IEnumerable ParseRules(string xmlText) { var xml = new XmlDocument(); From 3942a3ff0023d26cebf1ac28665ad6baf4e0d4c7 Mon Sep 17 00:00:00 2001 From: Kostiantyn Sharovarskyi Date: Thu, 1 Apr 2021 17:32:45 +0300 Subject: [PATCH 21/98] Add test for full source code --- .../PluralMetadataClassGeneratorTests.cs | 70 +++++++++++++++++++ ...ngTests.cs => RuleSourceGeneratorTests.cs} | 2 +- 2 files changed, 71 insertions(+), 1 deletion(-) create mode 100644 src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/PluralMetadataClassGeneratorTests.cs rename src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/{SourceGeneratingTests.cs => RuleSourceGeneratorTests.cs} (99%) diff --git a/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/PluralMetadataClassGeneratorTests.cs b/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/PluralMetadataClassGeneratorTests.cs new file mode 100644 index 0000000..a6caecd --- /dev/null +++ b/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/PluralMetadataClassGeneratorTests.cs @@ -0,0 +1,70 @@ +using Jeffijoe.MessageFormat.MetadataGenerator.Plural.Parsing; +using Jeffijoe.MessageFormat.MetadataGenerator.Plural.SourceGeneration; + +using Xunit; + +namespace Jeffijoe.MessageFormat.Tests.MetadataGenerator +{ + public class PluralMetadataClassGeneratorTests + { + [Fact] + public void CanGenerateClassFromRules() + { + var rules = new[] + { + new PluralRule(new[] {"en", "uk"}, + new[] + { + new Condition("one", string.Empty, new [] + { + new OrCondition(new[] + { + new Operation(new VariableOperand(OperandSymbol.AbsoluteValue), Relation.Equals, new[] {new NumberOperand(3) }) + }) + }) + }) + }; + var generator = new PluralRulesMetadataGenerator(rules); + + var actual = generator.GenerateClass(); + + var expected = @" +using System; +using System.Collections.Generic; +namespace Jeffijoe.MessageFormat.Formatting.Formatters +{ + public static partial class PluralRulesMetadata + { + public static string Locale_EN(double value) => Rule0(value); + + public static string Locale_UK(double value) => Rule0(value); + + private static string Rule0(double value) + { + var n = Math.Abs(value); + + if ((n == 3)) + return ""one""; + + return ""other""; + } + + private static readonly Dictionary Pluralizers = new Dictionary() + { + {""en"", (Pluralizer) Rule0}, + {""uk"", (Pluralizer) Rule0}, + + }; + + public static partial bool TryGetRuleByLocale(string locale, out Pluralizer pluralizer) + { + return Pluralizers.TryGetValue(locale, out pluralizer); + } + } +} +".TrimStart(); + + Assert.Equal(expected, actual); + } + } +} diff --git a/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/SourceGeneratingTests.cs b/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/RuleSourceGeneratorTests.cs similarity index 99% rename from src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/SourceGeneratingTests.cs rename to src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/RuleSourceGeneratorTests.cs index 9f6a550..53a310b 100644 --- a/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/SourceGeneratingTests.cs +++ b/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/RuleSourceGeneratorTests.cs @@ -9,7 +9,7 @@ namespace Jeffijoe.MessageFormat.Tests.MetadataGenerator { - public class SourceGeneratingTests + public class RuleSourceGeneratorTests { [Fact] public void CanGenerateEmptyRule() From cec254a30de90dd4319cef76da4e8365114e34bc Mon Sep 17 00:00:00 2001 From: Kostiantyn Sharovarskyi Date: Thu, 1 Apr 2021 18:37:01 +0300 Subject: [PATCH 22/98] Bump SDK version --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c1ef719..c75e97f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,7 +25,7 @@ jobs: - name: Setup .NET Core uses: actions/setup-dotnet@v1 with: - dotnet-version: 3.1.x + dotnet-version: 5.0.x - name: Install dependencies working-directory: ./src From 0f56f024207e548fa23b1541488167aaa8cf3a4e Mon Sep 17 00:00:00 2001 From: Kostiantyn Sharovarskyi Date: Thu, 1 Apr 2021 18:38:45 +0300 Subject: [PATCH 23/98] Update test project version --- .../Jeffijoe.MessageFormat.Tests.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Jeffijoe.MessageFormat.Tests/Jeffijoe.MessageFormat.Tests.csproj b/src/Jeffijoe.MessageFormat.Tests/Jeffijoe.MessageFormat.Tests.csproj index d187690..608711b 100644 --- a/src/Jeffijoe.MessageFormat.Tests/Jeffijoe.MessageFormat.Tests.csproj +++ b/src/Jeffijoe.MessageFormat.Tests/Jeffijoe.MessageFormat.Tests.csproj @@ -1,7 +1,7 @@  - netcoreapp3.1 + net5.0 True MessageFormat.snk False From 77915c933a58e28f1f7029c8a362683324f58261 Mon Sep 17 00:00:00 2001 From: Kostiantyn Sharovarskyi Date: Sat, 3 Apr 2021 00:49:27 +0300 Subject: [PATCH 24/98] Remove not used code --- .../Formatting/Formatters/PluralRulesMetadata.cs | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralRulesMetadata.cs b/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralRulesMetadata.cs index 226d123..0ac105f 100644 --- a/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralRulesMetadata.cs +++ b/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralRulesMetadata.cs @@ -6,21 +6,6 @@ namespace Jeffijoe.MessageFormat.Formatting.Formatters { public static partial class PluralRulesMetadata { - public static string DefaultPluralRule(double number) - { - if(number == 0) - { - return "zero"; - } - - if(number == 1) - { - return "one"; - } - - return "other"; - } - public static partial bool TryGetRuleByLocale(string locale, out Pluralizer pluralizer); } } From b787534f41d8d6a492e60edaed736cb389ddf735 Mon Sep 17 00:00:00 2001 From: Kostiantyn Sharovarskyi Date: Sat, 3 Apr 2021 14:20:52 +0300 Subject: [PATCH 25/98] Use proper null character for EOF, use appropriate FormatException for formatting errors --- .../Plural/Parsing/InvalidCharacterException.cs | 2 +- .../Plural/Parsing/RuleParser.cs | 13 ++++++++----- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/InvalidCharacterException.cs b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/InvalidCharacterException.cs index dc03017..d9d9f0c 100644 --- a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/InvalidCharacterException.cs +++ b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/InvalidCharacterException.cs @@ -2,7 +2,7 @@ namespace Jeffijoe.MessageFormat.MetadataGenerator.Plural.Parsing { - public class InvalidCharacterException : ArgumentException + public class InvalidCharacterException : FormatException { public InvalidCharacterException(char character) : base($"Invalid format, do not recognise character '{character}'") { diff --git a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/RuleParser.cs b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/RuleParser.cs index 0a6f8ce..604f6a1 100644 --- a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/RuleParser.cs +++ b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/RuleParser.cs @@ -50,20 +50,22 @@ public OrCondition[] ParseRuleContent() return conditions.ToArray(); } + private static readonly char NullCharacter = '\0'; + private char PeekCurrentChar => _position < _ruleText.Length ? _ruleText[_position] - : '0'; + : NullCharacter; private char PeekNextChar => _position + 1 < _ruleText.Length ? _ruleText[_position + 1] - : '0'; + : NullCharacter; private char PeekAt(int delta) { if (_position + delta > _ruleText.Length) - return '0'; + return NullCharacter; return _ruleText[_position + delta]; } @@ -87,14 +89,15 @@ private ReadOnlySpan ConsumeCharacters(int count) private char ConsumeChar() { if (IsEnd) - return '0'; + return NullCharacter; var character = PeekCurrentChar; _position++; return character; } - private bool IsEnd => _position >= _ruleText.Length; + private bool IsEnd => _position >= _ruleText.Length + || PeekCurrentChar == NullCharacter; private void AdvanceWhitespace() { From cec16c3a6bbffb3c7accfdfbb3f9641e80e5f76b Mon Sep 17 00:00:00 2001 From: Kostiantyn Sharovarskyi Date: Sat, 3 Apr 2021 18:14:46 +0300 Subject: [PATCH 26/98] Remove duplicate logic for IsEnd check, as PeekCurrentChar does absolutely the same check --- .../Plural/Parsing/RuleParser.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/RuleParser.cs b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/RuleParser.cs index 604f6a1..20e71ff 100644 --- a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/RuleParser.cs +++ b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/RuleParser.cs @@ -96,8 +96,7 @@ private char ConsumeChar() return character; } - private bool IsEnd => _position >= _ruleText.Length - || PeekCurrentChar == NullCharacter; + private bool IsEnd => _position >= _ruleText.Length; private void AdvanceWhitespace() { From 49b4652a7712a7a4660bb3566c27a8ae5e01b2c7 Mon Sep 17 00:00:00 2001 From: Kostiantyn Sharovarskyi Date: Thu, 8 Apr 2021 19:33:26 +0300 Subject: [PATCH 27/98] Address feedback Move AST to a separate folder, move classes into separate files. Use parametrized expression for documentation file. Apply simplification for ParseRuleContent. Remove 'and' calculation out of loop. Simplify number construction. --- .../Plural/Parsing/{ => AST}/Condition.cs | 2 +- .../Plural/Parsing/AST/ILeftOperand.cs | 6 +++ .../Plural/Parsing/AST/IRightOperand.cs | 6 +++ .../Plural/Parsing/{ => AST}/LeftOperand.cs | 29 +------------- .../Plural/Parsing/AST/NumberOperand.cs | 25 ++++++++++++ .../Plural/Parsing/{ => AST}/OperandSymbol.cs | 2 +- .../Plural/Parsing/{ => AST}/Operation.cs | 2 +- .../Plural/Parsing/{ => AST}/OrCondition.cs | 2 +- .../Plural/Parsing/{ => AST}/PluralRule.cs | 2 +- .../{RightOperand.cs => AST/RangeOperand.cs} | 29 +------------- .../Plural/Parsing/{ => AST}/Relation.cs | 2 +- .../Plural/Parsing/AST/VariableOperand.cs | 25 ++++++++++++ .../Plural/Parsing/PluralParser.cs | 4 +- .../Plural/Parsing/RuleParser.cs | 39 ++++++++----------- .../Plural/PluralLanguagesGenerator.cs | 4 +- .../PluralRulesMetadataGenerator.cs | 5 +-- .../Plural/SourceGeneration/RuleGenerator.cs | 5 +-- .../MetadataGenerator/ParserTests.cs | 1 + .../PluralMetadataClassGeneratorTests.cs | 2 +- .../RuleSourceGeneratorTests.cs | 5 +-- .../Jeffijoe.MessageFormat.csproj | 2 +- 21 files changed, 99 insertions(+), 100 deletions(-) rename src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/{ => AST}/Condition.cs (98%) create mode 100644 src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/ILeftOperand.cs create mode 100644 src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/IRightOperand.cs rename src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/{ => AST}/LeftOperand.cs (56%) create mode 100644 src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/NumberOperand.cs rename src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/{ => AST}/OperandSymbol.cs (98%) rename src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/{ => AST}/Operation.cs (98%) rename src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/{ => AST}/OrCondition.cs (96%) rename src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/{ => AST}/PluralRule.cs (97%) rename src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/{RightOperand.cs => AST/RangeOperand.cs} (54%) rename src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/{ => AST}/Relation.cs (93%) create mode 100644 src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/VariableOperand.cs diff --git a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/Condition.cs b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/Condition.cs similarity index 98% rename from src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/Condition.cs rename to src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/Condition.cs index a4d5dcf..00f75c5 100644 --- a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/Condition.cs +++ b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/Condition.cs @@ -1,6 +1,6 @@ using System.Diagnostics; -namespace Jeffijoe.MessageFormat.MetadataGenerator.Plural.Parsing +namespace Jeffijoe.MessageFormat.MetadataGenerator.Plural.Parsing.AST { [DebuggerDisplay("{{RuleDescription}}")] public class Condition diff --git a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/ILeftOperand.cs b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/ILeftOperand.cs new file mode 100644 index 0000000..b37db90 --- /dev/null +++ b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/ILeftOperand.cs @@ -0,0 +1,6 @@ +namespace Jeffijoe.MessageFormat.MetadataGenerator.Plural.Parsing.AST +{ + public interface ILeftOperand + { + } +} \ No newline at end of file diff --git a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/IRightOperand.cs b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/IRightOperand.cs new file mode 100644 index 0000000..96eeffe --- /dev/null +++ b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/IRightOperand.cs @@ -0,0 +1,6 @@ +namespace Jeffijoe.MessageFormat.MetadataGenerator.Plural.Parsing.AST +{ + public interface IRightOperand + { + } +} \ No newline at end of file diff --git a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/LeftOperand.cs b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/LeftOperand.cs similarity index 56% rename from src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/LeftOperand.cs rename to src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/LeftOperand.cs index 3325cc8..d42e5dd 100644 --- a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/LeftOperand.cs +++ b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/LeftOperand.cs @@ -1,32 +1,5 @@ -namespace Jeffijoe.MessageFormat.MetadataGenerator.Plural.Parsing +namespace Jeffijoe.MessageFormat.MetadataGenerator.Plural.Parsing.AST { - public interface ILeftOperand - { - } - - public class VariableOperand : ILeftOperand - { - public VariableOperand(OperandSymbol operand) - { - Operand = operand; - } - - public OperandSymbol Operand { get; } - - public override bool Equals(object obj) - { - if (obj is VariableOperand op) - return op.Operand == Operand; - - return base.Equals(obj); - } - - public override int GetHashCode() - { - return Operand.GetHashCode(); - } - } - public class ModuloOperand : ILeftOperand { public ModuloOperand(OperandSymbol operandSymbol, int modValue) diff --git a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/NumberOperand.cs b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/NumberOperand.cs new file mode 100644 index 0000000..2a0138a --- /dev/null +++ b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/NumberOperand.cs @@ -0,0 +1,25 @@ +namespace Jeffijoe.MessageFormat.MetadataGenerator.Plural.Parsing.AST +{ + public class NumberOperand : IRightOperand + { + public NumberOperand(int number) + { + Number = number; + } + + public int Number { get; } + + public override bool Equals(object obj) + { + if (obj is NumberOperand n) + return n.Number == Number; + + return base.Equals(obj); + } + + public override int GetHashCode() + { + return Number.GetHashCode(); + } + } +} \ No newline at end of file diff --git a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/OperandSymbol.cs b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/OperandSymbol.cs similarity index 98% rename from src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/OperandSymbol.cs rename to src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/OperandSymbol.cs index 6920149..75571a4 100644 --- a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/OperandSymbol.cs +++ b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/OperandSymbol.cs @@ -1,4 +1,4 @@ -namespace Jeffijoe.MessageFormat.MetadataGenerator.Plural.Parsing +namespace Jeffijoe.MessageFormat.MetadataGenerator.Plural.Parsing.AST { public enum OperandSymbol { diff --git a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/Operation.cs b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/Operation.cs similarity index 98% rename from src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/Operation.cs rename to src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/Operation.cs index 7a1c9da..8ed2388 100644 --- a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/Operation.cs +++ b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/Operation.cs @@ -1,4 +1,4 @@ -namespace Jeffijoe.MessageFormat.MetadataGenerator.Plural.Parsing +namespace Jeffijoe.MessageFormat.MetadataGenerator.Plural.Parsing.AST { public class Operation { diff --git a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/OrCondition.cs b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/OrCondition.cs similarity index 96% rename from src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/OrCondition.cs rename to src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/OrCondition.cs index 915ac38..e03c35e 100644 --- a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/OrCondition.cs +++ b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/OrCondition.cs @@ -1,4 +1,4 @@ -namespace Jeffijoe.MessageFormat.MetadataGenerator.Plural.Parsing +namespace Jeffijoe.MessageFormat.MetadataGenerator.Plural.Parsing.AST { public class OrCondition { diff --git a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/PluralRule.cs b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/PluralRule.cs similarity index 97% rename from src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/PluralRule.cs rename to src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/PluralRule.cs index a50a8c1..d5de89b 100644 --- a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/PluralRule.cs +++ b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/PluralRule.cs @@ -1,4 +1,4 @@ -namespace Jeffijoe.MessageFormat.MetadataGenerator.Plural.Parsing +namespace Jeffijoe.MessageFormat.MetadataGenerator.Plural.Parsing.AST { public class PluralRule { diff --git a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/RightOperand.cs b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/RangeOperand.cs similarity index 54% rename from src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/RightOperand.cs rename to src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/RangeOperand.cs index 7882f0c..681f52a 100644 --- a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/RightOperand.cs +++ b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/RangeOperand.cs @@ -1,32 +1,5 @@ -namespace Jeffijoe.MessageFormat.MetadataGenerator.Plural.Parsing +namespace Jeffijoe.MessageFormat.MetadataGenerator.Plural.Parsing.AST { - public interface IRightOperand - { - } - - public class NumberOperand : IRightOperand - { - public NumberOperand(int number) - { - Number = number; - } - - public int Number { get; } - - public override bool Equals(object obj) - { - if (obj is NumberOperand n) - return n.Number == Number; - - return base.Equals(obj); - } - - public override int GetHashCode() - { - return Number.GetHashCode(); - } - } - public class RangeOperand : IRightOperand { public RangeOperand(int start, int end) diff --git a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/Relation.cs b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/Relation.cs similarity index 93% rename from src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/Relation.cs rename to src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/Relation.cs index 9eb0c9d..fb92b51 100644 --- a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/Relation.cs +++ b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/Relation.cs @@ -1,4 +1,4 @@ -namespace Jeffijoe.MessageFormat.MetadataGenerator.Plural.Parsing +namespace Jeffijoe.MessageFormat.MetadataGenerator.Plural.Parsing.AST { public enum Relation { diff --git a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/VariableOperand.cs b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/VariableOperand.cs new file mode 100644 index 0000000..d36b0e9 --- /dev/null +++ b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/VariableOperand.cs @@ -0,0 +1,25 @@ +namespace Jeffijoe.MessageFormat.MetadataGenerator.Plural.Parsing.AST +{ + public class VariableOperand : ILeftOperand + { + public VariableOperand(OperandSymbol operand) + { + Operand = operand; + } + + public OperandSymbol Operand { get; } + + public override bool Equals(object obj) + { + if (obj is VariableOperand op) + return op.Operand == Operand; + + return base.Equals(obj); + } + + public override int GetHashCode() + { + return Operand.GetHashCode(); + } + } +} \ No newline at end of file diff --git a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/PluralParser.cs b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/PluralParser.cs index 6d706e6..6e9f23b 100644 --- a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/PluralParser.cs +++ b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/PluralParser.cs @@ -1,7 +1,7 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Xml; using System.Linq; +using Jeffijoe.MessageFormat.MetadataGenerator.Plural.Parsing.AST; namespace Jeffijoe.MessageFormat.MetadataGenerator.Plural.Parsing { diff --git a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/RuleParser.cs b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/RuleParser.cs index 20e71ff..a515aac 100644 --- a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/RuleParser.cs +++ b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/RuleParser.cs @@ -1,6 +1,6 @@ using System; using System.Collections.Generic; -using System.Linq; +using Jeffijoe.MessageFormat.MetadataGenerator.Plural.Parsing.AST; namespace Jeffijoe.MessageFormat.MetadataGenerator.Plural.Parsing { @@ -31,20 +31,21 @@ public OrCondition[] ParseRuleContent() AdvanceWhitespace(); var character = ConsumeChar(); - var characterNext = ConsumeChar(); + // This is where the samples start, we don't care about any of those. if (character == '@') { return conditions.ToArray(); } - else if (character == 'o' && characterNext == 'r') + + // We expect the next token to be "or" + var characterNext = ConsumeChar(); + if (character == 'o' && characterNext == 'r') { continue; } - else - { - throw new InvalidCharacterException(character); - } + + throw new InvalidCharacterException(character); } return conditions.ToArray(); @@ -198,6 +199,8 @@ private IRightOperand[] ParseRightOperand() private OrCondition ParseOrCondition() { + var andWordSpan = "and".AsSpan(); + var andConditions = new List(); while (!IsEnd) { @@ -209,27 +212,17 @@ private OrCondition ParseOrCondition() if (PeekCurrentChar == 'a') { var andWord = ConsumeCharacters(3); - ReadOnlySpan andWordExpected = stackalloc char[3] - { - 'a', - 'n', - 'd' - }; - if (andWord.SequenceEqual(andWordExpected)) + if (andWord.SequenceEqual(andWordSpan)) { continue; } - else - { - throw new InvalidCharacterException(andWord[0]); - } - } - else - { - return new OrCondition(andConditions.ToArray()); + + throw new InvalidCharacterException(andWord[0]); } + + return new OrCondition(andConditions.ToArray()); } return new OrCondition(andConditions.ToArray()); @@ -245,7 +238,7 @@ private int ParseNumber() var numberSpan = ConsumeCharacters(numbersCount); - var number = int.Parse(new string(numberSpan.ToArray())); + var number = int.Parse(numberSpan.ToString()); return number; } diff --git a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/PluralLanguagesGenerator.cs b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/PluralLanguagesGenerator.cs index dc95d0e..47f5d60 100644 --- a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/PluralLanguagesGenerator.cs +++ b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/PluralLanguagesGenerator.cs @@ -1,15 +1,15 @@ using Jeffijoe.MessageFormat.MetadataGenerator.Plural.Parsing; +using Jeffijoe.MessageFormat.MetadataGenerator.Plural.Parsing.AST; using Jeffijoe.MessageFormat.MetadataGenerator.Plural.SourceGeneration; using Microsoft.CodeAnalysis; using System; -using System.Diagnostics; using System.IO; using System.Linq; using System.Xml; -namespace Jeffijoe.MessageFormat.MetadataGenerator +namespace Jeffijoe.MessageFormat.MetadataGenerator.Plural { [Generator] public class PluralLanguagesGenerator : ISourceGenerator diff --git a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/SourceGeneration/PluralRulesMetadataGenerator.cs b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/SourceGeneration/PluralRulesMetadataGenerator.cs index 3c92ba8..5cfac44 100644 --- a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/SourceGeneration/PluralRulesMetadataGenerator.cs +++ b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/SourceGeneration/PluralRulesMetadataGenerator.cs @@ -1,6 +1,5 @@ -using Jeffijoe.MessageFormat.MetadataGenerator.Plural.Parsing; - -using System.Text; +using System.Text; +using Jeffijoe.MessageFormat.MetadataGenerator.Plural.Parsing.AST; namespace Jeffijoe.MessageFormat.MetadataGenerator.Plural.SourceGeneration { diff --git a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/SourceGeneration/RuleGenerator.cs b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/SourceGeneration/RuleGenerator.cs index 99d3cf8..21ae4c0 100644 --- a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/SourceGeneration/RuleGenerator.cs +++ b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/SourceGeneration/RuleGenerator.cs @@ -1,8 +1,7 @@ -using Jeffijoe.MessageFormat.MetadataGenerator.Plural.Parsing; - -using System; +using System; using System.Collections.Generic; using System.Text; +using Jeffijoe.MessageFormat.MetadataGenerator.Plural.Parsing.AST; namespace Jeffijoe.MessageFormat.MetadataGenerator.Plural.SourceGeneration { diff --git a/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/ParserTests.cs b/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/ParserTests.cs index b21c233..a2e7e73 100644 --- a/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/ParserTests.cs +++ b/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/ParserTests.cs @@ -1,4 +1,5 @@ using Jeffijoe.MessageFormat.MetadataGenerator.Plural.Parsing; +using Jeffijoe.MessageFormat.MetadataGenerator.Plural.Parsing.AST; using System; using System.Collections.Generic; diff --git a/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/PluralMetadataClassGeneratorTests.cs b/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/PluralMetadataClassGeneratorTests.cs index a6caecd..cd0ac0b 100644 --- a/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/PluralMetadataClassGeneratorTests.cs +++ b/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/PluralMetadataClassGeneratorTests.cs @@ -1,4 +1,4 @@ -using Jeffijoe.MessageFormat.MetadataGenerator.Plural.Parsing; +using Jeffijoe.MessageFormat.MetadataGenerator.Plural.Parsing.AST; using Jeffijoe.MessageFormat.MetadataGenerator.Plural.SourceGeneration; using Xunit; diff --git a/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/RuleSourceGeneratorTests.cs b/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/RuleSourceGeneratorTests.cs index 53a310b..7798654 100644 --- a/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/RuleSourceGeneratorTests.cs +++ b/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/RuleSourceGeneratorTests.cs @@ -1,6 +1,5 @@ -using Jeffijoe.MessageFormat.Formatting.Formatters; -using Jeffijoe.MessageFormat.MetadataGenerator.Plural.Parsing; -using Jeffijoe.MessageFormat.MetadataGenerator.Plural.SourceGeneration; +using Jeffijoe.MessageFormat.MetadataGenerator.Plural.SourceGeneration; +using Jeffijoe.MessageFormat.MetadataGenerator.Plural.Parsing.AST; using System; using System.Text; diff --git a/src/Jeffijoe.MessageFormat/Jeffijoe.MessageFormat.csproj b/src/Jeffijoe.MessageFormat/Jeffijoe.MessageFormat.csproj index e7d12a3..be0b5f5 100644 --- a/src/Jeffijoe.MessageFormat/Jeffijoe.MessageFormat.csproj +++ b/src/Jeffijoe.MessageFormat/Jeffijoe.MessageFormat.csproj @@ -14,7 +14,7 @@ - bin/Release/netstandard2.0/Jeffijoe.MessageFormat.xml + bin/$(Configuration)/$(TargetFramework)/$(AssemblyName).xml From fa5b60b342ff0d3f29a00e51dc4057d90136d6fc Mon Sep 17 00:00:00 2001 From: Kostiantyn Sharovarskyi Date: Sat, 17 Apr 2021 23:21:25 +0300 Subject: [PATCH 28/98] Add locale-specific tests to verify plural rules are there --- .../MetadataGenerator/GeneratedRulesTests.cs | 86 +++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/GeneratedRulesTests.cs diff --git a/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/GeneratedRulesTests.cs b/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/GeneratedRulesTests.cs new file mode 100644 index 0000000..3dea257 --- /dev/null +++ b/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/GeneratedRulesTests.cs @@ -0,0 +1,86 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Jeffijoe.MessageFormat.Formatting; +using Jeffijoe.MessageFormat.Formatting.Formatters; +using Jeffijoe.MessageFormat.Parsing; +using Xunit; + +namespace Jeffijoe.MessageFormat.Tests.MetadataGenerator +{ + public class GeneratedRulesTests + { + [Theory] + [InlineData(0, "днів")] + [InlineData(1, "день")] + [InlineData(101, "день")] + [InlineData(102, "дні")] + [InlineData(105, "днів")] + public void Uk_PluralizerTests(double n, string expected) + { + var subject = new PluralFormatter(); + var args = new Dictionary { { "test", n } }; + var arguments = + new ParsedArguments( + new[] + { + new KeyedBlock("one", "день"), + new KeyedBlock("few", "дні"), + new KeyedBlock("many", "днів"), + new KeyedBlock("other", "дня") + }, + new FormatterExtension[0]); + var request = new FormatterRequest(new Literal(1, 1, 1, 1, new StringBuilder()), "test", "plural", null); + var actual = subject.Pluralize("uk", arguments, Convert.ToDouble(args[request.Variable]), 0); + Assert.Equal(expected, actual); + } + + [Theory] + [InlineData(0, "дней")] + [InlineData(1, "день")] + [InlineData(101, "день")] + [InlineData(102, "дня")] + [InlineData(105, "дней")] + public void Ru_PluralizerTests(double n, string expected) + { + var subject = new PluralFormatter(); + var args = new Dictionary { { "test", n } }; + var arguments = + new ParsedArguments( + new[] + { + new KeyedBlock("one", "день"), + new KeyedBlock("few", "дня"), + new KeyedBlock("many", "дней"), + new KeyedBlock("other", "дня") + }, + new FormatterExtension[0]); + var request = new FormatterRequest(new Literal(1, 1, 1, 1, new StringBuilder()), "test", "plural", null); + var actual = subject.Pluralize("ru", arguments, Convert.ToDouble(args[request.Variable]), 0); + Assert.Equal(expected, actual); + } + + [Theory] + [InlineData(0, "days")] + [InlineData(1, "day")] + [InlineData(101, "days")] + [InlineData(102, "days")] + [InlineData(105, "days")] + public void En_PluralizerTests(double n, string expected) + { + var subject = new PluralFormatter(); + var args = new Dictionary { { "test", n } }; + var arguments = + new ParsedArguments( + new[] + { + new KeyedBlock("one", "day"), + new KeyedBlock("other", "days") + }, + new FormatterExtension[0]); + var request = new FormatterRequest(new Literal(1, 1, 1, 1, new StringBuilder()), "test", "plural", null); + var actual = subject.Pluralize("en", arguments, Convert.ToDouble(args[request.Variable]), 0); + Assert.Equal(expected, actual); + } + } +} \ No newline at end of file From 6e1fb68638cedfbbde8985815ae28e5497d136b3 Mon Sep 17 00:00:00 2001 From: Kostiantyn Sharovarskyi Date: Sat, 17 Apr 2021 23:29:17 +0300 Subject: [PATCH 29/98] Minor rename - indicate that test tests Plural rules --- .../{GeneratedRulesTests.cs => GeneratedPluralRulesTests.cs} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/{GeneratedRulesTests.cs => GeneratedPluralRulesTests.cs} (98%) diff --git a/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/GeneratedRulesTests.cs b/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/GeneratedPluralRulesTests.cs similarity index 98% rename from src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/GeneratedRulesTests.cs rename to src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/GeneratedPluralRulesTests.cs index 3dea257..8e6bd04 100644 --- a/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/GeneratedRulesTests.cs +++ b/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/GeneratedPluralRulesTests.cs @@ -8,7 +8,7 @@ namespace Jeffijoe.MessageFormat.Tests.MetadataGenerator { - public class GeneratedRulesTests + public class GeneratedPluralRulesTests { [Theory] [InlineData(0, "днів")] From 1a4c586e49e986a73d1b8f86d46a2f826384352d Mon Sep 17 00:00:00 2001 From: Kostiantyn Sharovarskyi Date: Sun, 25 Apr 2021 20:55:10 +0300 Subject: [PATCH 30/98] Support all rules except exponent via introducing PluralContext --- .../Plural/Parsing/AST/OperandSymbol.cs | 29 ++++- .../Plural/Parsing/RuleParser.cs | 14 ++- .../PluralRulesMetadataGenerator.cs | 12 +- .../Plural/SourceGeneration/RuleGenerator.cs | 47 +++----- .../Formatters/PluralFormatterTests.cs | 2 +- .../GeneratedPluralRulesTests.cs | 6 +- .../MetadataGenerator/ParserTests.cs | 23 ++++ .../MetadataGenerator/PluralContextTests.cs | 109 ++++++++++++++++++ .../PluralMetadataClassGeneratorTests.cs | 20 ++-- .../RuleSourceGeneratorTests.cs | 53 ++------- .../Formatting/Formatters/PluralContext.cs | 85 ++++++++++++++ .../Formatting/Formatters/PluralFormatter.cs | 57 ++++++--- .../Formatters/PluralRulesMetadata.cs | 8 +- .../Formatting/Formatters/Pluralizer.cs | 7 ++ .../Jeffijoe.MessageFormat.csproj | 5 +- 15 files changed, 350 insertions(+), 127 deletions(-) create mode 100644 src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/PluralContextTests.cs create mode 100644 src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralContext.cs diff --git a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/OperandSymbol.cs b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/OperandSymbol.cs index 75571a4..21f806a 100644 --- a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/OperandSymbol.cs +++ b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/OperandSymbol.cs @@ -7,14 +7,39 @@ public enum OperandSymbol /// AbsoluteValue, + /// + /// i - integer digits of n. + /// + IntegerDigits, + /// /// v - number of visible fraction digits in n, with trailing zeros. /// VisibleFractionDigitNumber, /// - /// i - integer digits of n. + /// w - number of visible fraction digits in n, without trailing zeros. /// - IntegerDigits, + VisibleFractionDigitNumberWithoutTrailingZeroes, + + /// + /// f - number of visible fraction digits in n, with trailing zeros. + /// + VisibleFractionDigits, + + /// + /// t - visible fraction digits in n, without trailing zeros. + /// + VisibleFractionDigitsWithoutTrailingZeroes, + + /// + /// c - compact decimal exponent value: exponent of the power of 10 used in compact decimal formatting. + /// + ExponentC, + + /// + /// e - currently, synonym for ‘c’. however, may be redefined in the future. + /// + ExponentE, } } diff --git a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/RuleParser.cs b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/RuleParser.cs index a515aac..887dd67 100644 --- a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/RuleParser.cs +++ b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/RuleParser.cs @@ -29,6 +29,11 @@ public OrCondition[] ParseRuleContent() conditions.Add(condition); AdvanceWhitespace(); + + if (IsEnd) + { + return conditions.ToArray(); + } var character = ConsumeChar(); @@ -65,7 +70,7 @@ public OrCondition[] ParseRuleContent() private char PeekAt(int delta) { - if (_position + delta > _ruleText.Length) + if (_position + delta >= _ruleText.Length) return NullCharacter; return _ruleText[_position + delta]; @@ -111,9 +116,14 @@ private ILeftOperand ParseLeftOperand() { var operandSymbol = ConsumeChar() switch { - 'v' => OperandSymbol.VisibleFractionDigitNumber, 'n' => OperandSymbol.AbsoluteValue, 'i' => OperandSymbol.IntegerDigits, + 'v' => OperandSymbol.VisibleFractionDigitNumber, + 'w' => OperandSymbol.VisibleFractionDigitNumberWithoutTrailingZeroes, + 'f' => OperandSymbol.VisibleFractionDigits, + 't' => OperandSymbol.VisibleFractionDigitsWithoutTrailingZeroes, + 'c' => OperandSymbol.ExponentC, + 'e' => OperandSymbol.ExponentE, var otherCharacter => throw new InvalidCharacterException(otherCharacter) }; diff --git a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/SourceGeneration/PluralRulesMetadataGenerator.cs b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/SourceGeneration/PluralRulesMetadataGenerator.cs index 5cfac44..a48b901 100644 --- a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/SourceGeneration/PluralRulesMetadataGenerator.cs +++ b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/SourceGeneration/PluralRulesMetadataGenerator.cs @@ -36,11 +36,11 @@ public string GenerateClass() foreach(var locale in rule.Locales) { - WriteLine($"public static string Locale_{locale.ToUpper()}(double value) => Rule{ruleIdx}(value);"); + WriteLine($"public static string Locale_{locale.ToUpper()}(PluralContext context) => Rule{ruleIdx}(context);"); WriteLine(string.Empty); } - WriteLine($"private static string Rule{ruleIdx}(double value)"); + WriteLine($"private static string Rule{ruleIdx}(PluralContext context)"); WriteLine("{"); AddIndent(); @@ -51,7 +51,7 @@ public string GenerateClass() WriteLine(string.Empty); } - WriteLine("private static readonly Dictionary Pluralizers = new Dictionary()"); + WriteLine("private static readonly Dictionary Pluralizers = new Dictionary()"); WriteLine("{"); AddIndent(); @@ -60,7 +60,7 @@ public string GenerateClass() PluralRule rule = _rules[ruleIdx]; foreach (var locale in rule.Locales) { - WriteLine($"{{\"{locale}\", (Pluralizer) Rule{ruleIdx}}},"); + WriteLine($"{{\"{locale}\", (ContextPluralizer) Rule{ruleIdx}}},"); } WriteLine(string.Empty); @@ -70,11 +70,11 @@ public string GenerateClass() WriteLine("};"); WriteLine(string.Empty); - WriteLine("public static partial bool TryGetRuleByLocale(string locale, out Pluralizer pluralizer)"); + WriteLine("public static partial bool TryGetRuleByLocale(string locale, out ContextPluralizer contextPluralizer)"); WriteLine("{"); AddIndent(); - WriteLine("return Pluralizers.TryGetValue(locale, out pluralizer);"); + WriteLine("return Pluralizers.TryGetValue(locale, out contextPluralizer);"); DecreaseIndent(); WriteLine("}"); diff --git a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/SourceGeneration/RuleGenerator.cs b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/SourceGeneration/RuleGenerator.cs index 21ae4c0..7c7106b 100644 --- a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/SourceGeneration/RuleGenerator.cs +++ b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/SourceGeneration/RuleGenerator.cs @@ -7,14 +7,12 @@ namespace Jeffijoe.MessageFormat.MetadataGenerator.Plural.SourceGeneration { public class RuleGenerator { - private PluralRule _rule; - private List _initializedSymbols; + private readonly PluralRule _rule; private int _innerIndent; public RuleGenerator(PluralRule rule) { _rule = rule; - _initializedSymbols = new List(); _innerIndent = 0; } @@ -30,26 +28,11 @@ public void WriteTo(StringBuilder builder, int indent) private void WriteOther(StringBuilder builder, int indent) { - if (_rule.Conditions.Length > 0) - WriteLine(builder, string.Empty, indent); - WriteLine(builder, "return \"other\";", indent); } private void WriteNext(Condition condition, StringBuilder builder, int indent) { - foreach (var operand in GetAllLeftOperands(condition.OrConditions)) - { - if(!_initializedSymbols.Contains(operand)) - { - var line = InitializeValue(operand); - WriteLine(builder, line, indent); - _initializedSymbols.Add(operand); - } - } - - WriteLine(builder, string.Empty, indent); - if(condition.OrConditions.Length > 0) { builder.Append(' ', _innerIndent + indent); @@ -73,6 +56,8 @@ private void WriteNext(Condition condition, StringBuilder builder, int indent) _innerIndent += 4; WriteLine(builder, $"return \"{condition.Count}\";", indent); _innerIndent -= 4; + + WriteLine(builder, string.Empty, indent); } else { @@ -94,8 +79,8 @@ private void WriteOrCondition(StringBuilder builder, OrCondition orCondition) var leftVariable = andCondition.OperandLeft switch { - VariableOperand op => OperandToVariable(op.Operand).ToString(), - ModuloOperand op => $"{OperandToVariable(op.Operand)} % {op.ModValue}", + VariableOperand op => $"context.{OperandToVariable(op.Operand)}", + ModuloOperand op => $"context.{OperandToVariable(op.Operand)} % {op.ModValue}", var otherOp => throw new InvalidOperationException($"Unknown operation {otherOp.GetType()}") }; @@ -130,24 +115,18 @@ private char OperandToVariable(OperandSymbol operand) { return operand switch { - OperandSymbol.AbsoluteValue => 'n', - OperandSymbol.VisibleFractionDigitNumber => 'v', - OperandSymbol.IntegerDigits => 'i', + OperandSymbol.AbsoluteValue => 'N', + OperandSymbol.IntegerDigits => 'I', + OperandSymbol.VisibleFractionDigitNumber => 'V', + OperandSymbol.VisibleFractionDigitNumberWithoutTrailingZeroes => 'W', + OperandSymbol.VisibleFractionDigits => 'F', + OperandSymbol.VisibleFractionDigitsWithoutTrailingZeroes => 'T', + OperandSymbol.ExponentC => 'C', + OperandSymbol.ExponentE => 'E', _ => throw new InvalidOperationException($"Unknown variable {operand}") }; } - private string InitializeValue(OperandSymbol operand) - { - return operand switch - { - OperandSymbol.AbsoluteValue => "var n = Math.Abs(value);", - OperandSymbol.VisibleFractionDigitNumber => "var v = (int)value == value ? 0 : 1;", - OperandSymbol.IntegerDigits => "var i = (int)value;", - var otherSymbol => throw new InvalidOperationException($"Unknown operand symbol {otherSymbol}") - }; - } - private IEnumerable GetAllLeftOperands(OrCondition[] conditions) { foreach (var condition in conditions) diff --git a/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/PluralFormatterTests.cs b/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/PluralFormatterTests.cs index 5bd014d..d8a1f6b 100644 --- a/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/PluralFormatterTests.cs +++ b/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/PluralFormatterTests.cs @@ -50,7 +50,7 @@ public void Pluralize(double n, string expected) }, new FormatterExtension[0]); var request = new FormatterRequest(new Literal(1, 1, 1, 1, new StringBuilder()), "test", "plural", null); - var actual = subject.Pluralize("en", arguments, Convert.ToDouble(args[request.Variable]), 0); + var actual = subject.Pluralize("en", arguments, new PluralContext(Convert.ToDecimal(Convert.ToDouble(args[request.Variable]))), 0); Assert.Equal(expected, actual); } diff --git a/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/GeneratedPluralRulesTests.cs b/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/GeneratedPluralRulesTests.cs index 8e6bd04..18ffcc7 100644 --- a/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/GeneratedPluralRulesTests.cs +++ b/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/GeneratedPluralRulesTests.cs @@ -31,7 +31,7 @@ public void Uk_PluralizerTests(double n, string expected) }, new FormatterExtension[0]); var request = new FormatterRequest(new Literal(1, 1, 1, 1, new StringBuilder()), "test", "plural", null); - var actual = subject.Pluralize("uk", arguments, Convert.ToDouble(args[request.Variable]), 0); + var actual = subject.Pluralize("uk", arguments, new PluralContext(Convert.ToDecimal(Convert.ToDouble(args[request.Variable]))), 0); Assert.Equal(expected, actual); } @@ -56,7 +56,7 @@ public void Ru_PluralizerTests(double n, string expected) }, new FormatterExtension[0]); var request = new FormatterRequest(new Literal(1, 1, 1, 1, new StringBuilder()), "test", "plural", null); - var actual = subject.Pluralize("ru", arguments, Convert.ToDouble(args[request.Variable]), 0); + var actual = subject.Pluralize("ru", arguments, new PluralContext(Convert.ToDecimal(args[request.Variable])), 0); Assert.Equal(expected, actual); } @@ -79,7 +79,7 @@ public void En_PluralizerTests(double n, string expected) }, new FormatterExtension[0]); var request = new FormatterRequest(new Literal(1, 1, 1, 1, new StringBuilder()), "test", "plural", null); - var actual = subject.Pluralize("en", arguments, Convert.ToDouble(args[request.Variable]), 0); + var actual = subject.Pluralize("en", arguments, new PluralContext(Convert.ToDecimal(args[request.Variable])), 0); Assert.Equal(expected, actual); } } diff --git a/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/ParserTests.cs b/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/ParserTests.cs index a2e7e73..3a79cb9 100644 --- a/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/ParserTests.cs +++ b/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/ParserTests.cs @@ -221,6 +221,29 @@ public void CanParseMixedCommaSeparatedAndRangeInRightOperator() AssertOperationEqual(expected, actual); } + [Theory] + [InlineData('n', OperandSymbol.AbsoluteValue)] + [InlineData('i', OperandSymbol.IntegerDigits)] + [InlineData('v', OperandSymbol.VisibleFractionDigitNumber)] + [InlineData('w', OperandSymbol.VisibleFractionDigitNumberWithoutTrailingZeroes)] + [InlineData('f', OperandSymbol.VisibleFractionDigits)] + [InlineData('t', OperandSymbol.VisibleFractionDigitsWithoutTrailingZeroes)] + [InlineData('c', OperandSymbol.ExponentC)] + [InlineData('e', OperandSymbol.ExponentE)] + public void MapsVariable_ToCorrectOperator(char variable, OperandSymbol symbol) + { + var rules = ParseRules( + GenerateXmlWithRuleContent($"{variable} = 3")); + var rule = Assert.Single(rules); + var condition = Assert.Single(rule.Conditions); + var orCondition = Assert.Single(condition.OrConditions); + var actual = Assert.Single(orCondition.AndConditions); + var right = new IRightOperand[] { new NumberOperand(3) }; + var expected = new Operation(new VariableOperand(symbol), Relation.Equals, right); + + AssertOperationEqual(expected, actual); + } + private static string GenerateXmlWithRuleContent(string ruleText) { return $@" diff --git a/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/PluralContextTests.cs b/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/PluralContextTests.cs new file mode 100644 index 0000000..da02049 --- /dev/null +++ b/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/PluralContextTests.cs @@ -0,0 +1,109 @@ +using Jeffijoe.MessageFormat.Formatting.Formatters; +using Xunit; + +namespace Jeffijoe.MessageFormat.Tests.MetadataGenerator +{ + public class PluralContextTests + { + [Theory] + [InlineData("-12312.213213", 12312.213213)] + [InlineData("12312.213213", 12312.213213)] + [InlineData("-12312", 12312)] + [InlineData("12312", 12312)] + [InlineData("0", 0)] + public void Parses_N(string s, double expectedN) + { + var ctx = new PluralContext(s); + + Assert.Equal(expectedN, ctx.N); + } + + [Theory] + [InlineData("-12312.213213", -12312)] + [InlineData("12312.213213", 12312)] + [InlineData("-12312", -12312)] + [InlineData("12312", 12312)] + [InlineData("0", 0)] + public void Parses_I(string s, double expectedI) + { + var ctx = new PluralContext(s); + + Assert.Equal(expectedI, ctx.I); + } + + [Theory] + [InlineData("-12312.213213", 6)] + [InlineData("12312.213213", 6)] + [InlineData("12312.2132130", 7)] + [InlineData("-12312", 0)] + [InlineData("12312", 0)] + [InlineData("0", 0)] + public void Parses_V(string s, double expectedV) + { + var ctx = new PluralContext(s); + + Assert.Equal(expectedV, ctx.V); + } + + [Theory] + [InlineData("-12312.213213", 6)] + [InlineData("12312.213213", 6)] + [InlineData("12312.2132130", 6)] + [InlineData("-12312", 0)] + [InlineData("12312", 0)] + [InlineData("0", 0)] + public void Parses_W(string s, double expectedW) + { + var ctx = new PluralContext(s); + + Assert.Equal(expectedW, ctx.W); + } + + [Theory] + [InlineData("-12312.213213", 213213)] + [InlineData("12312.213213", 213213)] + [InlineData("12312.2132130", 2132130)] + [InlineData("-12312", 0)] + [InlineData("12312", 0)] + [InlineData("0", 0)] + public void Parses_F(string s, double expectedF) + { + var ctx = new PluralContext(s); + + Assert.Equal(expectedF, ctx.F); + } + + [Theory] + [InlineData("-12312.213213", 213213)] + [InlineData("12312.213213", 213213)] + [InlineData("12312.2132130", 213213)] + [InlineData("-12312", 0)] + [InlineData("12312", 0)] + [InlineData("0", 0)] + public void Parses_T(string s, double expectedT) + { + var ctx = new PluralContext(s); + + Assert.Equal(expectedT, ctx.T); + } + + + /// + /// Exponents not supported yet + /// + [Theory] + [InlineData("-12312.213213", 0)] + [InlineData("12312.213213", 0)] + [InlineData("12312.2132130", 0)] + [InlineData("-12312", 0)] + [InlineData("12312", 0)] + [InlineData("0", 0)] + public void Parses_C_And_E(string s, double expectedC) + { + var ctx = new PluralContext(s); + + Assert.Equal(expectedC, ctx.C); + Assert.Equal(expectedC, ctx.E); + } + } +} diff --git a/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/PluralMetadataClassGeneratorTests.cs b/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/PluralMetadataClassGeneratorTests.cs index cd0ac0b..38b844a 100644 --- a/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/PluralMetadataClassGeneratorTests.cs +++ b/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/PluralMetadataClassGeneratorTests.cs @@ -35,30 +35,28 @@ namespace Jeffijoe.MessageFormat.Formatting.Formatters { public static partial class PluralRulesMetadata { - public static string Locale_EN(double value) => Rule0(value); + public static string Locale_EN(PluralContext context) => Rule0(context); - public static string Locale_UK(double value) => Rule0(value); + public static string Locale_UK(PluralContext context) => Rule0(context); - private static string Rule0(double value) + private static string Rule0(PluralContext context) { - var n = Math.Abs(value); - - if ((n == 3)) + if ((context.N == 3)) return ""one""; return ""other""; } - private static readonly Dictionary Pluralizers = new Dictionary() + private static readonly Dictionary Pluralizers = new Dictionary() { - {""en"", (Pluralizer) Rule0}, - {""uk"", (Pluralizer) Rule0}, + {""en"", (ContextPluralizer) Rule0}, + {""uk"", (ContextPluralizer) Rule0}, }; - public static partial bool TryGetRuleByLocale(string locale, out Pluralizer pluralizer) + public static partial bool TryGetRuleByLocale(string locale, out ContextPluralizer contextPluralizer) { - return Pluralizers.TryGetValue(locale, out pluralizer); + return Pluralizers.TryGetValue(locale, out contextPluralizer); } } } diff --git a/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/RuleSourceGeneratorTests.cs b/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/RuleSourceGeneratorTests.cs index 7798654..a04725d 100644 --- a/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/RuleSourceGeneratorTests.cs +++ b/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/RuleSourceGeneratorTests.cs @@ -36,9 +36,7 @@ public void CanGenerateRuleForFractionNumberEquals() var actual = GenerateText(generator); var expected = @$" -var v = (int)value == value ? 0 : 1; - -if ((v == 0)) +if ((context.V == 0)) return ""one""; return ""other""; @@ -62,9 +60,7 @@ public void CanGenerateRuleForIntegerDigitsEquals() var actual = GenerateText(generator); var expected = @$" -var i = (int)value; - -if ((i == 1)) +if ((context.I == 1)) return ""one""; return ""other""; @@ -88,9 +84,7 @@ public void CanGenerateRuleForModuloEquals() var actual = GenerateText(generator); var expected = @$" -var i = (int)value; - -if ((i % 5 == 1)) +if ((context.I % 5 == 1)) return ""one""; return ""other""; @@ -114,9 +108,7 @@ public void CanGenerateRuleForNumberEquals() var actual = GenerateText(generator); var expected = @$" -var n = Math.Abs(value); - -if ((n == 5)) +if ((context.N == 5)) return ""one""; return ""other""; @@ -140,9 +132,7 @@ public void CanGenerateRuleForNumberNotEquals() var actual = GenerateText(generator); var expected = @$" -var n = Math.Abs(value); - -if ((n != 5)) +if ((context.N != 5)) return ""one""; return ""other""; @@ -166,9 +156,7 @@ public void CanGenerateRuleForNumberRange() var actual = GenerateText(generator); var expected = @$" -var n = Math.Abs(value); - -if ((n >= 5 && n <= 6 || n == 10)) +if ((context.N >= 5 && context.N <= 6 || context.N == 10)) return ""one""; return ""other""; @@ -192,9 +180,7 @@ public void CanGenerateRuleForNegativeNumberRange() var actual = GenerateText(generator); var expected = @$" -var n = Math.Abs(value); - -if (((n < 5 || n > 6) && n != 10)) +if (((context.N < 5 || context.N > 6) && context.N != 10)) return ""one""; return ""other""; @@ -219,10 +205,7 @@ public void CanGenerateRuleForAndRules() var actual = GenerateText(generator); var expected = @$" -var n = Math.Abs(value); -var v = (int)value == value ? 0 : 1; - -if ((n == 4) && (v == 0)) +if ((context.N == 4) && (context.V == 0)) return ""one""; return ""other""; @@ -247,10 +230,7 @@ public void CanGenerateRuleForMixedRangeAndNumberRangeRules() var actual = GenerateText(generator); var expected = @$" -var n = Math.Abs(value); -var v = (int)value == value ? 0 : 1; - -if ((n >= 4 && n <= 5) && (v == 0)) +if ((context.N >= 4 && context.N <= 5) && (context.V == 0)) return ""one""; return ""other""; @@ -278,10 +258,7 @@ public void CanGenerateRuleForMultipleOrRules() var actual = GenerateText(generator); var expected = @$" -var n = Math.Abs(value); -var v = (int)value == value ? 0 : 1; - -if ((n == 4) || (v == 0)) +if ((context.N == 4) || (context.V == 0)) return ""one""; return ""other""; @@ -310,10 +287,7 @@ public void CanGenerateRuleForMixedAndOrRules() var actual = GenerateText(generator); var expected = @$" -var n = Math.Abs(value); -var v = (int)value == value ? 0 : 1; - -if ((n == 4) && (v != 0) || (v == 0)) +if ((context.N == 4) && (context.V != 0) || (context.V == 0)) return ""one""; return ""other""; @@ -342,10 +316,7 @@ public void CanGenerateRuleForMixedAndOrRangeRules() var actual = GenerateText(generator); var expected = @$" -var n = Math.Abs(value); -var v = (int)value == value ? 0 : 1; - -if ((n >= 4 && n <= 5) && (v != 0) || (v == 0)) +if ((context.N >= 4 && context.N <= 5) && (context.V != 0) || (context.V == 0)) return ""one""; return ""other""; diff --git a/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralContext.cs b/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralContext.cs new file mode 100644 index 0000000..393b54a --- /dev/null +++ b/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralContext.cs @@ -0,0 +1,85 @@ +using System; +using System.Globalization; + +namespace Jeffijoe.MessageFormat.Formatting.Formatters +{ + public readonly struct PluralContext + { + public PluralContext(decimal number) : this(number.ToString(CultureInfo.InvariantCulture)) + { + + } + + public PluralContext(int number) + { + Number = number; + N = Math.Abs(number); + I = number; + V = 0; + W = 0; + F = 0; + T = 0; + C = 0; + E = 0; + } + + public PluralContext(string number) + { + var dotIndex = number.IndexOf(".", StringComparison.InvariantCulture); + + if (dotIndex == -1) + { + var parsed = int.Parse(number); + + Number = parsed; + N = Math.Abs(parsed); + I = parsed; + V = 0; + W = 0; + F = 0; + T = 0; + C = 0; + E = 0; + } + else + { +#if NET5_0 + var fractionSpan = number.AsSpan(dotIndex + 1, number.Length - dotIndex - 1); + var fractionSpanWithoutZeroes = fractionSpan.TrimEnd('0'); +#else + var fractionSpan = number.Substring(dotIndex + 1, number.Length - dotIndex - 1); + var fractionSpanWithoutZeroes = fractionSpan.TrimEnd('0'); +#endif + var parsed = double.Parse(number); + + Number = parsed; + N = Math.Abs(parsed); + I = (int)parsed; + V = fractionSpan.Length; + W = fractionSpanWithoutZeroes.Length; + F = int.Parse(fractionSpan); + T = int.Parse(fractionSpanWithoutZeroes); + C = 0; + E = 0; + } + } + + public double Number { get; } + + public double N { get; } + + public int I { get; } + + public int V { get; } + + public int W { get; } + + public int F { get; } + + public int T { get; } + + public int C { get; } + + public int E { get; } + } +} \ No newline at end of file diff --git a/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralFormatter.cs b/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralFormatter.cs index e2f74c8..1a5c562 100644 --- a/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralFormatter.cs +++ b/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralFormatter.cs @@ -100,13 +100,33 @@ public string Format( offset = Convert.ToDouble(offsetExtension.Value); } - var n = Convert.ToDouble(value); - var pluralized = new StringBuilder(this.Pluralize(locale, arguments, n, offset)); - var result = this.ReplaceNumberLiterals(pluralized, n - offset); + var ctx = CreatePluralContext(value, offset); + var pluralized = new StringBuilder(this.Pluralize(locale, arguments, ctx, offset)); + var result = this.ReplaceNumberLiterals(pluralized, ctx.Number); var formatted = messageFormatter.FormatMessage(result, args); return formatted; } + private static PluralContext CreatePluralContext(object? value, double offset) + { + if (offset == 0) + { + if (value is string v) + { + return new PluralContext(v); + } + + if (value is int i) + { + return new PluralContext(i); + } + + return new PluralContext(Convert.ToDecimal(value)); + } + + return new PluralContext(Convert.ToDecimal(value) - (decimal) offset); + } + #endregion #region Methods @@ -120,11 +140,8 @@ public string Format( /// /// The parsed arguments string. /// - /// - /// The n. - /// - /// - /// The offset. + /// + /// The plural context. /// /// /// The . @@ -134,18 +151,22 @@ public string Format( /// [SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1126:PrefixCallsCorrectly", Justification = "Reviewed. Suppression is OK here.")] - internal string Pluralize(string locale, ParsedArguments arguments, double n, double offset) + internal string Pluralize(string locale, ParsedArguments arguments, PluralContext context, double offset) { - Pluralizer pluralizer; - if (this.Pluralizers.TryGetValue(locale, out pluralizer) == false) + string pluralForm; + if (this.Pluralizers.TryGetValue(locale, out var pluralizer)) { - if(PluralRulesMetadata.TryGetRuleByLocale(locale, out pluralizer) == false) - { - pluralizer = this.Pluralizers["en"]; - } + pluralForm = pluralizer(context.Number); } - - var pluralForm = pluralizer(n - offset); + else if (PluralRulesMetadata.TryGetRuleByLocale(locale, out var contextPluralizer)) + { + pluralForm= contextPluralizer(context); + } + else + { + pluralForm = this.Pluralizers["en"](context.Number); + } + KeyedBlock? other = null; foreach (var keyedBlock in arguments.KeyedBlocks) { @@ -159,7 +180,7 @@ internal string Pluralize(string locale, ParsedArguments arguments, double n, do var numberLiteral = Convert.ToDouble(keyedBlock.Key.Substring(1)); // ReSharper disable once CompareOfFloatsByEqualityOperator - if (numberLiteral == n) + if (numberLiteral == context.Number + offset) { return keyedBlock.BlockText; } diff --git a/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralRulesMetadata.cs b/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralRulesMetadata.cs index 0ac105f..96c7f26 100644 --- a/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralRulesMetadata.cs +++ b/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralRulesMetadata.cs @@ -1,11 +1,7 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace Jeffijoe.MessageFormat.Formatting.Formatters +namespace Jeffijoe.MessageFormat.Formatting.Formatters { public static partial class PluralRulesMetadata { - public static partial bool TryGetRuleByLocale(string locale, out Pluralizer pluralizer); + public static partial bool TryGetRuleByLocale(string locale, out ContextPluralizer pluralizer); } } diff --git a/src/Jeffijoe.MessageFormat/Formatting/Formatters/Pluralizer.cs b/src/Jeffijoe.MessageFormat/Formatting/Formatters/Pluralizer.cs index 16e1eff..a074335 100644 --- a/src/Jeffijoe.MessageFormat/Formatting/Formatters/Pluralizer.cs +++ b/src/Jeffijoe.MessageFormat/Formatting/Formatters/Pluralizer.cs @@ -10,4 +10,11 @@ namespace Jeffijoe.MessageFormat.Formatting.Formatters /// The number used to determine the pluralization rule.. /// The plural form to use. public delegate string Pluralizer(double n); + + /// + /// Given the specified number context, determines what plural form is being used. + /// + /// The context of the number used to determine the pluralization rule.. + /// The plural form to use. + public delegate string ContextPluralizer(PluralContext context); } \ No newline at end of file diff --git a/src/Jeffijoe.MessageFormat/Jeffijoe.MessageFormat.csproj b/src/Jeffijoe.MessageFormat/Jeffijoe.MessageFormat.csproj index be0b5f5..2003382 100644 --- a/src/Jeffijoe.MessageFormat/Jeffijoe.MessageFormat.csproj +++ b/src/Jeffijoe.MessageFormat/Jeffijoe.MessageFormat.csproj @@ -10,7 +10,7 @@ https://github.com/jeffijoe/messageformat.net latest enable - netstandard2.0 + netstandard2.0;net5.0 @@ -29,9 +29,8 @@ - si da is mk ceb fil tl lv prg bs hr sh sr fr dsb hsb lt + - From 0152076146957feb7a5d5e4b0ff02740f2e74650 Mon Sep 17 00:00:00 2001 From: Kostiantyn Sharovarskyi Date: Mon, 26 Apr 2021 03:47:08 +0300 Subject: [PATCH 31/98] Remove double parsing --- .../Formatting/Formatters/PluralContext.cs | 31 +++++++++---------- 1 file changed, 14 insertions(+), 17 deletions(-) diff --git a/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralContext.cs b/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralContext.cs index 393b54a..f6fc497 100644 --- a/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralContext.cs +++ b/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralContext.cs @@ -5,11 +5,6 @@ namespace Jeffijoe.MessageFormat.Formatting.Formatters { public readonly struct PluralContext { - public PluralContext(decimal number) : this(number.ToString(CultureInfo.InvariantCulture)) - { - - } - public PluralContext(int number) { Number = number; @@ -22,18 +17,24 @@ public PluralContext(int number) C = 0; E = 0; } + + public PluralContext(decimal number) : this(number.ToString(CultureInfo.InvariantCulture), (double) number) + { + } - public PluralContext(string number) + public PluralContext(string number) : this(number, double.Parse(number)) { - var dotIndex = number.IndexOf(".", StringComparison.InvariantCulture); + } + + private PluralContext(string number, double parsed) + { + Number = parsed; + N = Math.Abs(parsed); + I = (int) parsed; + var dotIndex = number.IndexOf(".", StringComparison.InvariantCulture); if (dotIndex == -1) { - var parsed = int.Parse(number); - - Number = parsed; - N = Math.Abs(parsed); - I = parsed; V = 0; W = 0; F = 0; @@ -50,16 +51,12 @@ public PluralContext(string number) var fractionSpan = number.Substring(dotIndex + 1, number.Length - dotIndex - 1); var fractionSpanWithoutZeroes = fractionSpan.TrimEnd('0'); #endif - var parsed = double.Parse(number); - Number = parsed; - N = Math.Abs(parsed); - I = (int)parsed; V = fractionSpan.Length; W = fractionSpanWithoutZeroes.Length; F = int.Parse(fractionSpan); T = int.Parse(fractionSpanWithoutZeroes); - C = 0; + C = 0; E = 0; } } From 21c0d3c415705f7d613df70f81ec9a38e12623ae Mon Sep 17 00:00:00 2001 From: Kostiantyn Sharovarskyi Date: Mon, 26 Apr 2021 03:48:10 +0300 Subject: [PATCH 32/98] Fix culture assumption --- .../Formatting/Formatters/PluralContext.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralContext.cs b/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralContext.cs index f6fc497..e93907b 100644 --- a/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralContext.cs +++ b/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralContext.cs @@ -22,7 +22,7 @@ public PluralContext(decimal number) : this(number.ToString(CultureInfo.Invarian { } - public PluralContext(string number) : this(number, double.Parse(number)) + public PluralContext(string number) : this(number, double.Parse(number, CultureInfo.InvariantCulture)) { } From 7ac0c61516243c07c4e45435c247cfc41ba73a67 Mon Sep 17 00:00:00 2001 From: Kostiantyn Sharovarskyi Date: Mon, 26 Apr 2021 03:51:38 +0300 Subject: [PATCH 33/98] Add special case for double in context --- .../Formatting/Formatters/PluralContext.cs | 4 ++++ .../Formatting/Formatters/PluralFormatter.cs | 9 +++++++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralContext.cs b/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralContext.cs index e93907b..8994f89 100644 --- a/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralContext.cs +++ b/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralContext.cs @@ -22,6 +22,10 @@ public PluralContext(decimal number) : this(number.ToString(CultureInfo.Invarian { } + public PluralContext(double number) : this(number.ToString(CultureInfo.InvariantCulture), number) + { + } + public PluralContext(string number) : this(number, double.Parse(number, CultureInfo.InvariantCulture)) { } diff --git a/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralFormatter.cs b/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralFormatter.cs index 1a5c562..416717b 100644 --- a/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralFormatter.cs +++ b/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralFormatter.cs @@ -121,10 +121,15 @@ private static PluralContext CreatePluralContext(object? value, double offset) return new PluralContext(i); } - return new PluralContext(Convert.ToDecimal(value)); + if (value is decimal d) + { + return new PluralContext(d); + } + + return new PluralContext(Convert.ToDouble(value)); } - return new PluralContext(Convert.ToDecimal(value) - (decimal) offset); + return new PluralContext(Convert.ToDouble(value) - offset); } #endregion From 4249738f3e1d6ee26ffcbf063a5a3c5e5895e609 Mon Sep 17 00:00:00 2001 From: Kostiantyn Sharovarskyi Date: Mon, 26 Apr 2021 03:54:21 +0300 Subject: [PATCH 34/98] Search for dot character, not string --- .../Formatting/Formatters/PluralContext.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralContext.cs b/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralContext.cs index 8994f89..8efebc3 100644 --- a/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralContext.cs +++ b/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralContext.cs @@ -36,7 +36,7 @@ private PluralContext(string number, double parsed) N = Math.Abs(parsed); I = (int) parsed; - var dotIndex = number.IndexOf(".", StringComparison.InvariantCulture); + var dotIndex = number.IndexOf('.'); if (dotIndex == -1) { V = 0; From 938bc4e2095f8c16b5215b2099e5d4e16ce6484d Mon Sep 17 00:00:00 2001 From: Kostiantyn Sharovarskyi Date: Mon, 26 Apr 2021 17:15:56 +0300 Subject: [PATCH 35/98] Remove useless cast in generated code --- .../Plural/SourceGeneration/PluralRulesMetadataGenerator.cs | 2 +- .../MetadataGenerator/PluralMetadataClassGeneratorTests.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/SourceGeneration/PluralRulesMetadataGenerator.cs b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/SourceGeneration/PluralRulesMetadataGenerator.cs index a48b901..cefca62 100644 --- a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/SourceGeneration/PluralRulesMetadataGenerator.cs +++ b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/SourceGeneration/PluralRulesMetadataGenerator.cs @@ -60,7 +60,7 @@ public string GenerateClass() PluralRule rule = _rules[ruleIdx]; foreach (var locale in rule.Locales) { - WriteLine($"{{\"{locale}\", (ContextPluralizer) Rule{ruleIdx}}},"); + WriteLine($"{{\"{locale}\", Rule{ruleIdx}}},"); } WriteLine(string.Empty); diff --git a/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/PluralMetadataClassGeneratorTests.cs b/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/PluralMetadataClassGeneratorTests.cs index 38b844a..9cb6344 100644 --- a/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/PluralMetadataClassGeneratorTests.cs +++ b/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/PluralMetadataClassGeneratorTests.cs @@ -49,8 +49,8 @@ private static string Rule0(PluralContext context) private static readonly Dictionary Pluralizers = new Dictionary() { - {""en"", (ContextPluralizer) Rule0}, - {""uk"", (ContextPluralizer) Rule0}, + {""en"", Rule0}, + {""uk"", Rule0}, }; From cb19c334ab0c7b975492576b3d315421271a699a Mon Sep 17 00:00:00 2001 From: Kostiantyn Sharovarskyi Date: Mon, 26 Apr 2021 17:41:27 +0300 Subject: [PATCH 36/98] Remove bunch of ToArrays in SourceGenerator --- .../Plural/Parsing/AST/Condition.cs | 7 ++++--- .../Plural/Parsing/AST/Operation.cs | 8 +++++--- .../Plural/Parsing/AST/OrCondition.cs | 8 +++++--- .../Plural/Parsing/AST/PluralRule.cs | 8 +++++--- .../Plural/Parsing/PluralParser.cs | 2 +- .../Plural/Parsing/RuleParser.cs | 20 +++++++++---------- .../Plural/PluralLanguagesGenerator.cs | 3 ++- .../PluralRulesMetadataGenerator.cs | 11 +++++----- .../Plural/SourceGeneration/RuleGenerator.cs | 14 ++++++------- .../MetadataGenerator/ParserTests.cs | 4 ++-- 10 files changed, 47 insertions(+), 38 deletions(-) diff --git a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/Condition.cs b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/Condition.cs index 00f75c5..d714868 100644 --- a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/Condition.cs +++ b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/Condition.cs @@ -1,11 +1,12 @@ -using System.Diagnostics; +using System.Collections.Generic; +using System.Diagnostics; namespace Jeffijoe.MessageFormat.MetadataGenerator.Plural.Parsing.AST { [DebuggerDisplay("{{RuleDescription}}")] public class Condition { - public Condition(string count, string ruleDescription, OrCondition[] orConditions) + public Condition(string count, string ruleDescription, IReadOnlyList orConditions) { Count = count; RuleDescription = ruleDescription; @@ -16,6 +17,6 @@ public Condition(string count, string ruleDescription, OrCondition[] orCondition public string RuleDescription { get; } - public OrCondition[] OrConditions { get; } + public IReadOnlyList OrConditions { get; } } } diff --git a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/Operation.cs b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/Operation.cs index 8ed2388..9dff3cb 100644 --- a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/Operation.cs +++ b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/Operation.cs @@ -1,8 +1,10 @@ -namespace Jeffijoe.MessageFormat.MetadataGenerator.Plural.Parsing.AST +using System.Collections.Generic; + +namespace Jeffijoe.MessageFormat.MetadataGenerator.Plural.Parsing.AST { public class Operation { - public Operation(ILeftOperand operandLeft, Relation relation, IRightOperand[] operandRight) + public Operation(ILeftOperand operandLeft, Relation relation, IReadOnlyList operandRight) { OperandLeft = operandLeft; Relation = relation; @@ -13,6 +15,6 @@ public Operation(ILeftOperand operandLeft, Relation relation, IRightOperand[] op public Relation Relation { get; } - public IRightOperand[] OperandRight { get; } + public IReadOnlyList OperandRight { get; } } } diff --git a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/OrCondition.cs b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/OrCondition.cs index e03c35e..435f455 100644 --- a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/OrCondition.cs +++ b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/OrCondition.cs @@ -1,12 +1,14 @@ -namespace Jeffijoe.MessageFormat.MetadataGenerator.Plural.Parsing.AST +using System.Collections.Generic; + +namespace Jeffijoe.MessageFormat.MetadataGenerator.Plural.Parsing.AST { public class OrCondition { - public OrCondition(Operation[] andConditions) + public OrCondition(IReadOnlyList andConditions) { AndConditions = andConditions; } - public Operation[] AndConditions { get; } + public IReadOnlyList AndConditions { get; } } } diff --git a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/PluralRule.cs b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/PluralRule.cs index d5de89b..97461f1 100644 --- a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/PluralRule.cs +++ b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/PluralRule.cs @@ -1,8 +1,10 @@ -namespace Jeffijoe.MessageFormat.MetadataGenerator.Plural.Parsing.AST +using System.Collections.Generic; + +namespace Jeffijoe.MessageFormat.MetadataGenerator.Plural.Parsing.AST { public class PluralRule { - public PluralRule(string[] locales, Condition[] conditions) + public PluralRule(string[] locales, IReadOnlyList conditions) { Locales = locales; Conditions = conditions; @@ -10,6 +12,6 @@ public PluralRule(string[] locales, Condition[] conditions) public string[] Locales { get; } - public Condition[] Conditions { get; } + public IReadOnlyList Conditions { get; } } } diff --git a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/PluralParser.cs b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/PluralParser.cs index 6e9f23b..ba8ca3c 100644 --- a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/PluralParser.cs +++ b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/PluralParser.cs @@ -68,7 +68,7 @@ public IEnumerable Parse() } } - return new PluralRule(locales, conditions.ToArray()); + return new PluralRule(locales, conditions); } } } diff --git a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/RuleParser.cs b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/RuleParser.cs index 887dd67..f5a62d4 100644 --- a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/RuleParser.cs +++ b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/RuleParser.cs @@ -6,7 +6,7 @@ namespace Jeffijoe.MessageFormat.MetadataGenerator.Plural.Parsing { public class RuleParser { - private string _ruleText; + private readonly string _ruleText; private int _position; public RuleParser(string ruleText) @@ -14,7 +14,7 @@ public RuleParser(string ruleText) _ruleText = ruleText; } - public OrCondition[] ParseRuleContent() + public IReadOnlyList ParseRuleContent() { var conditions = new List(); @@ -22,7 +22,7 @@ public OrCondition[] ParseRuleContent() { if (PeekCurrentChar == '@') { - return conditions.ToArray(); + return conditions; } var condition = ParseOrCondition(); @@ -32,7 +32,7 @@ public OrCondition[] ParseRuleContent() if (IsEnd) { - return conditions.ToArray(); + return conditions; } var character = ConsumeChar(); @@ -40,7 +40,7 @@ public OrCondition[] ParseRuleContent() // This is where the samples start, we don't care about any of those. if (character == '@') { - return conditions.ToArray(); + return conditions; } // We expect the next token to be "or" @@ -53,7 +53,7 @@ public OrCondition[] ParseRuleContent() throw new InvalidCharacterException(character); } - return conditions.ToArray(); + return conditions; } private static readonly char NullCharacter = '\0'; @@ -163,7 +163,7 @@ private Operation ParseAndCondition() return new Operation(leftOperand, relation, rightOperand); } - private IRightOperand[] ParseRightOperand() + private IReadOnlyList ParseRightOperand() { var numbers = new List(); @@ -204,7 +204,7 @@ private IRightOperand[] ParseRightOperand() } } - return numbers.ToArray(); + return numbers; } private OrCondition ParseOrCondition() @@ -232,10 +232,10 @@ private OrCondition ParseOrCondition() throw new InvalidCharacterException(andWord[0]); } - return new OrCondition(andConditions.ToArray()); + return new OrCondition(andConditions); } - return new OrCondition(andConditions.ToArray()); + return new OrCondition(andConditions); } private int ParseNumber() diff --git a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/PluralLanguagesGenerator.cs b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/PluralLanguagesGenerator.cs index 47f5d60..06251df 100644 --- a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/PluralLanguagesGenerator.cs +++ b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/PluralLanguagesGenerator.cs @@ -5,6 +5,7 @@ using Microsoft.CodeAnalysis; using System; +using System.Collections.Generic; using System.IO; using System.Linq; using System.Xml; @@ -35,7 +36,7 @@ private string[] ReadExcludeLocales(GeneratorExecutionContext context) return Array.Empty(); } - private PluralRule[] GetRules(string[] excludedLocales) + private IReadOnlyList GetRules(string[] excludedLocales) { using var rulesStream = GetRulesContentStream(); var xml = new XmlDocument(); diff --git a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/SourceGeneration/PluralRulesMetadataGenerator.cs b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/SourceGeneration/PluralRulesMetadataGenerator.cs index cefca62..a9bb3be 100644 --- a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/SourceGeneration/PluralRulesMetadataGenerator.cs +++ b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/SourceGeneration/PluralRulesMetadataGenerator.cs @@ -1,15 +1,16 @@ -using System.Text; +using System.Collections.Generic; +using System.Text; using Jeffijoe.MessageFormat.MetadataGenerator.Plural.Parsing.AST; namespace Jeffijoe.MessageFormat.MetadataGenerator.Plural.SourceGeneration { public class PluralRulesMetadataGenerator { - private readonly PluralRule[] _rules; + private readonly IReadOnlyList _rules; private readonly StringBuilder _sb; private int _indent; - public PluralRulesMetadataGenerator(PluralRule[] rules) + public PluralRulesMetadataGenerator(IReadOnlyList rules) { _rules = rules; _sb = new StringBuilder(); @@ -28,7 +29,7 @@ public string GenerateClass() WriteLine("{"); AddIndent(); - for (var ruleIdx = 0; ruleIdx < _rules.Length; ruleIdx++) + for (var ruleIdx = 0; ruleIdx < _rules.Count; ruleIdx++) { var rule = _rules[ruleIdx]; @@ -55,7 +56,7 @@ public string GenerateClass() WriteLine("{"); AddIndent(); - for (int ruleIdx = 0; ruleIdx < _rules.Length; ruleIdx++) + for (int ruleIdx = 0; ruleIdx < _rules.Count; ruleIdx++) { PluralRule rule = _rules[ruleIdx]; foreach (var locale in rule.Locales) diff --git a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/SourceGeneration/RuleGenerator.cs b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/SourceGeneration/RuleGenerator.cs index 7c7106b..dd44405 100644 --- a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/SourceGeneration/RuleGenerator.cs +++ b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/SourceGeneration/RuleGenerator.cs @@ -33,15 +33,15 @@ private void WriteOther(StringBuilder builder, int indent) private void WriteNext(Condition condition, StringBuilder builder, int indent) { - if(condition.OrConditions.Length > 0) + if(condition.OrConditions.Count > 0) { builder.Append(' ', _innerIndent + indent); builder.Append("if ("); - for (int orIdx = 0; orIdx < condition.OrConditions.Length; orIdx++) + for (int orIdx = 0; orIdx < condition.OrConditions.Count; orIdx++) { OrCondition orCondition = condition.OrConditions[orIdx]; - var orIsLast = orIdx == condition.OrConditions.Length - 1; + var orIsLast = orIdx == condition.OrConditions.Count - 1; WriteOrCondition(builder, orCondition); @@ -67,15 +67,15 @@ private void WriteNext(Condition condition, StringBuilder builder, int indent) private void WriteOrCondition(StringBuilder builder, OrCondition orCondition) { - for (int andIdx = 0; andIdx < orCondition.AndConditions.Length; andIdx++) + for (int andIdx = 0; andIdx < orCondition.AndConditions.Count; andIdx++) { - var andIsLast = andIdx == orCondition.AndConditions.Length - 1; + var andIsLast = andIdx == orCondition.AndConditions.Count - 1; Operation andCondition = orCondition.AndConditions[andIdx]; builder.Append('('); - for (int innerOrIdx = 0; innerOrIdx < andCondition.OperandRight.Length; innerOrIdx++) + for (int innerOrIdx = 0; innerOrIdx < andCondition.OperandRight.Count; innerOrIdx++) { - var isLast = innerOrIdx == andCondition.OperandRight.Length - 1; + var isLast = innerOrIdx == andCondition.OperandRight.Count - 1; var leftVariable = andCondition.OperandLeft switch { diff --git a/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/ParserTests.cs b/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/ParserTests.cs index 3a79cb9..a3480d0 100644 --- a/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/ParserTests.cs +++ b/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/ParserTests.cs @@ -123,7 +123,7 @@ public void CanParseOrRules() var rule = Assert.Single(rules); var condition = Assert.Single(rule.Conditions); - Assert.Equal(3, condition.OrConditions.Length); + Assert.Equal(3, condition.OrConditions.Count); var actualFirst = Assert.Single(condition.OrConditions[0].AndConditions); var expectedFirst = new Operation(new VariableOperand(OperandSymbol.AbsoluteValue), Relation.Equals, new[] { new NumberOperand(2) }); @@ -146,7 +146,7 @@ public void CanParseAndRules() var condition = Assert.Single(rule.Conditions); var orCondition = Assert.Single(condition.OrConditions); - Assert.Equal(3, orCondition.AndConditions.Length); + Assert.Equal(3, orCondition.AndConditions.Count); var actualFirst = orCondition.AndConditions[0]; var expectedFirst = new Operation(new VariableOperand(OperandSymbol.AbsoluteValue), Relation.Equals, new[] { new NumberOperand(2) }); From 49dac1ad6497250b3ba440a7a111defa880408cf Mon Sep 17 00:00:00 2001 From: Kostiantyn Sharovarskyi Date: Mon, 26 Apr 2021 18:30:34 +0300 Subject: [PATCH 37/98] Make plural metadata internal --- .../Plural/SourceGeneration/PluralRulesMetadataGenerator.cs | 2 +- .../MetadataGenerator/PluralMetadataClassGeneratorTests.cs | 2 +- .../Formatting/Formatters/PluralContext.cs | 2 +- .../Formatting/Formatters/PluralFormatter.cs | 3 +++ .../Formatting/Formatters/PluralRulesMetadata.cs | 2 +- .../Formatting/Formatters/Pluralizer.cs | 4 ++-- 6 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/SourceGeneration/PluralRulesMetadataGenerator.cs b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/SourceGeneration/PluralRulesMetadataGenerator.cs index a9bb3be..6c33015 100644 --- a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/SourceGeneration/PluralRulesMetadataGenerator.cs +++ b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/SourceGeneration/PluralRulesMetadataGenerator.cs @@ -25,7 +25,7 @@ public string GenerateClass() WriteLine("{"); AddIndent(); - WriteLine("public static partial class PluralRulesMetadata"); + WriteLine("internal static partial class PluralRulesMetadata"); WriteLine("{"); AddIndent(); diff --git a/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/PluralMetadataClassGeneratorTests.cs b/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/PluralMetadataClassGeneratorTests.cs index 9cb6344..bbcd101 100644 --- a/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/PluralMetadataClassGeneratorTests.cs +++ b/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/PluralMetadataClassGeneratorTests.cs @@ -33,7 +33,7 @@ public void CanGenerateClassFromRules() using System.Collections.Generic; namespace Jeffijoe.MessageFormat.Formatting.Formatters { - public static partial class PluralRulesMetadata + internal static partial class PluralRulesMetadata { public static string Locale_EN(PluralContext context) => Rule0(context); diff --git a/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralContext.cs b/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralContext.cs index 8efebc3..ab54340 100644 --- a/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralContext.cs +++ b/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralContext.cs @@ -3,7 +3,7 @@ namespace Jeffijoe.MessageFormat.Formatting.Formatters { - public readonly struct PluralContext + internal readonly struct PluralContext { public PluralContext(int number) { diff --git a/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralFormatter.cs b/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralFormatter.cs index 416717b..82842aa 100644 --- a/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralFormatter.cs +++ b/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralFormatter.cs @@ -148,6 +148,9 @@ private static PluralContext CreatePluralContext(object? value, double offset) /// /// The plural context. /// + /// + /// The offset (already applied in context). + /// /// /// The . /// diff --git a/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralRulesMetadata.cs b/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralRulesMetadata.cs index 96c7f26..9d76e4b 100644 --- a/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralRulesMetadata.cs +++ b/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralRulesMetadata.cs @@ -1,6 +1,6 @@ namespace Jeffijoe.MessageFormat.Formatting.Formatters { - public static partial class PluralRulesMetadata + internal static partial class PluralRulesMetadata { public static partial bool TryGetRuleByLocale(string locale, out ContextPluralizer pluralizer); } diff --git a/src/Jeffijoe.MessageFormat/Formatting/Formatters/Pluralizer.cs b/src/Jeffijoe.MessageFormat/Formatting/Formatters/Pluralizer.cs index a074335..c034f36 100644 --- a/src/Jeffijoe.MessageFormat/Formatting/Formatters/Pluralizer.cs +++ b/src/Jeffijoe.MessageFormat/Formatting/Formatters/Pluralizer.cs @@ -14,7 +14,7 @@ namespace Jeffijoe.MessageFormat.Formatting.Formatters /// /// Given the specified number context, determines what plural form is being used. /// - /// The context of the number used to determine the pluralization rule.. + /// The context of the number used to determine the pluralization rule.. /// The plural form to use. - public delegate string ContextPluralizer(PluralContext context); + internal delegate string ContextPluralizer(PluralContext context); } \ No newline at end of file From 18e64204fcfb80af5cd1b84969dadb5bfd6b83b6 Mon Sep 17 00:00:00 2001 From: Jeff Hansen Date: Mon, 26 Apr 2021 12:20:58 -0400 Subject: [PATCH 38/98] Add signing to new MetadataGenerator project --- .../Jeffijoe.MessageFormat.MetadataGenerator.csproj | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Jeffijoe.MessageFormat.MetadataGenerator/Jeffijoe.MessageFormat.MetadataGenerator.csproj b/src/Jeffijoe.MessageFormat.MetadataGenerator/Jeffijoe.MessageFormat.MetadataGenerator.csproj index 77cd2e4..8abc8d1 100644 --- a/src/Jeffijoe.MessageFormat.MetadataGenerator/Jeffijoe.MessageFormat.MetadataGenerator.csproj +++ b/src/Jeffijoe.MessageFormat.MetadataGenerator/Jeffijoe.MessageFormat.MetadataGenerator.csproj @@ -1,6 +1,8 @@  + True + ../Jeffijoe.MessageFormat/MessageFormat.snk netstandard2.0 8 enable From 83d95a270598ed6e451eefc1393508715d614492 Mon Sep 17 00:00:00 2001 From: Jeff Hansen Date: Mon, 26 Apr 2021 12:32:38 -0400 Subject: [PATCH 39/98] Update README --- README.md | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/README.md b/README.md index 2f902c3..91c7589 100644 --- a/README.md +++ b/README.md @@ -74,6 +74,7 @@ Install-Package MessageFormat and if you are reusing the same instance of `MessageFormatter`, the formatter will cache the tokens of each pattern (nested, too), so it won't have to spend CPU time to parse out literals every time. I benchmarked it, and on my monster machine, it didn't make much of a difference (10000 iterations). +* **Built-in pluralization formatters**. Generated from the [CLDR pluralization rule data](http://cldr.unicode.org/index/downloads). ## Performance @@ -149,12 +150,6 @@ be (somewhat) compatible with his. If you have issues with the library, and the exception makes no sense, please open an issue and include your message, as well as the data you used. -# Todo - -* Built-in locales (currently only `en` is added per default). - -Don't expect this in the near future - you're welcome to submit a PR. :) - # Author I'm Jeff Hansen, a software developer who likes to fiddle with string parsing when it is not too difficult. From ee224ab364d8a4e765f4e34b0a50bf683596866f Mon Sep 17 00:00:00 2001 From: Kostiantyn Sharovarskyi Date: Tue, 27 Apr 2021 03:23:57 +0300 Subject: [PATCH 40/98] Remove ToArray -> the requests are already cloned --- .../MessageFormatterTests.cs | 6 ++++++ src/Jeffijoe.MessageFormat/MessageFormatter.cs | 14 ++++---------- .../Parsing/IFormatterRequestCollection.cs | 2 +- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/Jeffijoe.MessageFormat.Tests/MessageFormatterTests.cs b/src/Jeffijoe.MessageFormat.Tests/MessageFormatterTests.cs index 5ab43e0..167822b 100644 --- a/src/Jeffijoe.MessageFormat.Tests/MessageFormatterTests.cs +++ b/src/Jeffijoe.MessageFormat.Tests/MessageFormatterTests.cs @@ -101,6 +101,8 @@ public void FormatMessage() this.formatterMock1.Setup(x => x.Format("en", requests[0], args, "Jeff", this.subject)).Returns("Jeff"); this.formatterMock2.Setup(x => x.Format("en", requests[1], args, 1, this.subject)).Returns("123 messages"); this.collectionMock.Setup(x => x.GetEnumerator()).Returns(requests.AsEnumerable().GetEnumerator()); + this.collectionMock.Setup(x => x.Count).Returns(requests.Length); + this.collectionMock.Setup(x => x[It.IsAny()]).Returns((int i) => requests[i]); this.libraryMock.Setup(x => x.GetFormatter(requests[0])).Returns(this.formatterMock1.Object); this.libraryMock.Setup(x => x.GetFormatter(requests[1])).Returns(this.formatterMock2.Object); this.patternParserMock.Setup(x => x.Parse(It.IsAny())).Returns(this.collectionMock.Object); @@ -163,6 +165,8 @@ public void VerifyFormatMessageThrowsWhenVariablesAreMissingAndTheFormatterRequi }; this.collectionMock.Setup(x => x.GetEnumerator()).Returns(() => requests.AsEnumerable().GetEnumerator()); + this.collectionMock.Setup(x => x.Count).Returns(requests.Length); + this.collectionMock.Setup(x => x[It.IsAny()]).Returns((int i) => requests[i]); this.patternParserMock.Setup(x => x.Parse(It.IsAny())).Returns(this.collectionMock.Object); this.formatterMock1.SetupGet(x => x.VariableMustExist).Returns(true); this.libraryMock.Setup(x => x.GetFormatter(It.IsAny())).Returns(formatterMock1.Object); @@ -197,6 +201,8 @@ public void VerifyFormatMessageAllowsNonExistentVariablesWhenFormatterAllowsIt() }; this.collectionMock.Setup(x => x.GetEnumerator()).Returns(() => requests.AsEnumerable().GetEnumerator()); + this.collectionMock.Setup(x => x.Count).Returns(requests.Length); + this.collectionMock.Setup(x => x[It.IsAny()]).Returns((int i) => requests[i]); this.patternParserMock.Setup(x => x.Parse(It.IsAny())).Returns(this.collectionMock.Object); this.libraryMock.Setup(x => x.GetFormatter(It.IsAny())).Returns(formatterMock2.Object); this.formatterMock2.SetupGet(x => x.VariableMustExist).Returns(false); diff --git a/src/Jeffijoe.MessageFormat/MessageFormatter.cs b/src/Jeffijoe.MessageFormat/MessageFormatter.cs index ed56c26..4ba9229 100644 --- a/src/Jeffijoe.MessageFormat/MessageFormatter.cs +++ b/src/Jeffijoe.MessageFormat/MessageFormatter.cs @@ -215,16 +215,9 @@ public string FormatMessage(string pattern, IDictionary args) */ var sourceBuilder = new StringBuilder(pattern); var requests = this.ParseRequests(pattern, sourceBuilder); - var requestsEnumerated = requests.ToArray(); + var requestsEnumerated = requests; - // If we got no formatters, then we only need to unescape the literals. - if (requestsEnumerated.Length == 0) - { - sourceBuilder = this.UnescapeLiterals(sourceBuilder); - return sourceBuilder.ToString(); - } - - for (int i = 0; i < requestsEnumerated.Length; i++) + for (int i = 0; i < requestsEnumerated.Count; i++) { var request = requestsEnumerated[i]; @@ -297,7 +290,7 @@ protected internal StringBuilder UnescapeLiterals(StringBuilder sourceBuilder) // If the block is empty, do nothing. if (sourceBuilder.Length == 0) { - return new StringBuilder(); + return sourceBuilder; } var dest = new StringBuilder(sourceBuilder.Length, sourceBuilder.Length); @@ -307,6 +300,7 @@ protected internal StringBuilder UnescapeLiterals(StringBuilder sourceBuilder) const char CloseBrace = '}'; var braceBalance = 0; var insideEscapeSequence = false; + for (int i = 0; i < length; i++) { var c = sourceBuilder[i]; diff --git a/src/Jeffijoe.MessageFormat/Parsing/IFormatterRequestCollection.cs b/src/Jeffijoe.MessageFormat/Parsing/IFormatterRequestCollection.cs index 067b319..f237978 100644 --- a/src/Jeffijoe.MessageFormat/Parsing/IFormatterRequestCollection.cs +++ b/src/Jeffijoe.MessageFormat/Parsing/IFormatterRequestCollection.cs @@ -12,7 +12,7 @@ namespace Jeffijoe.MessageFormat.Parsing /// /// Formatter requests collection. /// - public interface IFormatterRequestCollection : IEnumerable + public interface IFormatterRequestCollection : IReadOnlyList { #region Public Methods and Operators From ff4e0e1e3bcd47cdbd2b1eab48af1983ed0d1df3 Mon Sep 17 00:00:00 2001 From: Kostiantyn Sharovarskyi Date: Tue, 27 Apr 2021 03:48:38 +0300 Subject: [PATCH 41/98] Remove allocation in formatter get --- .../Formatting/FormatterLibrary.cs | 8 ++++---- .../Formatting/FormatterRequest.cs | 8 ++++---- .../Formatting/IFormatterLibrary.cs | 2 +- src/Jeffijoe.MessageFormat/MessageFormatter.cs | 12 ++++-------- 4 files changed, 13 insertions(+), 17 deletions(-) diff --git a/src/Jeffijoe.MessageFormat/Formatting/FormatterLibrary.cs b/src/Jeffijoe.MessageFormat/Formatting/FormatterLibrary.cs index d887535..a329e12 100644 --- a/src/Jeffijoe.MessageFormat/Formatting/FormatterLibrary.cs +++ b/src/Jeffijoe.MessageFormat/Formatting/FormatterLibrary.cs @@ -45,13 +45,13 @@ public FormatterLibrary() /// public IFormatter GetFormatter(FormatterRequest request) { - var formatter = this.FirstOrDefault(x => x.CanFormat(request)); - if (formatter == null) + foreach (var formatter in this) { - throw new FormatterNotFoundException(request); + if (formatter.CanFormat(request)) + return formatter; } - return formatter; + throw new FormatterNotFoundException(request); } #endregion diff --git a/src/Jeffijoe.MessageFormat/Formatting/FormatterRequest.cs b/src/Jeffijoe.MessageFormat/Formatting/FormatterRequest.cs index 071dc4e..2a964df 100644 --- a/src/Jeffijoe.MessageFormat/Formatting/FormatterRequest.cs +++ b/src/Jeffijoe.MessageFormat/Formatting/FormatterRequest.cs @@ -47,7 +47,7 @@ public FormatterRequest(Literal sourceLiteral, string variable, string? formatte /// /// The formatter arguments. /// - public string? FormatterArguments { get; private set; } + public string? FormatterArguments { get; } /// /// Gets the name of the formatter to use . e.g. 'select', 'plural'. Can be null. @@ -55,7 +55,7 @@ public FormatterRequest(Literal sourceLiteral, string variable, string? formatte /// /// The name of the formatter. /// - public string? FormatterName { get; private set; } + public string? FormatterName { get; } /// /// Gets the source literal. @@ -63,7 +63,7 @@ public FormatterRequest(Literal sourceLiteral, string variable, string? formatte /// /// The source literal. /// - public Literal SourceLiteral { get; private set; } + public Literal SourceLiteral { get; } /// /// Gets the variable name. Never null. @@ -71,7 +71,7 @@ public FormatterRequest(Literal sourceLiteral, string variable, string? formatte /// /// The variable. /// - public string Variable { get; private set; } + public string Variable { get; } #endregion diff --git a/src/Jeffijoe.MessageFormat/Formatting/IFormatterLibrary.cs b/src/Jeffijoe.MessageFormat/Formatting/IFormatterLibrary.cs index 11a4957..82ee28c 100644 --- a/src/Jeffijoe.MessageFormat/Formatting/IFormatterLibrary.cs +++ b/src/Jeffijoe.MessageFormat/Formatting/IFormatterLibrary.cs @@ -23,7 +23,7 @@ public interface IFormatterLibrary : IList /// /// The . /// - IFormatter? GetFormatter(FormatterRequest request); + IFormatter GetFormatter(FormatterRequest request); #endregion } diff --git a/src/Jeffijoe.MessageFormat/MessageFormatter.cs b/src/Jeffijoe.MessageFormat/MessageFormatter.cs index 4ba9229..46a79f5 100644 --- a/src/Jeffijoe.MessageFormat/MessageFormatter.cs +++ b/src/Jeffijoe.MessageFormat/MessageFormatter.cs @@ -215,17 +215,12 @@ public string FormatMessage(string pattern, IDictionary args) */ var sourceBuilder = new StringBuilder(pattern); var requests = this.ParseRequests(pattern, sourceBuilder); - var requestsEnumerated = requests; - for (int i = 0; i < requestsEnumerated.Count; i++) + for (int i = 0; i < requests.Count; i++) { - var request = requestsEnumerated[i]; + var request = requests[i]; var formatter = this.Formatters.GetFormatter(request); - if (formatter == null) - { - throw new FormatterNotFoundException(request); - } if (args.TryGetValue(request.Variable, out var value) == false && formatter.VariableMustExist) { @@ -298,6 +293,7 @@ protected internal StringBuilder UnescapeLiterals(StringBuilder sourceBuilder) const char EscapingChar = '\''; const char OpenBrace = '{'; const char CloseBrace = '}'; + var braceBalance = 0; var insideEscapeSequence = false; @@ -397,6 +393,6 @@ private IFormatterRequestCollection ParseRequests(string pattern, StringBuilder return requests; } - #endregion +#endregion } } \ No newline at end of file From 753fa1e7529fc373f08b23a18848e06f839c1866 Mon Sep 17 00:00:00 2001 From: Kostiantyn Sharovarskyi Date: Tue, 27 Apr 2021 04:16:49 +0300 Subject: [PATCH 42/98] Pool StringBuilders --- .../Formatters/PluralFormatterTests.cs | 2 +- .../Formatting/BaseFormatter.cs | 342 +++++++++--------- .../Formatting/Formatters/PluralFormatter.cs | 118 +++--- .../Jeffijoe.MessageFormat.csproj | 3 +- .../MessageFormatter.cs | 171 +++++---- .../Parsing/PatternParser.cs | 126 ++++--- .../StringBuilderPool.cs | 27 ++ 7 files changed, 434 insertions(+), 355 deletions(-) create mode 100644 src/Jeffijoe.MessageFormat/StringBuilderPool.cs diff --git a/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/PluralFormatterTests.cs b/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/PluralFormatterTests.cs index d8a1f6b..3eae442 100644 --- a/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/PluralFormatterTests.cs +++ b/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/PluralFormatterTests.cs @@ -71,7 +71,7 @@ public void Pluralize(double n, string expected) public void ReplaceNumberLiterals(string input, string expected) { var subject = new PluralFormatter(); - var actual = subject.ReplaceNumberLiterals(new StringBuilder(input), 1337); + var actual = subject.ReplaceNumberLiterals(input, 1337); Assert.Equal(expected, actual); } diff --git a/src/Jeffijoe.MessageFormat/Formatting/BaseFormatter.cs b/src/Jeffijoe.MessageFormat/Formatting/BaseFormatter.cs index e1f97ea..da1b4ad 100644 --- a/src/Jeffijoe.MessageFormat/Formatting/BaseFormatter.cs +++ b/src/Jeffijoe.MessageFormat/Formatting/BaseFormatter.cs @@ -4,6 +4,7 @@ // Copyright (C) Jeff Hansen 2014. All rights reserved. using System.Collections.Generic; +using System.IO; using System.Linq; using System.Text; @@ -62,55 +63,63 @@ protected internal IEnumerable ParseExtensions(FormatterRequ int length = request.FormatterArguments.Length; index = 0; - var extension = new StringBuilder(); - var value = new StringBuilder(); + var extension = StringBuilderPool.Get(); + var value = StringBuilderPool.Get(); - const char Colon = ':'; - bool foundExtension = false; - for (int i = 0; i < length; i++) + try { - var c = request.FormatterArguments[i]; - - // Whitespace is tolerated at the beginning. - bool isWhiteSpace = char.IsWhiteSpace(c); - if (isWhiteSpace) + const char Colon = ':'; + bool foundExtension = false; + for (int i = 0; i < length; i++) { - // We've reached the end - if (value.Length > 0) + var c = request.FormatterArguments[i]; + + // Whitespace is tolerated at the beginning. + bool isWhiteSpace = char.IsWhiteSpace(c); + if (isWhiteSpace) { - foundExtension = false; - result.Add(new FormatterExtension(extension.ToString(), value.ToString())); - extension.Clear(); - value.Clear(); - index = i; + // We've reached the end + if (value.Length > 0) + { + foundExtension = false; + result.Add(new FormatterExtension(extension.ToString(), value.ToString())); + extension.Clear(); + value.Clear(); + index = i; + continue; + } + + if (extension.Length > 0) + { + // It's not an extension, so we're done looking. + break; + } + continue; } - if (extension.Length > 0) + if (c == Colon) { - // It's not an extension, so we're done looking. - break; + foundExtension = true; + continue; } - continue; - } - - if (c == Colon) - { - foundExtension = true; - continue; - } + if (foundExtension) + { + value.Append(c); + continue; + } - if (foundExtension) - { - value.Append(c); - continue; + extension.Append(c); } - extension.Append(c); + return result; + } + finally + { + StringBuilderPool.Return(extension); + StringBuilderPool.Return(value); } - - return result; } /// @@ -132,8 +141,6 @@ protected internal IEnumerable ParseKeyedBlocks(FormatterRequest req const char EscapingChar = '\''; var result = new List(); - var key = new StringBuilder(); - var block = new StringBuilder(); var braceBalance = 0; var foundWhitespaceAfterKey = false; var insideEscapeSequence = false; @@ -141,178 +148,191 @@ protected internal IEnumerable ParseKeyedBlocks(FormatterRequest req { return Enumerable.Empty(); } - - for (int i = startIndex; i < request.FormatterArguments.Length; i++) - { - var c = request.FormatterArguments[i]; - var isWhitespace = char.IsWhiteSpace(c); - if (c == EscapingChar) + var key = StringBuilderPool.Get(); + var block = StringBuilderPool.Get(); + + try + { + for (int i = startIndex; i < request.FormatterArguments.Length; i++) { - if (braceBalance == 0) - { - throw new MalformedLiteralException( - "Expected a key, but found start of a escape sequence.", - 0, - 0, - request.FormatterArguments); - } + var c = request.FormatterArguments[i]; + var isWhitespace = char.IsWhiteSpace(c); - if (i == request.FormatterArguments.Length - 1) + if (c == EscapingChar) { - if (!insideEscapeSequence) + if (braceBalance == 0) + { + throw new MalformedLiteralException( + "Expected a key, but found start of a escape sequence.", + 0, + 0, + request.FormatterArguments); + } + + if (i == request.FormatterArguments.Length - 1) + { + if (!insideEscapeSequence) + block.Append(EscapingChar); + + // The last char can't open a new escape sequence, it can only close one + if (insideEscapeSequence) + { + insideEscapeSequence = false; + } + + continue; + } + + var nextChar = request.FormatterArguments[i + 1]; + if (nextChar == EscapingChar) + { + block.Append(EscapingChar); block.Append(EscapingChar); + ++i; + continue; + } - // The last char can't open a new escape sequence, it can only close one if (insideEscapeSequence) { + block.Append(EscapingChar); insideEscapeSequence = false; + continue; + } + + if (nextChar == '{' || nextChar == '}' || nextChar == '#') + { + block.Append(EscapingChar); + block.Append(nextChar); + insideEscapeSequence = true; + ++i; + continue; } - continue; - } - var nextChar = request.FormatterArguments[i + 1]; - if (nextChar == EscapingChar) - { - block.Append(EscapingChar); block.Append(EscapingChar); - ++i; continue; } if (insideEscapeSequence) { - block.Append(EscapingChar); - insideEscapeSequence = false; + block.Append(c); continue; } - if (nextChar == '{' || nextChar == '}' || nextChar == '#') + if (c == OpenBrace) { - block.Append(EscapingChar); - block.Append(nextChar); - insideEscapeSequence = true; - ++i; - continue; - } - - block.Append(EscapingChar); - continue; - } + if (key.Length == 0) + { + throw new MalformedLiteralException( + "Expected a key, but found start of a new block.", + 0, + 0, + request.FormatterArguments); + } - if (insideEscapeSequence) - { - block.Append(c); - continue; - } + braceBalance++; + if (braceBalance > 1) + { + block.Append(c); + } - if (c == OpenBrace) - { - if (key.Length == 0) - { - throw new MalformedLiteralException( - "Expected a key, but found start of a new block.", - 0, - 0, - request.FormatterArguments); + continue; } - braceBalance++; - if (braceBalance > 1) + if (c == CloseBrace) { - block.Append(c); - } + if (key.Length == 0) + { + throw new MalformedLiteralException( + "Expected a key, but found end of a block.", + 0, + 0, + request.FormatterArguments); + } - continue; - } + if (braceBalance == 0) + { + throw new MalformedLiteralException( + "Found end of a block, but no block has been started, or the" + + " block has already been closed. " + + "This could indicate an unescaped brace somewhere.", + 0, + 0, + request.FormatterArguments); + } - if (c == CloseBrace) - { - if (key.Length == 0) - { - throw new MalformedLiteralException( - "Expected a key, but found end of a block.", - 0, - 0, - request.FormatterArguments); - } + braceBalance--; + if (braceBalance == 0) + { + result.Add(new KeyedBlock(key.ToString(), block.ToString())); + block.Clear(); + key.Clear(); + foundWhitespaceAfterKey = false; + continue; + } - if (braceBalance == 0) - { - throw new MalformedLiteralException( - "Found end of a block, but no block has been started, or the" - + " block has already been closed. " + "This could indicate an unescaped brace somewhere.", - 0, - 0, - request.FormatterArguments); + if (braceBalance < 0) + { + throw new MalformedLiteralException( + "Expected '{', but found '}' - essentially this means there are more close braces than there are open braces.", + 0, + 0, + request.FormatterArguments); + } } - braceBalance--; - if (braceBalance == 0) + // If we are inside a block, append to the block buffer + if (braceBalance > 0) { - result.Add(new KeyedBlock(key.ToString(), block.ToString())); - block.Clear(); - key.Clear(); - foundWhitespaceAfterKey = false; + block.Append(c); continue; } - if (braceBalance < 0) + // Else, we are buffering our key + if (isWhitespace == false) + { + if (foundWhitespaceAfterKey) + { + throw new MalformedLiteralException( + "Any whitespace after a key should be followed by the beginning of a block.", + 0, + 0, + request.FormatterArguments); + } + + key.Append(c); + } + else if (key.Length > 0) { - throw new MalformedLiteralException( - "Expected '{', but found '}' - essentially this means there are more close braces than there are open braces.", - 0, - 0, - request.FormatterArguments); + foundWhitespaceAfterKey = true; } } - // If we are inside a block, append to the block buffer - if (braceBalance > 0) + if (insideEscapeSequence) { - block.Append(c); - continue; + throw new MalformedLiteralException( + "There is an unclosed escape sequence.", + 0, + 0, + request.FormatterArguments); } - // Else, we are buffering our key - if (isWhitespace == false) - { - if (foundWhitespaceAfterKey) - { - throw new MalformedLiteralException( - "Any whitespace after a key should be followed by the beginning of a block.", - 0, - 0, - request.FormatterArguments); - } - - key.Append(c); - } - else if (key.Length > 0) + if (braceBalance > 0) { - foundWhitespaceAfterKey = true; + throw new MalformedLiteralException( + "There are more open braces than there are close braces.", + 0, + 0, + request.FormatterArguments); } - } - if (insideEscapeSequence) - { - throw new MalformedLiteralException( - "There is an unclosed escape sequence.", - 0, - 0, - request.FormatterArguments); + return result; } - - if (braceBalance > 0) + finally { - throw new MalformedLiteralException( - "There are more open braces than there are close braces.", - 0, - 0, - request.FormatterArguments); + StringBuilderPool.Return(key); + StringBuilderPool.Return(block); } - - return result; } #endregion diff --git a/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralFormatter.cs b/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralFormatter.cs index 82842aa..50e91d7 100644 --- a/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralFormatter.cs +++ b/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralFormatter.cs @@ -101,7 +101,7 @@ public string Format( } var ctx = CreatePluralContext(value, offset); - var pluralized = new StringBuilder(this.Pluralize(locale, arguments, ctx, offset)); + var pluralized = this.Pluralize(locale, arguments, ctx, offset); var result = this.ReplaceNumberLiterals(pluralized, ctx.Number); var formatted = messageFormatter.FormatMessage(result, args); return formatted; @@ -220,86 +220,94 @@ internal string Pluralize(string locale, ParsedArguments arguments, PluralContex /// /// The . /// - internal string ReplaceNumberLiterals(StringBuilder pluralized, double n) + internal string ReplaceNumberLiterals(string pluralized, double n) { - // I've done this a few times now.. - const char OpenBrace = '{'; - const char CloseBrace = '}'; - const char Pound = '#'; - const char EscapeChar = '\''; - var braceBalance = 0; - var insideEscapeSequence = false; - var sb = new StringBuilder(); - for (int i = 0; i < pluralized.Length; i++) - { - var c = pluralized[i]; + var sb = StringBuilderPool.Get(); - if (c == EscapeChar) + try + { + // I've done this a few times now.. + const char OpenBrace = '{'; + const char CloseBrace = '}'; + const char Pound = '#'; + const char EscapeChar = '\''; + var braceBalance = 0; + var insideEscapeSequence = false; + for (int i = 0; i < pluralized.Length; i++) { - sb.Append(EscapeChar); + var c = pluralized[i]; - if (i == pluralized.Length - 1) + if (c == EscapeChar) { - // The last char can't open a new escape sequence, it can only close one + sb.Append(EscapeChar); + + if (i == pluralized.Length - 1) + { + // The last char can't open a new escape sequence, it can only close one + if (insideEscapeSequence) + { + insideEscapeSequence = false; + } + + continue; + } + + var nextChar = pluralized[i + 1]; + if (nextChar == EscapeChar) + { + sb.Append(EscapeChar); + ++i; + continue; + } + if (insideEscapeSequence) { insideEscapeSequence = false; + continue; } - continue; - } + if (nextChar == '{' || nextChar == '}' || nextChar == '#') + { + sb.Append(nextChar); + insideEscapeSequence = true; + ++i; + } - var nextChar = pluralized[i + 1]; - if (nextChar == EscapeChar) - { - sb.Append(EscapeChar); - ++i; continue; } if (insideEscapeSequence) { - insideEscapeSequence = false; + sb.Append(c); continue; } - if (nextChar == '{' || nextChar == '}' || nextChar == '#') + if (c == OpenBrace) { - sb.Append(nextChar); - insideEscapeSequence = true; - ++i; + braceBalance++; } - - continue; - } - - if (insideEscapeSequence) - { - sb.Append(c); - continue; - } - - if (c == OpenBrace) - { - braceBalance++; - } - else if (c == CloseBrace) - { - braceBalance--; - } - else if (c == Pound) - { - if (braceBalance == 0) + else if (c == CloseBrace) { - sb.Append(n); - continue; + braceBalance--; } + else if (c == Pound) + { + if (braceBalance == 0) + { + sb.Append(n); + continue; + } + } + + sb.Append(c); } - sb.Append(c); + return sb.ToString(); + } + finally + { + StringBuilderPool.Return(sb); } - - return sb.ToString(); } /// diff --git a/src/Jeffijoe.MessageFormat/Jeffijoe.MessageFormat.csproj b/src/Jeffijoe.MessageFormat/Jeffijoe.MessageFormat.csproj index 2003382..3b850a1 100644 --- a/src/Jeffijoe.MessageFormat/Jeffijoe.MessageFormat.csproj +++ b/src/Jeffijoe.MessageFormat/Jeffijoe.MessageFormat.csproj @@ -10,7 +10,7 @@ https://github.com/jeffijoe/messageformat.net latest enable - netstandard2.0;net5.0 + net5.0;netstandard2.0 @@ -18,6 +18,7 @@ + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Jeffijoe.MessageFormat/MessageFormatter.cs b/src/Jeffijoe.MessageFormat/MessageFormatter.cs index 46a79f5..6697111 100644 --- a/src/Jeffijoe.MessageFormat/MessageFormatter.cs +++ b/src/Jeffijoe.MessageFormat/MessageFormatter.cs @@ -213,41 +213,48 @@ public string FormatMessage(string pattern, IDictionary args) * We are asuming the formatters are ordered correctly * - that is, from left to right, string-wise. */ - var sourceBuilder = new StringBuilder(pattern); - var requests = this.ParseRequests(pattern, sourceBuilder); + var sourceBuilder = StringBuilderPool.Get(); - for (int i = 0; i < requests.Count; i++) + try { - var request = requests[i]; + sourceBuilder.Append(pattern); + var requests = this.ParseRequests(pattern, sourceBuilder); - var formatter = this.Formatters.GetFormatter(request); - - if (args.TryGetValue(request.Variable, out var value) == false && formatter.VariableMustExist) + for (int i = 0; i < requests.Count; i++) { - throw new VariableNotFoundException(request.Variable); - } + var request = requests[i]; - // Double dispatch, yeah! - var result = formatter.Format(this.Locale, request, args, value, this); + var formatter = this.Formatters.GetFormatter(request); - // First, we remove the literal from the source. - Literal sourceLiteral = request.SourceLiteral; + if (args.TryGetValue(request.Variable, out var value) == false && formatter.VariableMustExist) + { + throw new VariableNotFoundException(request.Variable); + } - // +1 because we want to include the last index. - var length = (sourceLiteral.EndIndex - sourceLiteral.StartIndex) + 1; - sourceBuilder.Remove(sourceLiteral.StartIndex, length); + // Double dispatch, yeah! + var result = formatter.Format(this.Locale, request, args, value, this); - // Now, we inject the result. - sourceBuilder.Insert(sourceLiteral.StartIndex, result); + // First, we remove the literal from the source. + Literal sourceLiteral = request.SourceLiteral; - // The next requests will want to know what happened. - requests.ShiftIndices(i, result.Length); - } + // +1 because we want to include the last index. + var length = (sourceLiteral.EndIndex - sourceLiteral.StartIndex) + 1; + sourceBuilder.Remove(sourceLiteral.StartIndex, length); - sourceBuilder = this.UnescapeLiterals(sourceBuilder); + // Now, we inject the result. + sourceBuilder.Insert(sourceLiteral.StartIndex, result); - // And we're done. - return sourceBuilder.ToString(); + // The next requests will want to know what happened. + requests.ShiftIndices(i, result.Length); + } + + // And we're done. + return this.UnescapeLiterals(sourceBuilder); + } + finally + { + StringBuilderPool.Return(sourceBuilder); + } } /// @@ -280,81 +287,89 @@ public string FormatMessage(string pattern, object args) /// /// The . /// - protected internal StringBuilder UnescapeLiterals(StringBuilder sourceBuilder) + protected internal string UnescapeLiterals(StringBuilder sourceBuilder) { // If the block is empty, do nothing. if (sourceBuilder.Length == 0) { - return sourceBuilder; + return string.Empty; } - var dest = new StringBuilder(sourceBuilder.Length, sourceBuilder.Length); - int length = sourceBuilder.Length; - const char EscapingChar = '\''; - const char OpenBrace = '{'; - const char CloseBrace = '}'; - - var braceBalance = 0; - var insideEscapeSequence = false; + var dest = StringBuilderPool.Get(); - for (int i = 0; i < length; i++) + try { - var c = sourceBuilder[i]; + int length = sourceBuilder.Length; + const char EscapingChar = '\''; + const char OpenBrace = '{'; + const char CloseBrace = '}'; - if (c == EscapingChar) + var braceBalance = 0; + var insideEscapeSequence = false; + + for (int i = 0; i < length; i++) { - if (braceBalance == 0) + var c = sourceBuilder[i]; + + if (c == EscapingChar) { - if (i == length - 1) + if (braceBalance == 0) { - if (!insideEscapeSequence) + if (i == length - 1) + { + if (!insideEscapeSequence) + dest.Append(EscapingChar); + continue; + } + + var nextChar = sourceBuilder[i + 1]; + if (nextChar == EscapingChar) + { dest.Append(EscapingChar); - continue; - } + ++i; + continue; + } + + if (insideEscapeSequence) + { + insideEscapeSequence = false; + continue; + } + + if (nextChar == '{' || nextChar == '}' || nextChar == '#') + { + dest.Append(nextChar); + insideEscapeSequence = true; + ++i; + continue; + } - var nextChar = sourceBuilder[i + 1]; - if (nextChar == EscapingChar) - { dest.Append(EscapingChar); - ++i; continue; } - - if (insideEscapeSequence) - { - insideEscapeSequence = false; - continue; - } - - if (nextChar == '{' || nextChar == '}' || nextChar == '#') - { - dest.Append(nextChar); - insideEscapeSequence = true; - ++i; - continue; - } - - dest.Append(EscapingChar); - continue; } - } - else if (insideEscapeSequence) - { - // fall through to append - } - else if (c == OpenBrace) - { - braceBalance++; - } - else if (c == CloseBrace) - { - braceBalance--; + else if (insideEscapeSequence) + { + // fall through to append + } + else if (c == OpenBrace) + { + braceBalance++; + } + else if (c == CloseBrace) + { + braceBalance--; + } + + dest.Append(c); } - dest.Append(c); + return dest.ToString(); + } + finally + { + StringBuilderPool.Return(dest); } - - return dest; } /// diff --git a/src/Jeffijoe.MessageFormat/Parsing/PatternParser.cs b/src/Jeffijoe.MessageFormat/Parsing/PatternParser.cs index da3101a..a89f438 100644 --- a/src/Jeffijoe.MessageFormat/Parsing/PatternParser.cs +++ b/src/Jeffijoe.MessageFormat/Parsing/PatternParser.cs @@ -122,85 +122,93 @@ public IFormatterRequestCollection Parse(StringBuilder source) out int lastIndex) { const char Comma = ','; - var sb = new StringBuilder(); - var innerText = literal.InnerText; - var column = literal.SourceColumnNumber; - var foundWhitespace = false; - lastIndex = 0; - for (var i = offset; i < innerText.Length; i++) - { - var c = innerText[i]; - column++; - lastIndex = i; - if (c == Comma) - { - break; - } + var sb = StringBuilderPool.Get(); - // Disregard whitespace. - var whitespace = c == ' ' || c == '\r' || c == '\n' || c == '\t'; - if (!whitespace) + try + { + var innerText = literal.InnerText; + var column = literal.SourceColumnNumber; + var foundWhitespace = false; + lastIndex = 0; + for (var i = offset; i < innerText.Length; i++) { - if (c.IsAlphaNumeric() == false) + var c = innerText[i]; + column++; + lastIndex = i; + if (c == Comma) { - var msg = string.Format("Invalid literal character '{0}'.", c); - - // Line number can't have changed. - throw new MalformedLiteralException(msg, literal.SourceLineNumber, column, - innerText.ToString()); + break; } - } - else - { - foundWhitespace = true; - } - sb.Append(c); - } + // Disregard whitespace. + var whitespace = c == ' ' || c == '\r' || c == '\n' || c == '\t'; + if (!whitespace) + { + if (c.IsAlphaNumeric() == false) + { + var msg = string.Format("Invalid literal character '{0}'.", c); + + // Line number can't have changed. + throw new MalformedLiteralException(msg, literal.SourceLineNumber, column, + innerText.ToString()); + } + } + else + { + foundWhitespace = true; + } - if (sb.Length != 0) - { - // Trim whitespace from beginning and end of the string, if necessary. - if (!foundWhitespace) - { - return sb.ToString(); + sb.Append(c); } - StringBuilder trimmed = sb.TrimWhitespace(); - if (trimmed.Length == 0) + if (sb.Length != 0) { - if (allowEmptyResult) + // Trim whitespace from beginning and end of the string, if necessary. + if (!foundWhitespace) + { + return sb.ToString(); + } + + StringBuilder trimmed = sb.TrimWhitespace(); + if (trimmed.Length == 0) { - return null; + if (allowEmptyResult) + { + return null; + } + + throw new MalformedLiteralException( + "Parsing the literal yielded a string that was pure whitespace.", + literal.SourceLineNumber, + column); } - throw new MalformedLiteralException( - "Parsing the literal yielded a string that was pure whitespace.", - literal.SourceLineNumber, - column); + if (trimmed.ContainsWhitespace()) + { + throw new MalformedLiteralException( + "Parsed literal must not contain whitespace.", + 0, + 0, + trimmed.ToString()); + } + + return sb.ToString(); } - if (trimmed.ContainsWhitespace()) + if (allowEmptyResult) { - throw new MalformedLiteralException( - "Parsed literal must not contain whitespace.", - 0, - 0, - trimmed.ToString()); + return null; } - return sb.ToString(); + throw new MalformedLiteralException( + "Parsing the literal yielded an empty string.", + literal.SourceLineNumber, + column); } - - if (allowEmptyResult) + finally { - return null; + StringBuilderPool.Return(sb); } - - throw new MalformedLiteralException( - "Parsing the literal yielded an empty string.", - literal.SourceLineNumber, - column); } #endregion diff --git a/src/Jeffijoe.MessageFormat/StringBuilderPool.cs b/src/Jeffijoe.MessageFormat/StringBuilderPool.cs new file mode 100644 index 0000000..12368af --- /dev/null +++ b/src/Jeffijoe.MessageFormat/StringBuilderPool.cs @@ -0,0 +1,27 @@ +using System; +using System.Text; +using Microsoft.Extensions.ObjectPool; + +namespace Jeffijoe.MessageFormat +{ + internal static class StringBuilderPool + { + private static readonly ObjectPool SbPool; + + static StringBuilderPool() + { + var shared = new DefaultObjectPoolProvider(); + SbPool = shared.CreateStringBuilderPool(); + } + + public static StringBuilder Get() + { + return SbPool.Get(); + } + + public static void Return(StringBuilder sb) + { + SbPool.Return(sb); + } + } +} From 8649a6113f3ccf6b651cea8b2b885ba33f782907 Mon Sep 17 00:00:00 2001 From: Kostiantyn Sharovarskyi Date: Tue, 27 Apr 2021 04:21:07 +0300 Subject: [PATCH 43/98] Literal InnerText to string --- src/Jeffijoe.MessageFormat/Parsing/Literal.cs | 4 +- .../Parsing/LiteralParser.cs | 185 +++++++++--------- .../Parsing/PatternParser.cs | 2 +- 3 files changed, 100 insertions(+), 91 deletions(-) diff --git a/src/Jeffijoe.MessageFormat/Parsing/Literal.cs b/src/Jeffijoe.MessageFormat/Parsing/Literal.cs index 1c00115..22562c1 100644 --- a/src/Jeffijoe.MessageFormat/Parsing/Literal.cs +++ b/src/Jeffijoe.MessageFormat/Parsing/Literal.cs @@ -37,7 +37,7 @@ public Literal( int endIndex, int sourceLineNumber, int sourceColumnNumber, - StringBuilder innerText) + string innerText) { this.StartIndex = startIndex; this.EndIndex = endIndex; @@ -64,7 +64,7 @@ public Literal( /// /// The inner text. /// - public StringBuilder InnerText { get; private set; } + public string InnerText { get; private set; } /// /// Gets the source column number. diff --git a/src/Jeffijoe.MessageFormat/Parsing/LiteralParser.cs b/src/Jeffijoe.MessageFormat/Parsing/LiteralParser.cs index 689855d..5ac6ed8 100644 --- a/src/Jeffijoe.MessageFormat/Parsing/LiteralParser.cs +++ b/src/Jeffijoe.MessageFormat/Parsing/LiteralParser.cs @@ -36,7 +36,6 @@ public IEnumerable ParseLiterals(StringBuilder sb) var closeBraces = 0; var start = 0; var braceBalance = 0; - var matchTextBuf = new StringBuilder(); var lineNumber = 1; var startLineNumber = 1; var startColumnNumber = 0; @@ -46,135 +45,145 @@ public IEnumerable ParseLiterals(StringBuilder sb) var currentEscapeSequenceColumnNumber = 0; const char Cr = '\r'; // Carriage return const char Lf = '\n'; // Line feed - for (var i = 0; i < sb.Length; i++) + + var matchTextBuf = StringBuilderPool.Get(); + try { - var c = sb[i]; - if (c == Lf) + for (var i = 0; i < sb.Length; i++) { - lineNumber++; - columnNumber = 0; - continue; - } + var c = sb[i]; + if (c == Lf) + { + lineNumber++; + columnNumber = 0; + continue; + } - if (c == Cr) - { - continue; - } + if (c == Cr) + { + continue; + } - columnNumber++; + columnNumber++; - if (c == EscapingChar) - { - if (i == sb.Length - 1) + if (c == EscapingChar) { - if (!insideEscapeSequence) + if (i == sb.Length - 1) + { + if (!insideEscapeSequence) + matchTextBuf.Append(EscapingChar); + + // The last char can't open a new escape sequence, it can only close one + if (insideEscapeSequence) + { + insideEscapeSequence = false; + } + + continue; + } + + matchTextBuf.Append(EscapingChar); + + var nextChar = sb[i + 1]; + if (nextChar == EscapingChar) + { matchTextBuf.Append(EscapingChar); + ++i; + continue; + } - // The last char can't open a new escape sequence, it can only close one if (insideEscapeSequence) { insideEscapeSequence = false; + continue; } - continue; - } - matchTextBuf.Append(EscapingChar); + if (nextChar == '{' || nextChar == '}' || nextChar == '#') + { + matchTextBuf.Append(nextChar); + insideEscapeSequence = true; + currentEscapeSequenceLineNumber = lineNumber; + currentEscapeSequenceColumnNumber = columnNumber; + ++i; + } - var nextChar = sb[i + 1]; - if (nextChar == EscapingChar) - { - matchTextBuf.Append(EscapingChar); - ++i; continue; } if (insideEscapeSequence) { - insideEscapeSequence = false; + matchTextBuf.Append(c); continue; } - if (nextChar == '{' || nextChar == '}' || nextChar == '#') + if (c == OpenBrace) { - matchTextBuf.Append(nextChar); - insideEscapeSequence = true; - currentEscapeSequenceLineNumber = lineNumber; - currentEscapeSequenceColumnNumber = columnNumber; - ++i; - } - - continue; - } - - if (insideEscapeSequence) - { - matchTextBuf.Append(c); - continue; - } + openBraces++; + braceBalance++; - if (c == OpenBrace) - { - openBraces++; - braceBalance++; + // Record starting position of possible new brace match. + if (braceBalance == 1) + { + start = i; + startColumnNumber = columnNumber; + startLineNumber = lineNumber; + matchTextBuf.Clear(); + } + } - // Record starting position of possible new brace match. - if (braceBalance == 1) + if (c == CloseBrace) { - start = i; - startColumnNumber = columnNumber; - startLineNumber = lineNumber; - matchTextBuf.Clear(); + closeBraces++; + braceBalance--; + + // Write the brace to the match buffer if it's not the closing brace + // we are looking for. + if (braceBalance > 0) + { + matchTextBuf.Append(c); + } } - } + else + { + if (i > start && braceBalance > 0) + { + matchTextBuf.Append(c); + } - if (c == CloseBrace) - { - closeBraces++; - braceBalance--; + continue; + } - // Write the brace to the match buffer if it's not the closing brace - // we are looking for. - if (braceBalance > 0) + if (openBraces != closeBraces) { - matchTextBuf.Append(c); + continue; } + + // Passing in the text buffer instead of the actual string to avoid allocating a new string. + result.Add(new Literal(start, i, startLineNumber, startColumnNumber, matchTextBuf.ToString())); + matchTextBuf.Clear(); + start = 0; } - else - { - if (i > start && braceBalance > 0) - { - matchTextBuf.Append(c); - } - continue; + if (insideEscapeSequence) + { + throw new MalformedLiteralException( + "There is an unclosed escape sequence.", + currentEscapeSequenceLineNumber, + currentEscapeSequenceColumnNumber, + matchTextBuf.ToString()); } if (openBraces != closeBraces) { - continue; + throw new UnbalancedBracesException(openBraces, closeBraces); } - // Passing in the text buffer instead of the actual string to avoid allocating a new string. - result.Add(new Literal(start, i, startLineNumber, startColumnNumber, matchTextBuf)); - matchTextBuf = new StringBuilder(); - start = 0; + return result; } - - if (insideEscapeSequence) + finally { - throw new MalformedLiteralException( - "There is an unclosed escape sequence.", - currentEscapeSequenceLineNumber, - currentEscapeSequenceColumnNumber, - matchTextBuf.ToString()); + StringBuilderPool.Return(matchTextBuf); } - - if (openBraces != closeBraces) - { - throw new UnbalancedBracesException(openBraces, closeBraces); - } - - return result; } #endregion diff --git a/src/Jeffijoe.MessageFormat/Parsing/PatternParser.cs b/src/Jeffijoe.MessageFormat/Parsing/PatternParser.cs index a89f438..88de68b 100644 --- a/src/Jeffijoe.MessageFormat/Parsing/PatternParser.cs +++ b/src/Jeffijoe.MessageFormat/Parsing/PatternParser.cs @@ -82,7 +82,7 @@ public IFormatterRequestCollection Parse(StringBuilder source) if (formatterKey != null) { formatterArgs = - literal.InnerText.ToString(lastIndex + 1, literal.InnerText.Length - lastIndex - 1).Trim(); + literal.InnerText.Substring(lastIndex + 1, literal.InnerText.Length - lastIndex - 1).Trim(); } } From a2774d7fd2fa04c1ef597991c02248eb3372b624 Mon Sep 17 00:00:00 2001 From: Kostiantyn Sharovarskyi Date: Tue, 27 Apr 2021 04:36:16 +0300 Subject: [PATCH 44/98] Fix tests after StringBuilder changes --- .../Formatting/BaseFormatterTests.cs | 12 ++++----- .../Formatting/FormatterLibraryTests.cs | 2 +- .../Formatters/PluralFormatterTests.cs | 2 +- .../Formatters/SelectFormatterTests.cs | 2 +- .../Formatters/VariableFormatterTests.cs | 2 +- .../MessageFormatterTests.cs | 10 +++---- .../GeneratedPluralRulesTests.cs | 6 ++--- .../FormatterRequestCollectionTests.cs | 12 ++++----- .../Parsing/LiteralTests.cs | 4 +-- .../PatternParser/PatternParserGetKeyTests.cs | 16 ++++++------ .../PatternParser/PatternParserParseTests.cs | 2 +- .../MessageFormatter.cs | 26 ++++++++----------- 12 files changed, 46 insertions(+), 50 deletions(-) diff --git a/src/Jeffijoe.MessageFormat.Tests/Formatting/BaseFormatterTests.cs b/src/Jeffijoe.MessageFormat.Tests/Formatting/BaseFormatterTests.cs index 39121d0..7d4d687 100644 --- a/src/Jeffijoe.MessageFormat.Tests/Formatting/BaseFormatterTests.cs +++ b/src/Jeffijoe.MessageFormat.Tests/Formatting/BaseFormatterTests.cs @@ -145,7 +145,7 @@ public void ParseArguments( string[] blocks) { var subject = new BaseFormatterImpl(); - var req = new FormatterRequest(new Literal(1, 1, 1, 1, new StringBuilder()), string.Empty, null, args); + var req = new FormatterRequest(new Literal(1, 1, 1, 1, ""), string.Empty, null, args); var actual = subject.ParseArguments(req); Assert.Equal(extensionKeys.Length, actual.Extensions.Count()); @@ -183,7 +183,7 @@ public void ParseArguments( public void ParseArguments_invalid(string args) { var subject = new BaseFormatterImpl(); - var req = new FormatterRequest(new Literal(1, 1, 1, 1, new StringBuilder()), string.Empty, null, args); + var req = new FormatterRequest(new Literal(1, 1, 1, 1, ""), string.Empty, null, args); var ex = Assert.Throws(() => subject.ParseArguments(req)); this.outputHelper.WriteLine(ex.Message); } @@ -210,7 +210,7 @@ public void ParseExtensions(string args, string extension, string value, int exp { var subject = new BaseFormatterImpl(); int index; - var req = new FormatterRequest(new Literal(1, 1, 1, 1, new StringBuilder()), string.Empty, null, args); + var req = new FormatterRequest(new Literal(1, 1, 1, 1, ""), string.Empty, null, args); // Warmup subject.ParseExtensions(req, out index); @@ -242,7 +242,7 @@ public void ParseExtensions_multiple() var args = " offset:2 code:js "; var expectedIndex = 17; - var req = new FormatterRequest(new Literal(1, 1, 1, 1, new StringBuilder()), string.Empty, null, args); + var req = new FormatterRequest(new Literal(1, 1, 1, 1, ""), string.Empty, null, args); var actual = subject.ParseExtensions(req, out index); Assert.NotEmpty(actual); @@ -274,7 +274,7 @@ public void ParseExtensions_multiple() public void ParseKeyedBlocks(string args, string[] keys, string[] values) { var subject = new BaseFormatterImpl(); - var req = new FormatterRequest(new Literal(1, 1, 1, 1, new StringBuilder()), string.Empty, null, args); + var req = new FormatterRequest(new Literal(1, 1, 1, 1, ""), string.Empty, null, args); // Warm-up subject.ParseKeyedBlocks(req, 0); @@ -316,7 +316,7 @@ public void ParseKeyedBlocks(string args, string[] keys, string[] values) public void ParseKeyedBlocks_unclosed_escape_sequence(string args) { var subject = new BaseFormatterImpl(); - var req = new FormatterRequest(new Literal(1, 1, 1, 1, new StringBuilder()), string.Empty, null, args); + var req = new FormatterRequest(new Literal(1, 1, 1, 1, ""), string.Empty, null, args); Assert.Throws(() => subject.ParseKeyedBlocks(req, 0)); } diff --git a/src/Jeffijoe.MessageFormat.Tests/Formatting/FormatterLibraryTests.cs b/src/Jeffijoe.MessageFormat.Tests/Formatting/FormatterLibraryTests.cs index 163888c..fea2fb8 100644 --- a/src/Jeffijoe.MessageFormat.Tests/Formatting/FormatterLibraryTests.cs +++ b/src/Jeffijoe.MessageFormat.Tests/Formatting/FormatterLibraryTests.cs @@ -32,7 +32,7 @@ public void GetFormatter() var mock1 = new Mock(); var mock2 = new Mock(); - var req = new FormatterRequest(new Literal(1, 1, 1, 1, new StringBuilder()), "test", "dawg", null); + var req = new FormatterRequest(new Literal(1, 1, 1, 1, ""), "test", "dawg", null); subject.Add(mock1.Object); subject.Add(mock2.Object); diff --git a/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/PluralFormatterTests.cs b/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/PluralFormatterTests.cs index 3eae442..db3823c 100644 --- a/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/PluralFormatterTests.cs +++ b/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/PluralFormatterTests.cs @@ -49,7 +49,7 @@ public void Pluralize(double n, string expected) new KeyedBlock("other", "wow") }, new FormatterExtension[0]); - var request = new FormatterRequest(new Literal(1, 1, 1, 1, new StringBuilder()), "test", "plural", null); + var request = new FormatterRequest(new Literal(1, 1, 1, 1, ""), "test", "plural", null); var actual = subject.Pluralize("en", arguments, new PluralContext(Convert.ToDecimal(Convert.ToDouble(args[request.Variable]))), 0); Assert.Equal(expected, actual); } diff --git a/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/SelectFormatterTests.cs b/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/SelectFormatterTests.cs index 7472463..c4be48d 100644 --- a/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/SelectFormatterTests.cs +++ b/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/SelectFormatterTests.cs @@ -62,7 +62,7 @@ public void Format(string formatterArgs, string gender, string expectedBlock) messageFormatterMock.Setup(x => x.FormatMessage(It.IsAny(), It.IsAny>())) .Returns((string input, Dictionary a) => input); var req = new FormatterRequest( - new Literal(1, 1, 1, 1, new StringBuilder()), + new Literal(1, 1, 1, 1, ""), "gender", "select", formatterArgs); diff --git a/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/VariableFormatterTests.cs b/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/VariableFormatterTests.cs index 121ece2..f97924f 100644 --- a/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/VariableFormatterTests.cs +++ b/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/VariableFormatterTests.cs @@ -87,7 +87,7 @@ public void VerifyTheValueFromTheGivenArgumentsIsReturnedAsAString() /// private static FormatterRequest CreateRequest() { - var req = new FormatterRequest(new Literal(1, 10, 1, 1, new StringBuilder()), "test", null, null); + var req = new FormatterRequest(new Literal(1, 10, 1, 1, ""), "test", null, null); return req; } diff --git a/src/Jeffijoe.MessageFormat.Tests/MessageFormatterTests.cs b/src/Jeffijoe.MessageFormat.Tests/MessageFormatterTests.cs index 167822b..a7c3202 100644 --- a/src/Jeffijoe.MessageFormat.Tests/MessageFormatterTests.cs +++ b/src/Jeffijoe.MessageFormat.Tests/MessageFormatterTests.cs @@ -87,12 +87,12 @@ public void FormatMessage() var requests = new[] { new FormatterRequest( - new Literal(0, 5, 1, 7, new StringBuilder("name")), + new Literal(0, 5, 1, 7, "name"), "name", null, null), new FormatterRequest( - new Literal(11, 33, 1, 7, new StringBuilder("messages, plural, 123")), + new Literal(11, 33, 1, 7, "messages, plural, 123"), "messages", "plural", " 123") @@ -153,12 +153,12 @@ public void VerifyFormatMessageThrowsWhenVariablesAreMissingAndTheFormatterRequi var requests = new[] { new FormatterRequest( - new Literal(0, 5, 1, 7, new StringBuilder("name")), + new Literal(0, 5, 1, 7, "name"), "name", null, null), new FormatterRequest( - new Literal(11, 33, 1, 7, new StringBuilder("messages, plural, 123")), + new Literal(11, 33, 1, 7, "messages, plural, 123"), "messages", "plural", " 123") @@ -194,7 +194,7 @@ public void VerifyFormatMessageAllowsNonExistentVariablesWhenFormatterAllowsIt() var requests = new[] { new FormatterRequest( - new Literal(0, 5, 1, 7, new StringBuilder("name")), + new Literal(0, 5, 1, 7, "name"), "name", null, null), diff --git a/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/GeneratedPluralRulesTests.cs b/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/GeneratedPluralRulesTests.cs index 18ffcc7..7f8606a 100644 --- a/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/GeneratedPluralRulesTests.cs +++ b/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/GeneratedPluralRulesTests.cs @@ -30,7 +30,7 @@ public void Uk_PluralizerTests(double n, string expected) new KeyedBlock("other", "дня") }, new FormatterExtension[0]); - var request = new FormatterRequest(new Literal(1, 1, 1, 1, new StringBuilder()), "test", "plural", null); + var request = new FormatterRequest(new Literal(1, 1, 1, 1, ""), "test", "plural", null); var actual = subject.Pluralize("uk", arguments, new PluralContext(Convert.ToDecimal(Convert.ToDouble(args[request.Variable]))), 0); Assert.Equal(expected, actual); } @@ -55,7 +55,7 @@ public void Ru_PluralizerTests(double n, string expected) new KeyedBlock("other", "дня") }, new FormatterExtension[0]); - var request = new FormatterRequest(new Literal(1, 1, 1, 1, new StringBuilder()), "test", "plural", null); + var request = new FormatterRequest(new Literal(1, 1, 1, 1, ""), "test", "plural", null); var actual = subject.Pluralize("ru", arguments, new PluralContext(Convert.ToDecimal(args[request.Variable])), 0); Assert.Equal(expected, actual); } @@ -78,7 +78,7 @@ public void En_PluralizerTests(double n, string expected) new KeyedBlock("other", "days") }, new FormatterExtension[0]); - var request = new FormatterRequest(new Literal(1, 1, 1, 1, new StringBuilder()), "test", "plural", null); + var request = new FormatterRequest(new Literal(1, 1, 1, 1, ""), "test", "plural", null); var actual = subject.Pluralize("en", arguments, new PluralContext(Convert.ToDecimal(args[request.Variable])), 0); Assert.Equal(expected, actual); } diff --git a/src/Jeffijoe.MessageFormat.Tests/Parsing/FormatterRequestCollectionTests.cs b/src/Jeffijoe.MessageFormat.Tests/Parsing/FormatterRequestCollectionTests.cs index 589391d..7508731 100644 --- a/src/Jeffijoe.MessageFormat.Tests/Parsing/FormatterRequestCollectionTests.cs +++ b/src/Jeffijoe.MessageFormat.Tests/Parsing/FormatterRequestCollectionTests.cs @@ -30,19 +30,19 @@ public void Clone() var subject = new FormatterRequestCollection(); subject.Add( new FormatterRequest( - new Literal(0, 9, 1, 1, new StringBuilder(new string('a', 10))), + new Literal(0, 9, 1, 1, new string('a', 10)), "test", "test", "test")); subject.Add( new FormatterRequest( - new Literal(10, 19, 1, 1, new StringBuilder(new string('a', 10))), + new Literal(10, 19, 1, 1, new string('a', 10)), "test", "test", "test")); subject.Add( new FormatterRequest( - new Literal(20, 29, 1, 1, new StringBuilder(new string('a', 10))), + new Literal(20, 29, 1, 1, new string('a', 10)), "test", "test", "test")); @@ -67,19 +67,19 @@ public void ShiftIndices() var subject = new FormatterRequestCollection(); subject.Add( new FormatterRequest( - new Literal(0, 9, 1, 1, new StringBuilder(new string('a', 10))), + new Literal(0, 9, 1, 1, new string('a', 10)), "test", "test", "test")); subject.Add( new FormatterRequest( - new Literal(10, 19, 1, 1, new StringBuilder(new string('a', 10))), + new Literal(10, 19, 1, 1, new string('a', 10)), "test", "test", "test")); subject.Add( new FormatterRequest( - new Literal(20, 29, 1, 1, new StringBuilder(new string('a', 10))), + new Literal(20, 29, 1, 1, new string('a', 10)), "test", "test", "test")); diff --git a/src/Jeffijoe.MessageFormat.Tests/Parsing/LiteralTests.cs b/src/Jeffijoe.MessageFormat.Tests/Parsing/LiteralTests.cs index dd55cc7..7b7a5cb 100644 --- a/src/Jeffijoe.MessageFormat.Tests/Parsing/LiteralTests.cs +++ b/src/Jeffijoe.MessageFormat.Tests/Parsing/LiteralTests.cs @@ -25,8 +25,8 @@ public class LiteralTests [Fact] public void ShiftIndices() { - var subject = new Literal(20, 29, 1, 1, new StringBuilder(new string('a', 10))); - var other = new Literal(5, 10, 1, 1, new StringBuilder(new string('a', 6))); + var subject = new Literal(20, 29, 1, 1, new string('a', 10)); + var other = new Literal(5, 10, 1, 1, new string('a', 6)); subject.ShiftIndices(2, other); diff --git a/src/Jeffijoe.MessageFormat.Tests/Parsing/PatternParser/PatternParserGetKeyTests.cs b/src/Jeffijoe.MessageFormat.Tests/Parsing/PatternParser/PatternParserGetKeyTests.cs index 132ed3e..84161b2 100644 --- a/src/Jeffijoe.MessageFormat.Tests/Parsing/PatternParser/PatternParserGetKeyTests.cs +++ b/src/Jeffijoe.MessageFormat.Tests/Parsing/PatternParser/PatternParserGetKeyTests.cs @@ -52,12 +52,12 @@ public static IEnumerable GetKey_throws_with_invalid_characters_Case { get { - yield return new object[] { new Literal(3, 10, 1, 3, new StringBuilder("Hellåw,")), 1, 8 }; - yield return new object[] { new Literal(0, 0, 3, 3, new StringBuilder(",")), 3, 4 }; - yield return new object[] { new Literal(0, 0, 3, 3, new StringBuilder(" hello dawg")), 0, 0 }; - yield return new object[] { new Literal(0, 0, 3, 3, new StringBuilder("hello dawg ")), 0, 0 }; - yield return new object[] { new Literal(0, 0, 3, 3, new StringBuilder(" hello dawg")), 0, 0 }; - yield return new object[] { new Literal(0, 0, 3, 3, new StringBuilder(" hello\r\ndawg")), 0, 0 }; + yield return new object[] { new Literal(3, 10, 1, 3, "Hellåw,"), 1, 8 }; + yield return new object[] { new Literal(0, 0, 3, 3, ","), 3, 4 }; + yield return new object[] { new Literal(0, 0, 3, 3, " hello dawg"), 0, 0 }; + yield return new object[] { new Literal(0, 0, 3, 3, "hello dawg "), 0, 0 }; + yield return new object[] { new Literal(0, 0, 3, 3, " hello dawg"), 0, 0 }; + yield return new object[] { new Literal(0, 0, 3, 3, " hello\r\ndawg"), 0, 0 }; } } @@ -88,7 +88,7 @@ public static IEnumerable GetKey_throws_with_invalid_characters_Case [InlineData("0", "0", 0)] public void ReadLiteralSection(string source, string expected, int expectedLastIndex) { - var literal = new Literal(10, 10, 1, 1, new StringBuilder(source)); + var literal = new Literal(10, 10, 1, 1, source); int lastIndex; Assert.Equal(expected, PatternParser.ReadLiteralSection(literal, 0, false, out lastIndex)); Assert.Equal(expectedLastIndex, lastIndex); @@ -142,7 +142,7 @@ public void ReadLiteralSection_throws_with_invalid_characters( [InlineData("SupDawg,", null, 8)] public void ReadLiteralSection_with_offset(string source, string expected, int offset) { - var literal = new Literal(10, 10, 1, 1, new StringBuilder(source)); + var literal = new Literal(10, 10, 1, 1, source); int lastIndex; Assert.Equal(expected, PatternParser.ReadLiteralSection(literal, offset, true, out lastIndex)); } diff --git a/src/Jeffijoe.MessageFormat.Tests/Parsing/PatternParser/PatternParserParseTests.cs b/src/Jeffijoe.MessageFormat.Tests/Parsing/PatternParser/PatternParserParseTests.cs index 1b7b12f..92a3abd 100644 --- a/src/Jeffijoe.MessageFormat.Tests/Parsing/PatternParser/PatternParserParseTests.cs +++ b/src/Jeffijoe.MessageFormat.Tests/Parsing/PatternParser/PatternParserParseTests.cs @@ -77,7 +77,7 @@ public void Parse(string source, string expectedKey, string expectedFormat, stri var sb = new StringBuilder(source); literalParserMock.Setup(x => x.ParseLiterals(sb)); literalParserMock.Setup(x => x.ParseLiterals(sb)) - .Returns(new[] { new Literal(0, source.Length, 1, 1, new StringBuilder(source)) }); + .Returns(new[] { new Literal(0, source.Length, 1, 1, source) }); var subject = new PatternParser(literalParserMock.Object); diff --git a/src/Jeffijoe.MessageFormat/MessageFormatter.cs b/src/Jeffijoe.MessageFormat/MessageFormatter.cs index 6697111..d89c9aa 100644 --- a/src/Jeffijoe.MessageFormat/MessageFormatter.cs +++ b/src/Jeffijoe.MessageFormat/MessageFormatter.cs @@ -210,7 +210,7 @@ public static string Format(string pattern, object data) public string FormatMessage(string pattern, IDictionary args) { /* - * We are asuming the formatters are ordered correctly + * We are assuming the formatters are ordered correctly * - that is, from left to right, string-wise. */ var sourceBuilder = StringBuilderPool.Get(); @@ -295,18 +295,18 @@ protected internal string UnescapeLiterals(StringBuilder sourceBuilder) return string.Empty; } + var length = sourceBuilder.Length; + const char EscapingChar = '\''; + const char OpenBrace = '{'; + const char CloseBrace = '}'; + + var braceBalance = 0; + var insideEscapeSequence = false; + var dest = StringBuilderPool.Get(); try { - int length = sourceBuilder.Length; - const char EscapingChar = '\''; - const char OpenBrace = '{'; - const char CloseBrace = '}'; - - var braceBalance = 0; - var insideEscapeSequence = false; - for (int i = 0; i < length; i++) { var c = sourceBuilder[i]; @@ -393,17 +393,13 @@ private IFormatterRequestCollection ParseRequests(string pattern, StringBuilder } // If we have a cached result from this pattern, clone it and return the clone. - IFormatterRequestCollection cached; - if (this.cache.TryGetValue(pattern, out cached)) + if (this.cache.TryGetValue(pattern, out var cached)) { return cached.Clone(); } var requests = this.patternParser.Parse(sourceBuilder); - if (this.cache != null) - { - this.cache.TryAdd(pattern, requests.Clone()); - } + this.cache?.TryAdd(pattern, requests.Clone()); return requests; } From 23ea38df6d4346ca01b82fb264a63a0631f4b860 Mon Sep 17 00:00:00 2001 From: Kostiantyn Sharovarskyi Date: Tue, 27 Apr 2021 04:49:42 +0300 Subject: [PATCH 45/98] Do not allocate twice on subsstring+trim for net5.0 --- .../Formatting/Formatters/PluralContext.cs | 2 +- src/Jeffijoe.MessageFormat/Parsing/PatternParser.cs | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralContext.cs b/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralContext.cs index ab54340..55f8ec6 100644 --- a/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralContext.cs +++ b/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralContext.cs @@ -48,7 +48,7 @@ private PluralContext(string number, double parsed) } else { -#if NET5_0 +#if NET5_0_OR_GREATER var fractionSpan = number.AsSpan(dotIndex + 1, number.Length - dotIndex - 1); var fractionSpanWithoutZeroes = fractionSpan.TrimEnd('0'); #else diff --git a/src/Jeffijoe.MessageFormat/Parsing/PatternParser.cs b/src/Jeffijoe.MessageFormat/Parsing/PatternParser.cs index 88de68b..bed1972 100644 --- a/src/Jeffijoe.MessageFormat/Parsing/PatternParser.cs +++ b/src/Jeffijoe.MessageFormat/Parsing/PatternParser.cs @@ -81,8 +81,13 @@ public IFormatterRequestCollection Parse(StringBuilder source) formatterKey = ReadLiteralSection(literal, variableName.Length + 1, true, out lastIndex); if (formatterKey != null) { +#if NET5_0_OR_GREATER + formatterArgs = + literal.InnerText.AsSpan(lastIndex + 1, literal.InnerText.Length - lastIndex - 1).Trim().ToString(); +#else formatterArgs = literal.InnerText.Substring(lastIndex + 1, literal.InnerText.Length - lastIndex - 1).Trim(); +#endif } } From 35e6bc1dbbba36c9c0e60ff63960ae07f714089c Mon Sep 17 00:00:00 2001 From: Kostiantyn Sharovarskyi Date: Tue, 27 Apr 2021 04:55:44 +0300 Subject: [PATCH 46/98] Minor indent fix --- src/Jeffijoe.MessageFormat/Parsing/PatternParser.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Jeffijoe.MessageFormat/Parsing/PatternParser.cs b/src/Jeffijoe.MessageFormat/Parsing/PatternParser.cs index bed1972..717a4e7 100644 --- a/src/Jeffijoe.MessageFormat/Parsing/PatternParser.cs +++ b/src/Jeffijoe.MessageFormat/Parsing/PatternParser.cs @@ -127,14 +127,14 @@ public IFormatterRequestCollection Parse(StringBuilder source) out int lastIndex) { const char Comma = ','; - var sb = StringBuilderPool.Get(); + var innerText = literal.InnerText; + var column = literal.SourceColumnNumber; + var foundWhitespace = false; + lastIndex = 0; + var sb = StringBuilderPool.Get(); try { - var innerText = literal.InnerText; - var column = literal.SourceColumnNumber; - var foundWhitespace = false; - lastIndex = 0; for (var i = offset; i < innerText.Length; i++) { var c = innerText[i]; From f346cf99e31c5ca64dcd2cad59d235521516dddf Mon Sep 17 00:00:00 2001 From: Kostiantyn Sharovarskyi Date: Tue, 27 Apr 2021 05:02:14 +0300 Subject: [PATCH 47/98] Minor formatting change - move stuff out of try/finally --- src/Jeffijoe.MessageFormat/Formatting/BaseFormatter.cs | 5 ++--- src/Jeffijoe.MessageFormat/Jeffijoe.MessageFormat.csproj | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/Jeffijoe.MessageFormat/Formatting/BaseFormatter.cs b/src/Jeffijoe.MessageFormat/Formatting/BaseFormatter.cs index da1b4ad..78f4bba 100644 --- a/src/Jeffijoe.MessageFormat/Formatting/BaseFormatter.cs +++ b/src/Jeffijoe.MessageFormat/Formatting/BaseFormatter.cs @@ -62,14 +62,13 @@ protected internal IEnumerable ParseExtensions(FormatterRequ int length = request.FormatterArguments.Length; index = 0; + const char Colon = ':'; + bool foundExtension = false; var extension = StringBuilderPool.Get(); var value = StringBuilderPool.Get(); - try { - const char Colon = ':'; - bool foundExtension = false; for (int i = 0; i < length; i++) { var c = request.FormatterArguments[i]; diff --git a/src/Jeffijoe.MessageFormat/Jeffijoe.MessageFormat.csproj b/src/Jeffijoe.MessageFormat/Jeffijoe.MessageFormat.csproj index 3b850a1..9cc3d23 100644 --- a/src/Jeffijoe.MessageFormat/Jeffijoe.MessageFormat.csproj +++ b/src/Jeffijoe.MessageFormat/Jeffijoe.MessageFormat.csproj @@ -10,7 +10,7 @@ https://github.com/jeffijoe/messageformat.net latest enable - net5.0;netstandard2.0 + netstandard2.0;net5.0 From 1ea6acd24926c91e9c410e1181003858d1103130 Mon Sep 17 00:00:00 2001 From: Kostiantyn Sharovarskyi Date: Tue, 27 Apr 2021 05:43:16 +0300 Subject: [PATCH 48/98] Do not unescape if nothing to escape --- .../Helpers/StringBuilderHelper.cs | 46 ++++++++++++++++++- .../MessageFormatter.cs | 7 ++- 2 files changed, 51 insertions(+), 2 deletions(-) diff --git a/src/Jeffijoe.MessageFormat/Helpers/StringBuilderHelper.cs b/src/Jeffijoe.MessageFormat/Helpers/StringBuilderHelper.cs index b83356f..972ffa7 100644 --- a/src/Jeffijoe.MessageFormat/Helpers/StringBuilderHelper.cs +++ b/src/Jeffijoe.MessageFormat/Helpers/StringBuilderHelper.cs @@ -3,6 +3,7 @@ // Author: Jeff Hansen // Copyright (C) Jeff Hansen 2014. All rights reserved. +using System; using System.Text; namespace Jeffijoe.MessageFormat.Helpers @@ -28,6 +29,15 @@ internal static class StringBuilderHelper /// internal static bool Contains(this StringBuilder src, params char[] chars) { +#if NET5_0_OR_GREATER + foreach (var chunk in src.GetChunks()) + { + if (chunk.Span.IndexOfAny(chars) != -1) + { + return true; + } + } +#else for (int i = 0; i < src.Length; i++) { foreach (var c in chars) @@ -38,6 +48,40 @@ internal static bool Contains(this StringBuilder src, params char[] chars) } } } +#endif + + return false; + } + + /// + /// Determines whether the specified source contains the specified character. + /// + /// + /// The source. + /// + /// + /// The character. + /// + /// + /// The . + /// + internal static bool Contains(this StringBuilder src, char character) + { +#if NET5_0_OR_GREATER + foreach (var chunk in src.GetChunks()) + { + if (chunk.Span.IndexOf(character) != -1) + return true; + } +#else + for (int i = 0; i < src.Length; i++) + { + if (src[i] == character) + { + return true; + } + } +#endif return false; } @@ -105,6 +149,6 @@ internal static StringBuilder TrimWhitespace(this StringBuilder src) return src; } - #endregion +#endregion } } \ No newline at end of file diff --git a/src/Jeffijoe.MessageFormat/MessageFormatter.cs b/src/Jeffijoe.MessageFormat/MessageFormatter.cs index d89c9aa..e02a5b1 100644 --- a/src/Jeffijoe.MessageFormat/MessageFormatter.cs +++ b/src/Jeffijoe.MessageFormat/MessageFormatter.cs @@ -295,11 +295,16 @@ protected internal string UnescapeLiterals(StringBuilder sourceBuilder) return string.Empty; } - var length = sourceBuilder.Length; const char EscapingChar = '\''; const char OpenBrace = '{'; const char CloseBrace = '}'; + if (!sourceBuilder.Contains(EscapingChar)) + { + return sourceBuilder.ToString(); + } + + var length = sourceBuilder.Length; var braceBalance = 0; var insideEscapeSequence = false; From 1c1ca78eaa6f0c385aae31cb4f875829a9b3e541 Mon Sep 17 00:00:00 2001 From: Kostiantyn Sharovarskyi Date: Tue, 27 Apr 2021 16:45:04 +0300 Subject: [PATCH 49/98] Remove outdated comment --- src/Jeffijoe.MessageFormat/Parsing/LiteralParser.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Jeffijoe.MessageFormat/Parsing/LiteralParser.cs b/src/Jeffijoe.MessageFormat/Parsing/LiteralParser.cs index 5ac6ed8..1bf0f51 100644 --- a/src/Jeffijoe.MessageFormat/Parsing/LiteralParser.cs +++ b/src/Jeffijoe.MessageFormat/Parsing/LiteralParser.cs @@ -158,7 +158,6 @@ public IEnumerable ParseLiterals(StringBuilder sb) continue; } - // Passing in the text buffer instead of the actual string to avoid allocating a new string. result.Add(new Literal(start, i, startLineNumber, startColumnNumber, matchTextBuf.ToString())); matchTextBuf.Clear(); start = 0; From 4989ff62f7d50dabb7d7f97ac7be1f9560c3b6d1 Mon Sep 17 00:00:00 2001 From: Kostiantyn Sharovarskyi Date: Tue, 27 Apr 2021 16:50:37 +0300 Subject: [PATCH 50/98] Fix nullability warning --- .../Formatting/Formatters/VariableFormatter.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Jeffijoe.MessageFormat/Formatting/Formatters/VariableFormatter.cs b/src/Jeffijoe.MessageFormat/Formatting/Formatters/VariableFormatter.cs index 02e3b35..4fb9f6b 100644 --- a/src/Jeffijoe.MessageFormat/Formatting/Formatters/VariableFormatter.cs +++ b/src/Jeffijoe.MessageFormat/Formatting/Formatters/VariableFormatter.cs @@ -69,12 +69,10 @@ public string Format( { switch (value) { - case null: - return string.Empty; case IFormattable formattable: return formattable.ToString(null, GetCultureInfo(locale)); default: - return value.ToString(); + return value?.ToString() ?? string.Empty; } } From 01a7efbf2e4cf51955646efe91629acf32525722 Mon Sep 17 00:00:00 2001 From: Jeff Hansen Date: Fri, 7 Jan 2022 12:24:37 -0500 Subject: [PATCH 51/98] Add .NET 6 support --- .github/workflows/ci.yml | 10 +- README.md | 2 +- .../.idea/contentModel.xml | 112 ------------------ .../.idea.MessageFormat/.idea/indexLayout.xml | 6 +- .../.idea.MessageFormat/.idea/modules.xml | 8 -- src/.idea/.idea.MessageFormat/riderModule.iml | 10 -- ...joe.MessageFormat.MetadataGenerator.csproj | 8 +- .../Plural/Parsing/AST/LeftOperand.cs | 2 +- .../Plural/Parsing/AST/NumberOperand.cs | 2 +- .../Plural/Parsing/AST/RangeOperand.cs | 2 +- .../Plural/Parsing/AST/VariableOperand.cs | 2 +- .../Plural/Parsing/PluralParser.cs | 26 ++-- .../Plural/Parsing/RuleParser.cs | 6 +- .../Plural/PluralLanguagesGenerator.cs | 2 +- .../Plural/SourceGeneration/RuleGenerator.cs | 19 --- .../Formatting/BaseFormatterTests.cs | 16 ++- .../Formatting/FormatterLibraryTests.cs | 2 - .../Formatters/PluralFormatterTests.cs | 2 - .../Formatters/SelectFormatterTests.cs | 8 +- .../Formatters/VariableFormatterTests.cs | 2 - .../Helpers/ObjectHelperTests.cs | 2 + .../Jeffijoe.MessageFormat.Tests.csproj | 22 ++-- .../MessageFormatterCachingTests.cs | 2 +- .../MessageFormatterFullIntegrationTests.cs | 3 +- .../MessageFormatterIssues.cs | 28 ----- .../MessageFormatterTests.cs | 6 +- .../GeneratedPluralRulesTests.cs | 1 - .../FormatterRequestCollectionTests.cs | 2 - .../Parsing/LiteralParserTests.cs | 2 +- .../Parsing/LiteralTests.cs | 2 - .../PatternParserGetKeyTests.cs | 13 +- .../PatternParserParseTests.cs | 3 +- .../PatternParserWithRealLiteralParser.cs | 0 .../Formatting/BaseFormatter.cs | 3 - .../Formatting/FormatterLibrary.cs | 2 - .../Formatting/Formatters/PluralFormatter.cs | 1 - .../Formatters/PluralRulesMetadata.cs | 2 +- .../Helpers/ObjectHelper.cs | 2 +- .../Helpers/StringBuilderHelper.cs | 5 +- .../Jeffijoe.MessageFormat.csproj | 6 +- src/Jeffijoe.MessageFormat/Parsing/Literal.cs | 2 - .../Parsing/PatternParser.cs | 12 +- .../Properties/AssemblyInfo.cs | 2 - .../StringBuilderPool.cs | 3 +- 44 files changed, 90 insertions(+), 283 deletions(-) delete mode 100644 src/.idea/.idea.MessageFormat/.idea/contentModel.xml delete mode 100644 src/.idea/.idea.MessageFormat/.idea/modules.xml delete mode 100644 src/.idea/.idea.MessageFormat/riderModule.iml rename src/Jeffijoe.MessageFormat.Tests/Parsing/{PatternParser => }/PatternParserGetKeyTests.cs (93%) rename src/Jeffijoe.MessageFormat.Tests/Parsing/{PatternParser => }/PatternParserParseTests.cs (98%) rename src/Jeffijoe.MessageFormat.Tests/Parsing/{PatternParser => }/PatternParserWithRealLiteralParser.cs (100%) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c75e97f..3ca23e7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,4 +1,4 @@ -name: .NET Core +name: Build, Test and Release on: push: @@ -6,7 +6,7 @@ on: pull_request: branches: [ master ] release: - types: [released] + types: [ released ] env: DOTNET_NOLOGO: true @@ -22,10 +22,10 @@ jobs: with: fetch-depth: 100 - - name: Setup .NET Core + - name: Setup .NET uses: actions/setup-dotnet@v1 with: - dotnet-version: 5.0.x + dotnet-version: 6.0.x - name: Install dependencies working-directory: ./src @@ -36,7 +36,7 @@ jobs: run: | dotnet build --configuration Release --no-restore dotnet pack -c Release Jeffijoe.MessageFormat/Jeffijoe.MessageFormat.csproj - + - name: Test working-directory: ./src run: dotnet test --no-restore --verbosity normal diff --git a/README.md b/README.md index 91c7589..6f08c95 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,7 @@ Install-Package MessageFormat ## Features * **It's fast.** Everything is hand-written; no parser-generators, *not even regular expressions*. -* **It's portable.** The library is targeting **.NET Standard 1.1**. +* **It's portable.** The library is targeting **.NET Standard 2.0**. * **It's compatible with other implementations.** I've been peeking a bit at the [MessageFormat.js][0] library to make sure the results would be the same. * **It's (relatively) small**. For a .NET library, ~25kb is not a lot. diff --git a/src/.idea/.idea.MessageFormat/.idea/contentModel.xml b/src/.idea/.idea.MessageFormat/.idea/contentModel.xml deleted file mode 100644 index b0abb30..0000000 --- a/src/.idea/.idea.MessageFormat/.idea/contentModel.xml +++ /dev/null @@ -1,112 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/.idea/.idea.MessageFormat/.idea/indexLayout.xml b/src/.idea/.idea.MessageFormat/.idea/indexLayout.xml index 27ba142..074ca59 100644 --- a/src/.idea/.idea.MessageFormat/.idea/indexLayout.xml +++ b/src/.idea/.idea.MessageFormat/.idea/indexLayout.xml @@ -1,7 +1,9 @@ - - + + + ../../messageformat-dotnet + diff --git a/src/.idea/.idea.MessageFormat/.idea/modules.xml b/src/.idea/.idea.MessageFormat/.idea/modules.xml deleted file mode 100644 index bdeedf2..0000000 --- a/src/.idea/.idea.MessageFormat/.idea/modules.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/src/.idea/.idea.MessageFormat/riderModule.iml b/src/.idea/.idea.MessageFormat/riderModule.iml deleted file mode 100644 index a59a05c..0000000 --- a/src/.idea/.idea.MessageFormat/riderModule.iml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/src/Jeffijoe.MessageFormat.MetadataGenerator/Jeffijoe.MessageFormat.MetadataGenerator.csproj b/src/Jeffijoe.MessageFormat.MetadataGenerator/Jeffijoe.MessageFormat.MetadataGenerator.csproj index 8abc8d1..b5e494d 100644 --- a/src/Jeffijoe.MessageFormat.MetadataGenerator/Jeffijoe.MessageFormat.MetadataGenerator.csproj +++ b/src/Jeffijoe.MessageFormat.MetadataGenerator/Jeffijoe.MessageFormat.MetadataGenerator.csproj @@ -3,9 +3,9 @@ True ../Jeffijoe.MessageFormat/MessageFormat.snk - netstandard2.0 - 8 + default enable + net5.0;net6.0;netstandard2.0;netstandard2.1 @@ -13,11 +13,11 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/LeftOperand.cs b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/LeftOperand.cs index d42e5dd..7229eb6 100644 --- a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/LeftOperand.cs +++ b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/LeftOperand.cs @@ -16,7 +16,7 @@ public override bool Equals(object obj) if (obj is ModuloOperand op) return op.Operand == Operand && op.ModValue == ModValue; - return base.Equals(obj); + return this == obj; } public override int GetHashCode() diff --git a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/NumberOperand.cs b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/NumberOperand.cs index 2a0138a..9f5bbdc 100644 --- a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/NumberOperand.cs +++ b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/NumberOperand.cs @@ -14,7 +14,7 @@ public override bool Equals(object obj) if (obj is NumberOperand n) return n.Number == Number; - return base.Equals(obj); + return this == obj; } public override int GetHashCode() diff --git a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/RangeOperand.cs b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/RangeOperand.cs index 681f52a..150dadc 100644 --- a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/RangeOperand.cs +++ b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/RangeOperand.cs @@ -16,7 +16,7 @@ public override bool Equals(object obj) if (obj is RangeOperand n) return n.Start == Start && n.End == End; - return base.Equals(obj); + return this == obj; } public override int GetHashCode() diff --git a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/VariableOperand.cs b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/VariableOperand.cs index d36b0e9..51e476e 100644 --- a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/VariableOperand.cs +++ b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/VariableOperand.cs @@ -14,7 +14,7 @@ public override bool Equals(object obj) if (obj is VariableOperand op) return op.Operand == Operand; - return base.Equals(obj); + return this == obj; } public override int GetHashCode() diff --git a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/PluralParser.cs b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/PluralParser.cs index ba8ca3c..d29084e 100644 --- a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/PluralParser.cs +++ b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/PluralParser.cs @@ -18,21 +18,23 @@ public PluralParser(XmlDocument rulesDocument, string[] excludedLocales) public IEnumerable Parse() { - var root = _rulesDocument.DocumentElement; - + var root = _rulesDocument.DocumentElement!; + foreach(XmlNode dataElement in root.ChildNodes) { - if(dataElement.Name == "plurals") + if (dataElement.Name != "plurals") + { + continue; + } + + foreach (XmlNode rule in dataElement.ChildNodes) { - foreach (XmlNode rule in dataElement.ChildNodes) + if(rule.Name == "pluralRules") { - if(rule.Name == "pluralRules") + var parsed = ParseSingleRule(rule); + if (parsed != null) { - var parsed = ParseSingleRule(rule); - if (parsed != null) - { - yield return parsed; - } + yield return parsed; } } } @@ -41,7 +43,7 @@ public IEnumerable Parse() private PluralRule? ParseSingleRule(XmlNode rule) { - var locales = rule.Attributes["locales"].Value.Split(' '); + var locales = rule.Attributes!["locales"].Value.Split(' '); if (locales.All(l => _excludedLocales.Contains(l))) { @@ -53,7 +55,7 @@ public IEnumerable Parse() { if (condition.Name == "pluralRule") { - var count = condition.Attributes["count"].Value; + var count = condition.Attributes!["count"].Value; // Ignore other, because other is basically everything else except for the conditions present if (count == "other") diff --git a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/RuleParser.cs b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/RuleParser.cs index f5a62d4..44d4493 100644 --- a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/RuleParser.cs +++ b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/RuleParser.cs @@ -198,10 +198,8 @@ private IReadOnlyList ParseRightOperand() AdvanceWhitespace(); continue; } - else - { - break; - } + + break; } return numbers; diff --git a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/PluralLanguagesGenerator.cs b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/PluralLanguagesGenerator.cs index 06251df..0d4fe2a 100644 --- a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/PluralLanguagesGenerator.cs +++ b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/PluralLanguagesGenerator.cs @@ -49,7 +49,7 @@ private IReadOnlyList GetRules(string[] excludedLocales) private Stream GetRulesContentStream() { - return typeof(PluralLanguagesGenerator).Assembly.GetManifestResourceStream("Jeffijoe.MessageFormat.MetadataGenerator.data.plurals.xml"); + return typeof(PluralLanguagesGenerator).Assembly.GetManifestResourceStream("Jeffijoe.MessageFormat.MetadataGenerator.data.plurals.xml")!; } public void Initialize(GeneratorInitializationContext context) diff --git a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/SourceGeneration/RuleGenerator.cs b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/SourceGeneration/RuleGenerator.cs index dd44405..0087ea6 100644 --- a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/SourceGeneration/RuleGenerator.cs +++ b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/SourceGeneration/RuleGenerator.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.Text; using Jeffijoe.MessageFormat.MetadataGenerator.Plural.Parsing.AST; @@ -127,24 +126,6 @@ private char OperandToVariable(OperandSymbol operand) }; } - private IEnumerable GetAllLeftOperands(OrCondition[] conditions) - { - foreach (var condition in conditions) - { - foreach(var operation in condition.AndConditions) - { - var operand = operation.OperandLeft switch - { - VariableOperand op => op.Operand, - ModuloOperand op => op.Operand, - var op => throw new InvalidOperationException($"Unexpected operand {op.GetType()}") - }; - - yield return operand; - } - } - } - private void WriteLine(StringBuilder builder, string value, int indent) { builder.Append(' ', indent + _innerIndent); diff --git a/src/Jeffijoe.MessageFormat.Tests/Formatting/BaseFormatterTests.cs b/src/Jeffijoe.MessageFormat.Tests/Formatting/BaseFormatterTests.cs index 7d4d687..e1c52d8 100644 --- a/src/Jeffijoe.MessageFormat.Tests/Formatting/BaseFormatterTests.cs +++ b/src/Jeffijoe.MessageFormat.Tests/Formatting/BaseFormatterTests.cs @@ -6,8 +6,6 @@ using System.Collections.Generic; using System.Linq; -using System.Text; - using Jeffijoe.MessageFormat.Formatting; using Jeffijoe.MessageFormat.Parsing; using Jeffijoe.MessageFormat.Tests.TestHelpers; @@ -51,7 +49,7 @@ public BaseFormatterTests(ITestOutputHelper outputHelper) /// /// Gets the parse arguments_tests. /// - public static IEnumerable ParseArguments_tests + public static IEnumerable ParseArgumentsTests { get { @@ -80,7 +78,7 @@ public static IEnumerable ParseArguments_tests /// /// Gets the parse keyed blocks_tests. /// - public static IEnumerable ParseKeyedBlocks_tests + public static IEnumerable ParseKeyedBlocksTests { get { @@ -136,7 +134,7 @@ public static IEnumerable ParseKeyedBlocks_tests /// The blocks. /// [Theory] - [MemberData(nameof(ParseArguments_tests))] + [MemberData(nameof(ParseArgumentsTests))] public void ParseArguments( string args, string[] extensionKeys, @@ -223,7 +221,7 @@ public void ParseExtensions(string args, string extension, string value, int exp Benchmark.End(this.outputHelper); - var actual = subject.ParseExtensions(req, out index); + var actual = subject.ParseExtensions(req, out index).ToList(); Assert.NotEmpty(actual); var first = actual.First(); Assert.Equal(extension, first.Extension); @@ -244,7 +242,7 @@ public void ParseExtensions_multiple() var req = new FormatterRequest(new Literal(1, 1, 1, 1, ""), string.Empty, null, args); - var actual = subject.ParseExtensions(req, out index); + var actual = subject.ParseExtensions(req, out index).ToList(); Assert.NotEmpty(actual); var result = actual.First(); Assert.Equal("offset", result.Extension); @@ -270,7 +268,7 @@ public void ParseExtensions_multiple() /// The values. /// [Theory] - [MemberData(nameof(ParseKeyedBlocks_tests))] + [MemberData(nameof(ParseKeyedBlocksTests))] public void ParseKeyedBlocks(string args, string[] keys, string[] values) { var subject = new BaseFormatterImpl(); @@ -287,7 +285,7 @@ public void ParseKeyedBlocks(string args, string[] keys, string[] values) Benchmark.End(this.outputHelper); - var actual = subject.ParseKeyedBlocks(req, 0); + var actual = subject.ParseKeyedBlocks(req, 0).ToList(); Assert.Equal(keys.Length, actual.Count()); this.outputHelper.WriteLine("Input: " + args); this.outputHelper.WriteLine("-----"); diff --git a/src/Jeffijoe.MessageFormat.Tests/Formatting/FormatterLibraryTests.cs b/src/Jeffijoe.MessageFormat.Tests/Formatting/FormatterLibraryTests.cs index fea2fb8..83795d7 100644 --- a/src/Jeffijoe.MessageFormat.Tests/Formatting/FormatterLibraryTests.cs +++ b/src/Jeffijoe.MessageFormat.Tests/Formatting/FormatterLibraryTests.cs @@ -4,8 +4,6 @@ // Author: Jeff Hansen // Copyright (C) Jeff Hansen 2015. All rights reserved. -using System.Text; - using Jeffijoe.MessageFormat.Formatting; using Jeffijoe.MessageFormat.Parsing; diff --git a/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/PluralFormatterTests.cs b/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/PluralFormatterTests.cs index db3823c..fe398e5 100644 --- a/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/PluralFormatterTests.cs +++ b/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/PluralFormatterTests.cs @@ -6,8 +6,6 @@ using System; using System.Collections.Generic; -using System.Text; - using Jeffijoe.MessageFormat.Formatting; using Jeffijoe.MessageFormat.Formatting.Formatters; using Jeffijoe.MessageFormat.Parsing; diff --git a/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/SelectFormatterTests.cs b/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/SelectFormatterTests.cs index c4be48d..d37e634 100644 --- a/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/SelectFormatterTests.cs +++ b/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/SelectFormatterTests.cs @@ -5,8 +5,6 @@ // Copyright (C) Jeff Hansen 2015. All rights reserved. using System.Collections.Generic; -using System.Text; - using Jeffijoe.MessageFormat.Formatting; using Jeffijoe.MessageFormat.Formatting.Formatters; using Jeffijoe.MessageFormat.Parsing; @@ -27,7 +25,7 @@ public class SelectFormatterTests /// /// Gets the format_tests. /// - public static IEnumerable Format_tests + public static IEnumerable FormatTests { get { @@ -54,13 +52,13 @@ public static IEnumerable Format_tests /// The expected block. /// [Theory] - [MemberData(nameof(Format_tests))] + [MemberData(nameof(FormatTests))] public void Format(string formatterArgs, string gender, string expectedBlock) { var subject = new SelectFormatter(); var messageFormatterMock = new Mock(); messageFormatterMock.Setup(x => x.FormatMessage(It.IsAny(), It.IsAny>())) - .Returns((string input, Dictionary a) => input); + .Returns((string input, Dictionary _) => input); var req = new FormatterRequest( new Literal(1, 1, 1, 1, ""), "gender", diff --git a/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/VariableFormatterTests.cs b/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/VariableFormatterTests.cs index f97924f..c8e095c 100644 --- a/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/VariableFormatterTests.cs +++ b/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/VariableFormatterTests.cs @@ -5,8 +5,6 @@ // Copyright (C) Jeff Hansen 2015. All rights reserved. using System.Collections.Generic; -using System.Text; - using Jeffijoe.MessageFormat.Formatting; using Jeffijoe.MessageFormat.Formatting.Formatters; using Jeffijoe.MessageFormat.Parsing; diff --git a/src/Jeffijoe.MessageFormat.Tests/Helpers/ObjectHelperTests.cs b/src/Jeffijoe.MessageFormat.Tests/Helpers/ObjectHelperTests.cs index 21b95a6..a6d6652 100644 --- a/src/Jeffijoe.MessageFormat.Tests/Helpers/ObjectHelperTests.cs +++ b/src/Jeffijoe.MessageFormat.Tests/Helpers/ObjectHelperTests.cs @@ -12,6 +12,8 @@ using Xunit; using Xunit.Abstractions; +// ReSharper disable UnusedMember.Local + namespace Jeffijoe.MessageFormat.Tests.Helpers { /// diff --git a/src/Jeffijoe.MessageFormat.Tests/Jeffijoe.MessageFormat.Tests.csproj b/src/Jeffijoe.MessageFormat.Tests/Jeffijoe.MessageFormat.Tests.csproj index 608711b..3274415 100644 --- a/src/Jeffijoe.MessageFormat.Tests/Jeffijoe.MessageFormat.Tests.csproj +++ b/src/Jeffijoe.MessageFormat.Tests/Jeffijoe.MessageFormat.Tests.csproj @@ -1,23 +1,27 @@  - net5.0 True MessageFormat.snk False 9 enable + net6.0 - - - - - - - - + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/src/Jeffijoe.MessageFormat.Tests/MessageFormatterCachingTests.cs b/src/Jeffijoe.MessageFormat.Tests/MessageFormatterCachingTests.cs index 9a72d70..dd26e6a 100644 --- a/src/Jeffijoe.MessageFormat.Tests/MessageFormatterCachingTests.cs +++ b/src/Jeffijoe.MessageFormat.Tests/MessageFormatterCachingTests.cs @@ -87,7 +87,7 @@ public void FormatMessage_caches_reused_pattern() [Fact] public void FormatMessage_with_cache_benchmark() { - var subject = new MessageFormatter(true); + var subject = new MessageFormatter(useCache: true); this.Benchmark(subject); } diff --git a/src/Jeffijoe.MessageFormat.Tests/MessageFormatterFullIntegrationTests.cs b/src/Jeffijoe.MessageFormat.Tests/MessageFormatterFullIntegrationTests.cs index 5148af6..786d7cc 100644 --- a/src/Jeffijoe.MessageFormat.Tests/MessageFormatterFullIntegrationTests.cs +++ b/src/Jeffijoe.MessageFormat.Tests/MessageFormatterFullIntegrationTests.cs @@ -581,7 +581,7 @@ public void ReadMe_test_to_make_sure_I_dont_look_like_a_fool() } { - var mf = new MessageFormatter(true, "en"); + var mf = new MessageFormatter(useCache: true, locale: "en"); mf.Pluralizers!["en"] = n => { // ´n´ is the number being pluralized. // ReSharper disable once CompareOfFloatsByEqualityOperator @@ -590,6 +590,7 @@ public void ReadMe_test_to_make_sure_I_dont_look_like_a_fool() return "zero"; } + // ReSharper disable once CompareOfFloatsByEqualityOperator if (n == 1) { return "one"; diff --git a/src/Jeffijoe.MessageFormat.Tests/MessageFormatterIssues.cs b/src/Jeffijoe.MessageFormat.Tests/MessageFormatterIssues.cs index 7d3d40e..fbddc7d 100644 --- a/src/Jeffijoe.MessageFormat.Tests/MessageFormatterIssues.cs +++ b/src/Jeffijoe.MessageFormat.Tests/MessageFormatterIssues.cs @@ -4,13 +4,7 @@ // Author: Jeff Hansen // Copyright (C) Jeff Hansen 2015. All rights reserved. -using System.Collections.Generic; - -using Jeffijoe.MessageFormat.Formatting; -using Jeffijoe.MessageFormat.Tests.TestHelpers; - using Xunit; -using Xunit.Abstractions; namespace Jeffijoe.MessageFormat.Tests { @@ -19,28 +13,6 @@ namespace Jeffijoe.MessageFormat.Tests /// public class MessageFormatterIssues { - #region Fields - - /// - /// The output helper. - /// - private readonly ITestOutputHelper outputHelper; - - #endregion - - #region Constructors and Destructors - - /// - /// Ctor. - /// - /// - public MessageFormatterIssues(ITestOutputHelper outputHelper) - { - this.outputHelper = outputHelper; - } - - #endregion - [Fact] public void Issue13() { diff --git a/src/Jeffijoe.MessageFormat.Tests/MessageFormatterTests.cs b/src/Jeffijoe.MessageFormat.Tests/MessageFormatterTests.cs index a7c3202..62b533a 100644 --- a/src/Jeffijoe.MessageFormat.Tests/MessageFormatterTests.cs +++ b/src/Jeffijoe.MessageFormat.Tests/MessageFormatterTests.cs @@ -111,7 +111,7 @@ public void FormatMessage() this.collectionMock.Setup(x => x.ShiftIndices(0, 4)).Callback( // The '- 2' is also done in the used implementation. - (int index, int length) => requests[1].SourceLiteral.ShiftIndices(length - 2, requests[0].SourceLiteral)); + (int _, int length) => requests[1].SourceLiteral.ShiftIndices(length - 2, requests[0].SourceLiteral)); var actual = this.subject.FormatMessage(Pattern, args); this.collectionMock.Verify(x => x.ShiftIndices(0, 4), Times.Once); @@ -136,7 +136,7 @@ public void FormatMessage() [InlineData(@"Hello ''{buddy}'', how are you '{doing}'?", @"Hello '{buddy}', how are you {doing}?")] public void UnescapeLiterals(string source, string expected) { - var actual = this.subject.UnescapeLiterals(new StringBuilder(source)).ToString(); + var actual = this.subject.UnescapeLiterals(new StringBuilder(source)); Assert.Equal(expected, actual); } @@ -175,7 +175,7 @@ public void VerifyFormatMessageThrowsWhenVariablesAreMissingAndTheFormatterRequi this.collectionMock.Setup(x => x.ShiftIndices(0, 4)).Callback( // The '- 2' is also done in the used implementation. - (int index, int length) => requests[1].SourceLiteral.ShiftIndices(length - 2, requests[0].SourceLiteral)); + (int _, int length) => requests[1].SourceLiteral.ShiftIndices(length - 2, requests[0].SourceLiteral)); var ex = Assert.Throws(() => this.subject.FormatMessage(Pattern, args)); Assert.Equal("name", ex.MissingVariable); diff --git a/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/GeneratedPluralRulesTests.cs b/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/GeneratedPluralRulesTests.cs index 7f8606a..dac2477 100644 --- a/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/GeneratedPluralRulesTests.cs +++ b/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/GeneratedPluralRulesTests.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Text; using Jeffijoe.MessageFormat.Formatting; using Jeffijoe.MessageFormat.Formatting.Formatters; using Jeffijoe.MessageFormat.Parsing; diff --git a/src/Jeffijoe.MessageFormat.Tests/Parsing/FormatterRequestCollectionTests.cs b/src/Jeffijoe.MessageFormat.Tests/Parsing/FormatterRequestCollectionTests.cs index 7508731..4ca2b19 100644 --- a/src/Jeffijoe.MessageFormat.Tests/Parsing/FormatterRequestCollectionTests.cs +++ b/src/Jeffijoe.MessageFormat.Tests/Parsing/FormatterRequestCollectionTests.cs @@ -5,8 +5,6 @@ // Copyright (C) Jeff Hansen 2015. All rights reserved. using System.Linq; -using System.Text; - using Jeffijoe.MessageFormat.Formatting; using Jeffijoe.MessageFormat.Parsing; diff --git a/src/Jeffijoe.MessageFormat.Tests/Parsing/LiteralParserTests.cs b/src/Jeffijoe.MessageFormat.Tests/Parsing/LiteralParserTests.cs index f80eb02..61fcb65 100644 --- a/src/Jeffijoe.MessageFormat.Tests/Parsing/LiteralParserTests.cs +++ b/src/Jeffijoe.MessageFormat.Tests/Parsing/LiteralParserTests.cs @@ -133,7 +133,7 @@ public void ParseLiterals_position_and_inner_text(string source, int[] position, var subject = new LiteralParser(); var actual = subject.ParseLiterals(sb); var first = actual.First(); - string innerText = first.InnerText.ToString(); + string innerText = first.InnerText; Assert.Equal(expectedInnerText, innerText); Assert.Equal(position[0], first.StartIndex); diff --git a/src/Jeffijoe.MessageFormat.Tests/Parsing/LiteralTests.cs b/src/Jeffijoe.MessageFormat.Tests/Parsing/LiteralTests.cs index 7b7a5cb..0addf7a 100644 --- a/src/Jeffijoe.MessageFormat.Tests/Parsing/LiteralTests.cs +++ b/src/Jeffijoe.MessageFormat.Tests/Parsing/LiteralTests.cs @@ -4,8 +4,6 @@ // Author: Jeff Hansen // Copyright (C) Jeff Hansen 2015. All rights reserved. -using System.Text; - using Jeffijoe.MessageFormat.Parsing; using Xunit; diff --git a/src/Jeffijoe.MessageFormat.Tests/Parsing/PatternParser/PatternParserGetKeyTests.cs b/src/Jeffijoe.MessageFormat.Tests/Parsing/PatternParserGetKeyTests.cs similarity index 93% rename from src/Jeffijoe.MessageFormat.Tests/Parsing/PatternParser/PatternParserGetKeyTests.cs rename to src/Jeffijoe.MessageFormat.Tests/Parsing/PatternParserGetKeyTests.cs index 84161b2..7e5c79f 100644 --- a/src/Jeffijoe.MessageFormat.Tests/Parsing/PatternParser/PatternParserGetKeyTests.cs +++ b/src/Jeffijoe.MessageFormat.Tests/Parsing/PatternParserGetKeyTests.cs @@ -5,10 +5,7 @@ // Copyright (C) Jeff Hansen 2015. All rights reserved. using System.Collections.Generic; -using System.Text; - using Jeffijoe.MessageFormat.Parsing; - using Xunit; using Xunit.Abstractions; @@ -48,7 +45,7 @@ public PatternParserGetKeyTests(ITestOutputHelper outputHelper) /// /// Gets the get key_throws_with_invalid_characters_ case. /// - public static IEnumerable GetKey_throws_with_invalid_characters_Case + public static IEnumerable GetKeyThrowsWithInvalidCharactersCase { get { @@ -107,16 +104,15 @@ public void ReadLiteralSection(string source, string expected, int expectedLastI /// The expected column. /// [Theory] - [MemberData(nameof(GetKey_throws_with_invalid_characters_Case))] + [MemberData(nameof(GetKeyThrowsWithInvalidCharactersCase))] public void ReadLiteralSection_throws_with_invalid_characters( Literal literal, int expectedLine, int expectedColumn) { - int lastIndex; var ex = Assert.Throws( - () => PatternParser.ReadLiteralSection(literal, 0, false, out lastIndex)); + () => PatternParser.ReadLiteralSection(literal, 0, false, out _)); Assert.Equal(expectedLine, ex.LineNumber); Assert.Equal(expectedColumn, ex.ColumnNumber); this.outputHelper.WriteLine(ex.Message); @@ -143,8 +139,7 @@ public void ReadLiteralSection_throws_with_invalid_characters( public void ReadLiteralSection_with_offset(string source, string expected, int offset) { var literal = new Literal(10, 10, 1, 1, source); - int lastIndex; - Assert.Equal(expected, PatternParser.ReadLiteralSection(literal, offset, true, out lastIndex)); + Assert.Equal(expected, PatternParser.ReadLiteralSection(literal, offset, true, out _)); } #endregion diff --git a/src/Jeffijoe.MessageFormat.Tests/Parsing/PatternParser/PatternParserParseTests.cs b/src/Jeffijoe.MessageFormat.Tests/Parsing/PatternParserParseTests.cs similarity index 98% rename from src/Jeffijoe.MessageFormat.Tests/Parsing/PatternParser/PatternParserParseTests.cs rename to src/Jeffijoe.MessageFormat.Tests/Parsing/PatternParserParseTests.cs index 92a3abd..f25cbbb 100644 --- a/src/Jeffijoe.MessageFormat.Tests/Parsing/PatternParser/PatternParserParseTests.cs +++ b/src/Jeffijoe.MessageFormat.Tests/Parsing/PatternParserParseTests.cs @@ -4,7 +4,6 @@ // Author: Jeff Hansen // Copyright (C) Jeff Hansen 2015. All rights reserved. -using System.Linq; using System.Text; using Jeffijoe.MessageFormat.Parsing; @@ -89,7 +88,7 @@ public void Parse(string source, string expectedKey, string expectedFormat, stri var actual = subject.Parse(sb); Benchmark.End(this.outputHelper); Assert.Single(actual); - var first = actual.First(); + var first = actual[0]; Assert.Equal(expectedKey, first.Variable); Assert.Equal(expectedFormat, first.FormatterName); Assert.Equal(expectedArgs, first.FormatterArguments); diff --git a/src/Jeffijoe.MessageFormat.Tests/Parsing/PatternParser/PatternParserWithRealLiteralParser.cs b/src/Jeffijoe.MessageFormat.Tests/Parsing/PatternParserWithRealLiteralParser.cs similarity index 100% rename from src/Jeffijoe.MessageFormat.Tests/Parsing/PatternParser/PatternParserWithRealLiteralParser.cs rename to src/Jeffijoe.MessageFormat.Tests/Parsing/PatternParserWithRealLiteralParser.cs diff --git a/src/Jeffijoe.MessageFormat/Formatting/BaseFormatter.cs b/src/Jeffijoe.MessageFormat/Formatting/BaseFormatter.cs index 78f4bba..f039d88 100644 --- a/src/Jeffijoe.MessageFormat/Formatting/BaseFormatter.cs +++ b/src/Jeffijoe.MessageFormat/Formatting/BaseFormatter.cs @@ -4,10 +4,7 @@ // Copyright (C) Jeff Hansen 2014. All rights reserved. using System.Collections.Generic; -using System.IO; using System.Linq; -using System.Text; - using Jeffijoe.MessageFormat.Parsing; namespace Jeffijoe.MessageFormat.Formatting diff --git a/src/Jeffijoe.MessageFormat/Formatting/FormatterLibrary.cs b/src/Jeffijoe.MessageFormat/Formatting/FormatterLibrary.cs index a329e12..699815e 100644 --- a/src/Jeffijoe.MessageFormat/Formatting/FormatterLibrary.cs +++ b/src/Jeffijoe.MessageFormat/Formatting/FormatterLibrary.cs @@ -4,8 +4,6 @@ // Copyright (C) Jeff Hansen 2014. All rights reserved. using System.Collections.Generic; -using System.Linq; - using Jeffijoe.MessageFormat.Formatting.Formatters; namespace Jeffijoe.MessageFormat.Formatting diff --git a/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralFormatter.cs b/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralFormatter.cs index 50e91d7..c9469fb 100644 --- a/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralFormatter.cs +++ b/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralFormatter.cs @@ -7,7 +7,6 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; -using System.Text; namespace Jeffijoe.MessageFormat.Formatting.Formatters { diff --git a/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralRulesMetadata.cs b/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralRulesMetadata.cs index 9d76e4b..305870d 100644 --- a/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralRulesMetadata.cs +++ b/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralRulesMetadata.cs @@ -2,6 +2,6 @@ { internal static partial class PluralRulesMetadata { - public static partial bool TryGetRuleByLocale(string locale, out ContextPluralizer pluralizer); + public static partial bool TryGetRuleByLocale(string locale, out ContextPluralizer contextPluralizer); } } diff --git a/src/Jeffijoe.MessageFormat/Helpers/ObjectHelper.cs b/src/Jeffijoe.MessageFormat/Helpers/ObjectHelper.cs index d09a31c..5e5af0f 100644 --- a/src/Jeffijoe.MessageFormat/Helpers/ObjectHelper.cs +++ b/src/Jeffijoe.MessageFormat/Helpers/ObjectHelper.cs @@ -31,7 +31,7 @@ internal static IEnumerable GetProperties(object obj) var properties = new List(); var type = obj.GetType(); var typeInfo = type.GetTypeInfo(); - while (typeInfo != null) + while (true) { properties.AddRange(typeInfo.DeclaredProperties); if (typeInfo.BaseType == null) diff --git a/src/Jeffijoe.MessageFormat/Helpers/StringBuilderHelper.cs b/src/Jeffijoe.MessageFormat/Helpers/StringBuilderHelper.cs index 972ffa7..285c2de 100644 --- a/src/Jeffijoe.MessageFormat/Helpers/StringBuilderHelper.cs +++ b/src/Jeffijoe.MessageFormat/Helpers/StringBuilderHelper.cs @@ -3,7 +3,10 @@ // Author: Jeff Hansen // Copyright (C) Jeff Hansen 2014. All rights reserved. +#if NET5_0_OR_GREATER using System; +#endif + using System.Text; namespace Jeffijoe.MessageFormat.Helpers @@ -27,7 +30,7 @@ internal static class StringBuilderHelper /// /// The . /// - internal static bool Contains(this StringBuilder src, params char[] chars) + private static bool Contains(this StringBuilder src, params char[] chars) { #if NET5_0_OR_GREATER foreach (var chunk in src.GetChunks()) diff --git a/src/Jeffijoe.MessageFormat/Jeffijoe.MessageFormat.csproj b/src/Jeffijoe.MessageFormat/Jeffijoe.MessageFormat.csproj index 9cc3d23..1418745 100644 --- a/src/Jeffijoe.MessageFormat/Jeffijoe.MessageFormat.csproj +++ b/src/Jeffijoe.MessageFormat/Jeffijoe.MessageFormat.csproj @@ -10,7 +10,7 @@ https://github.com/jeffijoe/messageformat.net latest enable - netstandard2.0;net5.0 + net5.0;net6.0;netstandard2.0;netstandard2.1 @@ -18,8 +18,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Jeffijoe.MessageFormat/Parsing/Literal.cs b/src/Jeffijoe.MessageFormat/Parsing/Literal.cs index 22562c1..660e6da 100644 --- a/src/Jeffijoe.MessageFormat/Parsing/Literal.cs +++ b/src/Jeffijoe.MessageFormat/Parsing/Literal.cs @@ -3,8 +3,6 @@ // Author: Jeff Hansen // Copyright (C) Jeff Hansen 2014. All rights reserved. -using System.Text; - namespace Jeffijoe.MessageFormat.Parsing { /// diff --git a/src/Jeffijoe.MessageFormat/Parsing/PatternParser.cs b/src/Jeffijoe.MessageFormat/Parsing/PatternParser.cs index 717a4e7..47d744c 100644 --- a/src/Jeffijoe.MessageFormat/Parsing/PatternParser.cs +++ b/src/Jeffijoe.MessageFormat/Parsing/PatternParser.cs @@ -83,7 +83,8 @@ public IFormatterRequestCollection Parse(StringBuilder source) { #if NET5_0_OR_GREATER formatterArgs = - literal.InnerText.AsSpan(lastIndex + 1, literal.InnerText.Length - lastIndex - 1).Trim().ToString(); + literal.InnerText.AsSpan(lastIndex + 1, literal.InnerText.Length - lastIndex - 1).Trim() + .ToString(); #else formatterArgs = literal.InnerText.Substring(lastIndex + 1, literal.InnerText.Length - lastIndex - 1).Trim(); @@ -151,11 +152,14 @@ public IFormatterRequestCollection Parse(StringBuilder source) { if (c.IsAlphaNumeric() == false) { - var msg = string.Format("Invalid literal character '{0}'.", c); + var msg = $"Invalid literal character '{c}'."; // Line number can't have changed. - throw new MalformedLiteralException(msg, literal.SourceLineNumber, column, - innerText.ToString()); + throw new MalformedLiteralException( + msg, + literal.SourceLineNumber, + column, + innerText); } } else diff --git a/src/Jeffijoe.MessageFormat/Properties/AssemblyInfo.cs b/src/Jeffijoe.MessageFormat/Properties/AssemblyInfo.cs index 18ae6d2..f9ea5d5 100644 --- a/src/Jeffijoe.MessageFormat/Properties/AssemblyInfo.cs +++ b/src/Jeffijoe.MessageFormat/Properties/AssemblyInfo.cs @@ -3,8 +3,6 @@ // Author: Jeff Hansen // Copyright (C) Jeff Hansen 2014. All rights reserved. -using System.Reflection; -using System.Resources; using System.Runtime.CompilerServices; [assembly: InternalsVisibleTo("Jeffijoe.MessageFormat.Tests, PublicKey=00240000048000009400000006020000002400005253413100040000010001004f0dc10ad8873751f986416a7d3134e473ad3454a7138e26cf9760eff341709fc50e69d985b4e0ea512b5628079043a31ce1e402f4eaebffb5772eed9ef3a7a9e39f122633f19529a1e9988005289ed09a1e294abe13152afebc59835c2fef2879d8641b564daa7f511298ec2f8a3e9b322819e05cbaea0bfc73fe100615bfc7")] diff --git a/src/Jeffijoe.MessageFormat/StringBuilderPool.cs b/src/Jeffijoe.MessageFormat/StringBuilderPool.cs index 12368af..8720a86 100644 --- a/src/Jeffijoe.MessageFormat/StringBuilderPool.cs +++ b/src/Jeffijoe.MessageFormat/StringBuilderPool.cs @@ -1,5 +1,4 @@ -using System; -using System.Text; +using System.Text; using Microsoft.Extensions.ObjectPool; namespace Jeffijoe.MessageFormat From 104d00979769ee837d3d61c01d77a12c92ec74cc Mon Sep 17 00:00:00 2001 From: Jeff Hansen Date: Fri, 14 Jan 2022 17:05:29 -0500 Subject: [PATCH 52/98] Use correct offset when parsing formatter arguments Fixes #27 --- .../MessageFormatterFullIntegrationTests.cs | 18 +++++++++++++----- .../MessageFormatterIssues.cs | 14 +++++++++++++- .../Parsing/PatternParser.cs | 7 +++---- 3 files changed, 29 insertions(+), 10 deletions(-) diff --git a/src/Jeffijoe.MessageFormat.Tests/MessageFormatterFullIntegrationTests.cs b/src/Jeffijoe.MessageFormat.Tests/MessageFormatterFullIntegrationTests.cs index 786d7cc..d4044ff 100644 --- a/src/Jeffijoe.MessageFormat.Tests/MessageFormatterFullIntegrationTests.cs +++ b/src/Jeffijoe.MessageFormat.Tests/MessageFormatterFullIntegrationTests.cs @@ -5,10 +5,8 @@ // Copyright (C) Jeff Hansen 2015. All rights reserved. using System.Collections.Generic; - using Jeffijoe.MessageFormat.Formatting; using Jeffijoe.MessageFormat.Tests.TestHelpers; - using Xunit; using Xunit.Abstractions; @@ -208,7 +206,8 @@ public static IEnumerable Tests new object[] { Case2, - new Dictionary { { "gender", "female" }, { "name", "Amanda" }, { "count", 1 } }, + new Dictionary + { { "gender", "female" }, { "name", "Amanda" }, { "count", 1 } }, "She - {Amanda} - said: You have just one notification. Have a nice day!" }; yield return @@ -271,7 +270,8 @@ public static IEnumerable Tests new object[] { Case5, - new Dictionary { { "count", 42 }, { "gender", "female" }, { "genitals", 102 } }, + new Dictionary + { { "count", 42 }, { "gender", "female" }, { "genitals", 102 } }, "She (who has the freakish amount of 102 boobies) said: You have a universal amount of notifications. Have a nice day!" }; yield return @@ -334,6 +334,13 @@ public static IEnumerable Tests new Dictionary { { "count", 3 } }, "You and 2 others added this to their profiles." }; + yield return + new object[] + { + "{ count, plural, one {1 thing} other {# things} }", + new Dictionary { { "count", 2 } }, + "2 things" + }; } } @@ -582,7 +589,8 @@ public void ReadMe_test_to_make_sure_I_dont_look_like_a_fool() { var mf = new MessageFormatter(useCache: true, locale: "en"); - mf.Pluralizers!["en"] = n => { + mf.Pluralizers!["en"] = n => + { // ´n´ is the number being pluralized. // ReSharper disable once CompareOfFloatsByEqualityOperator if (n == 0) diff --git a/src/Jeffijoe.MessageFormat.Tests/MessageFormatterIssues.cs b/src/Jeffijoe.MessageFormat.Tests/MessageFormatterIssues.cs index fbddc7d..f6da2d2 100644 --- a/src/Jeffijoe.MessageFormat.Tests/MessageFormatterIssues.cs +++ b/src/Jeffijoe.MessageFormat.Tests/MessageFormatterIssues.cs @@ -14,7 +14,7 @@ namespace Jeffijoe.MessageFormat.Tests public class MessageFormatterIssues { [Fact] - public void Issue13() + public void Issue13_Bad_escaping_on_pound_symbol() { string plural = @"{num_guests, plural, offset:1, other {# {host} invites # people to their party.}}"; string broken = @"{num_guests, plural, offset:1, other {{host} invites # people to their party.}}"; @@ -24,5 +24,17 @@ public void Issue13() Assert.Equal("Mary invites 4 people to their party.", mf.FormatMessage(broken, vars)); Assert.Equal("4 Mary invites 4 people to their party.", mf.FormatMessage(plural, vars)); } + + [Fact] + public void Issue27_WhiteSpace_in_identifiers_is_ignored() + { + var subject = new MessageFormatter(false); + var result = subject.FormatMessage("{ count, plural , one {1 thing} other {# things} }", new + { + count = 2 + }); + + Assert.Equal("2 things", result); + } } } \ No newline at end of file diff --git a/src/Jeffijoe.MessageFormat/Parsing/PatternParser.cs b/src/Jeffijoe.MessageFormat/Parsing/PatternParser.cs index 47d744c..aede0a5 100644 --- a/src/Jeffijoe.MessageFormat/Parsing/PatternParser.cs +++ b/src/Jeffijoe.MessageFormat/Parsing/PatternParser.cs @@ -68,8 +68,7 @@ public IFormatterRequestCollection Parse(StringBuilder source) foreach (var literal in literals) { // The first token to follow an opening brace will be the variable name. - int lastIndex; - string variableName = ReadLiteralSection(literal, 0, false, out lastIndex)!; + var variableName = ReadLiteralSection(literal, 0, false, out var lastIndex)!; // The next (if any), is the formatter to use. Null is allowed. string? formatterKey = null; @@ -78,7 +77,7 @@ public IFormatterRequestCollection Parse(StringBuilder source) string? formatterArgs = null; if (variableName.Length != literal.InnerText.Length) { - formatterKey = ReadLiteralSection(literal, variableName.Length + 1, true, out lastIndex); + formatterKey = ReadLiteralSection(literal, lastIndex + 1, true, out lastIndex); if (formatterKey != null) { #if NET5_0_OR_GREATER @@ -147,7 +146,7 @@ public IFormatterRequestCollection Parse(StringBuilder source) } // Disregard whitespace. - var whitespace = c == ' ' || c == '\r' || c == '\n' || c == '\t'; + var whitespace = char.IsWhiteSpace(c); if (!whitespace) { if (c.IsAlphaNumeric() == false) From facedda335f03426211fad782708d367f386cd37 Mon Sep 17 00:00:00 2001 From: Jeff Hansen Date: Fri, 14 Jan 2022 20:13:50 -0500 Subject: [PATCH 53/98] 100% coverage, remove Moq and delete unreachable code --- .../Formatting/BaseFormatterTests.cs | 68 +++++-- .../Formatting/FormatterLibraryTests.cs | 28 +-- .../Formatters/PluralFormatterTests.cs | 45 ++++- .../Formatters/SelectFormatterTests.cs | 40 ++-- .../Formatters/VariableFormatterTests.cs | 17 +- .../Jeffijoe.MessageFormat.Tests.csproj | 1 - .../MessageFormatterCachingTests.cs | 16 +- .../MessageFormatterFullIntegrationTests.cs | 14 ++ .../MessageFormatterTests.cs | 183 ++++-------------- .../MessageFormatterUsingRealParserTests.cs | 14 +- .../Parsing/PatternParserParseTests.cs | 26 +-- .../TestHelpers/FakeFormatter.cs | 49 +++++ .../TestHelpers/FakeLiteralParser.cs | 41 ++++ .../TestHelpers/FakeMessageFormatter.cs | 14 ++ .../TestHelpers/TrackingPatternParser.cs | 36 ++++ .../Formatting/BaseFormatter.cs | 9 - .../Formatting/Formatters/PluralFormatter.cs | 60 +++--- .../Formatters/PluralRulesMetadata.cs | 1 + .../Formatting/Formatters/SelectFormatter.cs | 1 + .../Formatting/ParsedArguments.cs | 10 - .../MessageFormatter.cs | 82 +++----- .../MessageFormatterException.cs | 15 -- .../Parsing/PatternParser.cs | 12 +- .../Parsing/UnbalancedBracesException.cs | 5 - 24 files changed, 447 insertions(+), 340 deletions(-) create mode 100644 src/Jeffijoe.MessageFormat.Tests/TestHelpers/FakeFormatter.cs create mode 100644 src/Jeffijoe.MessageFormat.Tests/TestHelpers/FakeLiteralParser.cs create mode 100644 src/Jeffijoe.MessageFormat.Tests/TestHelpers/FakeMessageFormatter.cs create mode 100644 src/Jeffijoe.MessageFormat.Tests/TestHelpers/TrackingPatternParser.cs diff --git a/src/Jeffijoe.MessageFormat.Tests/Formatting/BaseFormatterTests.cs b/src/Jeffijoe.MessageFormat.Tests/Formatting/BaseFormatterTests.cs index e1c52d8..a2b8bed 100644 --- a/src/Jeffijoe.MessageFormat.Tests/Formatting/BaseFormatterTests.cs +++ b/src/Jeffijoe.MessageFormat.Tests/Formatting/BaseFormatterTests.cs @@ -4,12 +4,12 @@ // Author: Jeff Hansen // Copyright (C) Jeff Hansen 2015. All rights reserved. +using System; using System.Collections.Generic; using System.Linq; using Jeffijoe.MessageFormat.Formatting; using Jeffijoe.MessageFormat.Parsing; using Jeffijoe.MessageFormat.Tests.TestHelpers; - using Xunit; using Xunit.Abstractions; @@ -78,10 +78,17 @@ public static IEnumerable ParseArgumentsTests /// /// Gets the parse keyed blocks_tests. /// - public static IEnumerable ParseKeyedBlocksTests + public static IEnumerable ParseKeyedBlocksTests { get { + yield return + new object?[] + { + null, + Array.Empty(), + Array.Empty() + }; yield return new object[] { @@ -96,18 +103,33 @@ public static IEnumerable ParseKeyedBlocksTests new[] { "zero", "other" }, new[] { string.Empty, "wee" } }; - yield return new object[] { @" + yield return + new object[] + { + "male {''he''}", + new[] { "male"}, + new[] { "''he''" } + }; + yield return new object[] + { + @" male {he} female {she} unknown {they} -", new[] { "male", "female", "unknown" }, new[] { "he", "she", "they" } }; - yield return new object[] { @" +", + new[] { "male", "female", "unknown" }, new[] { "he", "she", "they" } + }; + yield return new object[] + { + @" male {he} female {she{dawg}} unknown {they'{dawg}'} -", new[] { "male", "female", "unknown" }, new[] { "he", "she{dawg}", @"they'{dawg}'" } }; +", + new[] { "male", "female", "unknown" }, new[] { "he", "she{dawg}", @"they'{dawg}'" } + }; } } @@ -183,6 +205,7 @@ public void ParseArguments_invalid(string args) var subject = new BaseFormatterImpl(); var req = new FormatterRequest(new Literal(1, 1, 1, 1, ""), string.Empty, null, args); var ex = Assert.Throws(() => subject.ParseArguments(req)); + Assert.Equal(args, ex.SourceSnippet); this.outputHelper.WriteLine(ex.Message); } @@ -204,14 +227,13 @@ public void ParseArguments_invalid(string args) [Theory] [InlineData(" offset:3 boom", "offset", "3", 9)] [InlineData("testie:dawg lel", "testie", "dawg", 11)] - public void ParseExtensions(string args, string extension, string value, int expectedIndex) + public void ParseExtensions(string? args, string extension, string value, int expectedIndex) { var subject = new BaseFormatterImpl(); - int index; var req = new FormatterRequest(new Literal(1, 1, 1, 1, ""), string.Empty, null, args); // Warmup - subject.ParseExtensions(req, out index); + subject.ParseExtensions(req, out var index); Benchmark.Start("Parsing extensions a few times (warmed up)", this.outputHelper); for (int i = 0; i < 1000; i++) @@ -228,6 +250,22 @@ public void ParseExtensions(string args, string extension, string value, int exp Assert.Equal(value, first.Value); Assert.Equal(expectedIndex, index); } + + /// + /// The parse extensions returns empty collection when formatter arguments is null. + /// + [Fact] + public void ParseExtensions_returns_empty_collection_when_formatter_arguments_is_null() + { + string? args = null; + var subject = new BaseFormatterImpl(); + var req = new FormatterRequest(new Literal(1, 1, 1, 1, ""), string.Empty, null, args); + + var actual = subject.ParseExtensions(req, out var index); + + Assert.Empty(actual); + Assert.Equal(-1, index); + } /// /// The parse extensions_multiple. @@ -236,13 +274,12 @@ public void ParseExtensions(string args, string extension, string value, int exp public void ParseExtensions_multiple() { var subject = new BaseFormatterImpl(); - int index; var args = " offset:2 code:js "; var expectedIndex = 17; var req = new FormatterRequest(new Literal(1, 1, 1, 1, ""), string.Empty, null, args); - var actual = subject.ParseExtensions(req, out index).ToList(); + var actual = subject.ParseExtensions(req, out var index).ToList(); Assert.NotEmpty(actual); var result = actual.First(); Assert.Equal("offset", result.Extension); @@ -269,7 +306,7 @@ public void ParseExtensions_multiple() /// [Theory] [MemberData(nameof(ParseKeyedBlocksTests))] - public void ParseKeyedBlocks(string args, string[] keys, string[] values) + public void ParseKeyedBlocks(string? args, string[] keys, string[] values) { var subject = new BaseFormatterImpl(); var req = new FormatterRequest(new Literal(1, 1, 1, 1, ""), string.Empty, null, args); @@ -311,7 +348,12 @@ public void ParseKeyedBlocks(string args, string[] keys, string[] values) [Theory] [InlineData("male {he} other {'{they}")] [InlineData("male {he} other {'# they}")] - public void ParseKeyedBlocks_unclosed_escape_sequence(string args) + [InlineData("male {he} other }")] + [InlineData("male {he} other {'")] + [InlineData("male {he} other {'{'")] + [InlineData("male{}} female{}")] + [InlineData("male haha")] + public void ParseKeyedBlocks_bad_formatting(string? args) { var subject = new BaseFormatterImpl(); var req = new FormatterRequest(new Literal(1, 1, 1, 1, ""), string.Empty, null, args); diff --git a/src/Jeffijoe.MessageFormat.Tests/Formatting/FormatterLibraryTests.cs b/src/Jeffijoe.MessageFormat.Tests/Formatting/FormatterLibraryTests.cs index 83795d7..be95522 100644 --- a/src/Jeffijoe.MessageFormat.Tests/Formatting/FormatterLibraryTests.cs +++ b/src/Jeffijoe.MessageFormat.Tests/Formatting/FormatterLibraryTests.cs @@ -6,9 +6,7 @@ using Jeffijoe.MessageFormat.Formatting; using Jeffijoe.MessageFormat.Parsing; - -using Moq; - +using Jeffijoe.MessageFormat.Tests.TestHelpers; using Xunit; namespace Jeffijoe.MessageFormat.Tests.Formatting @@ -27,24 +25,30 @@ public class FormatterLibraryTests public void GetFormatter() { var subject = new FormatterLibrary(); - var mock1 = new Mock(); - var mock2 = new Mock(); - + var req = new FormatterRequest(new Literal(1, 1, 1, 1, ""), "test", "dawg", null); - subject.Add(mock1.Object); - subject.Add(mock2.Object); + var formatter1 = new FakeFormatter(); + var formatter2 = new FakeFormatter(); + + subject.Add(formatter1); + subject.Add(formatter2); Assert.Throws(() => subject.GetFormatter(req)); - mock2.Setup(x => x.CanFormat(req)).Returns(true); + formatter2.SetCanFormat(true); + var actual = subject.GetFormatter(req); - Assert.Same(mock2.Object, actual); + Assert.Same(formatter2, actual); - mock1.Setup(x => x.CanFormat(req)).Returns(true); + formatter1.SetCanFormat(true); actual = subject.GetFormatter(req); - Assert.Same(mock1.Object, actual); + Assert.Same(formatter1, actual); } #endregion + + #region Fakes + + #endregion } } \ No newline at end of file diff --git a/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/PluralFormatterTests.cs b/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/PluralFormatterTests.cs index fe398e5..43d56bc 100644 --- a/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/PluralFormatterTests.cs +++ b/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/PluralFormatterTests.cs @@ -46,12 +46,54 @@ public void Pluralize(double n, string expected) new KeyedBlock("one", "just one"), new KeyedBlock("other", "wow") }, - new FormatterExtension[0]); + Array.Empty()); var request = new FormatterRequest(new Literal(1, 1, 1, 1, ""), "test", "plural", null); var actual = subject.Pluralize("en", arguments, new PluralContext(Convert.ToDecimal(Convert.ToDouble(args[request.Variable]))), 0); Assert.Equal(expected, actual); } + /// + /// The pluralize_defaults_to_en_locale_when_specified_locale_is_not_found + /// + [Fact] + public void Pluralize_defaults_to_en_locale_when_specified_locale_is_not_found() + { + var subject = new PluralFormatter(); + var args = new Dictionary { { "test", 1 } }; + var arguments = + new ParsedArguments( + new[] + { + new KeyedBlock("zero", "nothing"), + new KeyedBlock("one", "just one"), + new KeyedBlock("other", "wow") + }, + Array.Empty()); + var request = new FormatterRequest(new Literal(1, 1, 1, 1, ""), "test", "plural", null); + var actual = subject.Pluralize("unknown", arguments, new PluralContext(Convert.ToDecimal(Convert.ToDouble(args[request.Variable]))), 0); + Assert.Equal("just one", actual); + } + + /// + /// The pluralize_defaults_to_en_locale_when_specified_locale_is_not_found + /// + [Fact] + public void Pluralize_throws_when_missing_other_block() + { + var subject = new PluralFormatter(); + var args = new Dictionary { { "test", 5 } }; + var arguments = + new ParsedArguments( + new[] + { + new KeyedBlock("zero", "nothing"), + new KeyedBlock("one", "just one") + }, + Array.Empty()); + var request = new FormatterRequest(new Literal(1, 1, 1, 1, ""), "test", "plural", null); + Assert.Throws(() => subject.Pluralize("unknown", arguments, new PluralContext(Convert.ToDecimal(Convert.ToDouble(args[request.Variable]))), 0)); + } + /// /// The replace number literals. /// @@ -65,6 +107,7 @@ public void Pluralize(double n, string expected) [InlineData(@"Number '#1' has # results", "Number '#1' has 1337 results")] [InlineData(@"Number '#'1 has # results", "Number '#'1 has 1337 results")] [InlineData(@"Number '#'# has # results", "Number '#'1337 has 1337 results")] + [InlineData(@"Number '''#'''# has # results", "Number '''#'''1337 has 1337 results")] [InlineData(@"# results", "1337 results")] public void ReplaceNumberLiterals(string input, string expected) { diff --git a/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/SelectFormatterTests.cs b/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/SelectFormatterTests.cs index d37e634..a35bedb 100644 --- a/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/SelectFormatterTests.cs +++ b/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/SelectFormatterTests.cs @@ -8,9 +8,7 @@ using Jeffijoe.MessageFormat.Formatting; using Jeffijoe.MessageFormat.Formatting.Formatters; using Jeffijoe.MessageFormat.Parsing; - -using Moq; - +using Jeffijoe.MessageFormat.Tests.TestHelpers; using Xunit; namespace Jeffijoe.MessageFormat.Tests.Formatting.Formatters @@ -30,7 +28,8 @@ public static IEnumerable FormatTests get { yield return new object[] { "male {he said} female {she said} other {they said}", "male", "he said" }; - yield return new object[] { "male {he said} female {she said} other {they said}", "female", "she said" }; + yield return new object[] + { "male {he said} female {she said} other {they said}", "female", "she said" }; yield return new object[] { "male {he said} female {she said} other {they said}", "dawg", "they said" }; } } @@ -56,19 +55,38 @@ public static IEnumerable FormatTests public void Format(string formatterArgs, string gender, string expectedBlock) { var subject = new SelectFormatter(); - var messageFormatterMock = new Mock(); - messageFormatterMock.Setup(x => x.FormatMessage(It.IsAny(), It.IsAny>())) - .Returns((string input, Dictionary _) => input); + var messageFormatter = new FakeMessageFormatter(); var req = new FormatterRequest( - new Literal(1, 1, 1, 1, ""), - "gender", - "select", + new Literal(1, 1, 1, 1, ""), + "gender", + "select", formatterArgs); var args = new Dictionary { { "gender", gender } }; - var result = subject.Format("en", req, args, gender, messageFormatterMock.Object); + var result = subject.Format("en", req, args, gender, messageFormatter); Assert.Equal(expectedBlock, result); } + /// + /// Verifies that format throws when no other option is given. + /// + [Fact] + public void VerifyFormatThrowsWhenNoOtherOptionIsGiven() + { + var subject = new SelectFormatter(); + var messageFormatter = new FakeMessageFormatter(); + var req = new FormatterRequest( + new Literal(1, 1, 1, 1, ""), + "gender", + "select", + "male {he} female{she}"); + var args = new Dictionary { { "gender", "non-binary" } }; + + Assert.Throws(() => + { + subject.Format("en", req, args, "non-binary", messageFormatter); + }); + } + #endregion } } \ No newline at end of file diff --git a/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/VariableFormatterTests.cs b/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/VariableFormatterTests.cs index c8e095c..6c0232b 100644 --- a/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/VariableFormatterTests.cs +++ b/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/VariableFormatterTests.cs @@ -8,8 +8,7 @@ using Jeffijoe.MessageFormat.Formatting; using Jeffijoe.MessageFormat.Formatting.Formatters; using Jeffijoe.MessageFormat.Parsing; - -using Moq; +using Jeffijoe.MessageFormat.Tests.TestHelpers; using Xunit; @@ -23,14 +22,14 @@ public class VariableFormatterTests #region Fields /// - /// The formatter mock. + /// The subject. /// - private readonly Mock formatterMock; + private readonly VariableFormatter subject; /// - /// The subject. + /// The fake message formatter. /// - private readonly VariableFormatter subject; + private readonly IMessageFormatter formatter; #endregion @@ -41,7 +40,7 @@ public class VariableFormatterTests /// public VariableFormatterTests() { - this.formatterMock = new Mock(); + this.formatter = new FakeMessageFormatter(); this.subject = new VariableFormatter(); } @@ -58,7 +57,7 @@ public void VerifyAnEmptyStringIsReturnedWhenTheArgumentIsNull() var req = CreateRequest(); var args = new Dictionary(); - Assert.Equal(string.Empty, this.subject.Format("en", req, args, null, this.formatterMock.Object)); + Assert.Equal(string.Empty, this.subject.Format("en", req, args, null, this.formatter)); } /// @@ -70,7 +69,7 @@ public void VerifyTheValueFromTheGivenArgumentsIsReturnedAsAString() var req = CreateRequest(); var args = new Dictionary(); - Assert.Equal("is good", this.subject.Format("en", req, args, "is good", this.formatterMock.Object)); + Assert.Equal("is good", this.subject.Format("en", req, args, "is good", this.formatter)); } #endregion diff --git a/src/Jeffijoe.MessageFormat.Tests/Jeffijoe.MessageFormat.Tests.csproj b/src/Jeffijoe.MessageFormat.Tests/Jeffijoe.MessageFormat.Tests.csproj index 3274415..c2405df 100644 --- a/src/Jeffijoe.MessageFormat.Tests/Jeffijoe.MessageFormat.Tests.csproj +++ b/src/Jeffijoe.MessageFormat.Tests/Jeffijoe.MessageFormat.Tests.csproj @@ -12,7 +12,6 @@ - diff --git a/src/Jeffijoe.MessageFormat.Tests/MessageFormatterCachingTests.cs b/src/Jeffijoe.MessageFormat.Tests/MessageFormatterCachingTests.cs index dd26e6a..7b676b1 100644 --- a/src/Jeffijoe.MessageFormat.Tests/MessageFormatterCachingTests.cs +++ b/src/Jeffijoe.MessageFormat.Tests/MessageFormatterCachingTests.cs @@ -11,8 +11,7 @@ using Jeffijoe.MessageFormat.Formatting; using Jeffijoe.MessageFormat.Helpers; using Jeffijoe.MessageFormat.Parsing; - -using Moq; +using Jeffijoe.MessageFormat.Tests.TestHelpers; using Xunit; using Xunit.Abstractions; @@ -56,29 +55,26 @@ public MessageFormatterCachingTests(ITestOutputHelper outputHelper) [Fact] public void FormatMessage_caches_reused_pattern() { - var parserMock = new Mock(); - var realParser = new PatternParser(new LiteralParser()); - parserMock.Setup(x => x.Parse(It.IsAny())) - .Returns((StringBuilder sb) => realParser.Parse(sb)); + var parser = new TrackingPatternParser(); var library = new FormatterLibrary(); - var subject = new MessageFormatter(patternParser: parserMock.Object, library: library, useCache: true); + var subject = new MessageFormatter(patternParser: parser, library: library, useCache: true); var pattern = "Hi {gender, select, male {Sir} female {Ma'am}}!"; var actual = subject.FormatMessage(pattern, new { gender = "male" }); Assert.Equal("Hi Sir!", actual); // '2' because it did not format "Ma'am" yet. - parserMock.Verify(x => x.Parse(It.IsAny()), Times.Exactly(2)); + Assert.Equal(2, parser.ParseCount); actual = subject.FormatMessage(pattern, new { gender = "female" }); Assert.Equal("Hi Ma'am!", actual); - parserMock.Verify(x => x.Parse(It.IsAny()), Times.Exactly(3)); + Assert.Equal(3, parser.ParseCount); // '3' because it has cached all options actual = subject.FormatMessage(pattern, new { gender = "female" }); Assert.Equal("Hi Ma'am!", actual); - parserMock.Verify(x => x.Parse(It.IsAny()), Times.Exactly(3)); + Assert.Equal(3, parser.ParseCount); } /// diff --git a/src/Jeffijoe.MessageFormat.Tests/MessageFormatterFullIntegrationTests.cs b/src/Jeffijoe.MessageFormat.Tests/MessageFormatterFullIntegrationTests.cs index d4044ff..8592c39 100644 --- a/src/Jeffijoe.MessageFormat.Tests/MessageFormatterFullIntegrationTests.cs +++ b/src/Jeffijoe.MessageFormat.Tests/MessageFormatterFullIntegrationTests.cs @@ -117,6 +117,20 @@ public static IEnumerable EscapingTests new Dictionary { { "num", 2 } }, "#{2#2}" }; + yield return + new object[] + { + "'''{'''", + new Dictionary(), + "'{'" + }; + // yield return + // new object[] + // { + // "{num, plural, =1 {1} other {'''{'''#'''}'''}}", + // new Dictionary { { "num", 2 } }, + // "'{'2'}'" + // }; } } diff --git a/src/Jeffijoe.MessageFormat.Tests/MessageFormatterTests.cs b/src/Jeffijoe.MessageFormat.Tests/MessageFormatterTests.cs index 62b533a..edae297 100644 --- a/src/Jeffijoe.MessageFormat.Tests/MessageFormatterTests.cs +++ b/src/Jeffijoe.MessageFormat.Tests/MessageFormatterTests.cs @@ -5,14 +5,11 @@ // Copyright (C) Jeff Hansen 2015. All rights reserved. using System.Collections.Generic; -using System.Linq; using System.Text; using Jeffijoe.MessageFormat.Formatting; using Jeffijoe.MessageFormat.Parsing; -using Moq; - using Xunit; namespace Jeffijoe.MessageFormat.Tests @@ -22,57 +19,6 @@ namespace Jeffijoe.MessageFormat.Tests /// public class MessageFormatterTests { - #region Fields - - /// - /// The collection mock. - /// - private readonly Mock collectionMock; - - /// - /// The formatter mock 1. - /// - private readonly Mock formatterMock1; - - /// - /// The formatter mock 2. - /// - private readonly Mock formatterMock2; - - /// - /// The library mock. - /// - private readonly Mock libraryMock; - - /// - /// The message formatter. - /// - private readonly MessageFormatter subject; - - /// - /// The pattern parser mock. - /// - private readonly Mock patternParserMock; - - #endregion - - #region Constructors and Destructors - - /// - /// Initializes a new instance of the class. - /// - public MessageFormatterTests() - { - this.patternParserMock = new Mock(); - this.libraryMock = new Mock(); - this.collectionMock = new Mock(); - this.formatterMock1 = new Mock(); - this.formatterMock2 = new Mock(); - this.subject = new MessageFormatter(this.patternParserMock.Object, this.libraryMock.Object, false); - } - - #endregion - #region Public Methods and Operators /// @@ -81,44 +27,12 @@ public MessageFormatterTests() [Fact] public void FormatMessage() { - const string Pattern = "{name} has {messages, plural, 123}."; + const string Pattern = "{name} has {messages, plural, other {# messages}}."; const string Expected = "Jeff has 123 messages."; - var args = new Dictionary { { "name", "Jeff" }, { "messages", 1 } }; - var requests = new[] - { - new FormatterRequest( - new Literal(0, 5, 1, 7, "name"), - "name", - null, - null), - new FormatterRequest( - new Literal(11, 33, 1, 7, "messages, plural, 123"), - "messages", - "plural", - " 123") - }; - - this.formatterMock1.Setup(x => x.Format("en", requests[0], args, "Jeff", this.subject)).Returns("Jeff"); - this.formatterMock2.Setup(x => x.Format("en", requests[1], args, 1, this.subject)).Returns("123 messages"); - this.collectionMock.Setup(x => x.GetEnumerator()).Returns(requests.AsEnumerable().GetEnumerator()); - this.collectionMock.Setup(x => x.Count).Returns(requests.Length); - this.collectionMock.Setup(x => x[It.IsAny()]).Returns((int i) => requests[i]); - this.libraryMock.Setup(x => x.GetFormatter(requests[0])).Returns(this.formatterMock1.Object); - this.libraryMock.Setup(x => x.GetFormatter(requests[1])).Returns(this.formatterMock2.Object); - this.patternParserMock.Setup(x => x.Parse(It.IsAny())).Returns(this.collectionMock.Object); - - // First request, and "name" is 4 chars. - this.collectionMock.Setup(x => x.ShiftIndices(0, 4)).Callback( - - // The '- 2' is also done in the used implementation. - (int _, int length) => requests[1].SourceLiteral.ShiftIndices(length - 2, requests[0].SourceLiteral)); - - var actual = this.subject.FormatMessage(Pattern, args); - this.collectionMock.Verify(x => x.ShiftIndices(0, 4), Times.Once); - this.libraryMock.VerifyAll(); - this.formatterMock1.VerifyAll(); - this.formatterMock2.VerifyAll(); - this.patternParserMock.VerifyAll(); + var args = new Dictionary { { "name", "Jeff" }, { "messages", 123} }; + + var actual = MessageFormatter.Format(Pattern, args); + Assert.Equal(Expected, actual); } @@ -134,9 +48,10 @@ public void FormatMessage() [Theory] [InlineData(@"Hello '{buddy}', how are you '{doing}'?", "Hello {buddy}, how are you {doing}?")] [InlineData(@"Hello ''{buddy}'', how are you '{doing}'?", @"Hello '{buddy}', how are you {doing}?")] + [InlineData(@"{''}", @"{'}")] public void UnescapeLiterals(string source, string expected) { - var actual = this.subject.UnescapeLiterals(new StringBuilder(source)); + var actual = MessageFormatter.UnescapeLiterals(new StringBuilder(source)); Assert.Equal(expected, actual); } @@ -146,38 +61,14 @@ public void UnescapeLiterals(string source, string expected) [Fact] public void VerifyFormatMessageThrowsWhenVariablesAreMissingAndTheFormatterRequiresItToExist() { - const string Pattern = "{name} has {messages, plural, 123}."; + const string Pattern = "{name}"; // Note the missing "name" variable. var args = new Dictionary { { "messages", 1 } }; - var requests = new[] - { - new FormatterRequest( - new Literal(0, 5, 1, 7, "name"), - "name", - null, - null), - new FormatterRequest( - new Literal(11, 33, 1, 7, "messages, plural, 123"), - "messages", - "plural", - " 123") - }; - - this.collectionMock.Setup(x => x.GetEnumerator()).Returns(() => requests.AsEnumerable().GetEnumerator()); - this.collectionMock.Setup(x => x.Count).Returns(requests.Length); - this.collectionMock.Setup(x => x[It.IsAny()]).Returns((int i) => requests[i]); - this.patternParserMock.Setup(x => x.Parse(It.IsAny())).Returns(this.collectionMock.Object); - this.formatterMock1.SetupGet(x => x.VariableMustExist).Returns(true); - this.libraryMock.Setup(x => x.GetFormatter(It.IsAny())).Returns(formatterMock1.Object); - - // First request, and "name" is 4 chars. - this.collectionMock.Setup(x => x.ShiftIndices(0, 4)).Callback( - - // The '- 2' is also done in the used implementation. - (int _, int length) => requests[1].SourceLiteral.ShiftIndices(length - 2, requests[0].SourceLiteral)); - var ex = Assert.Throws(() => this.subject.FormatMessage(Pattern, args)); + var subject = new MessageFormatter(); + + var ex = Assert.Throws(() => subject.FormatMessage(Pattern, args)); Assert.Equal("name", ex.MissingVariable); } @@ -187,31 +78,43 @@ public void VerifyFormatMessageThrowsWhenVariablesAreMissingAndTheFormatterRequi [Fact] public void VerifyFormatMessageAllowsNonExistentVariablesWhenFormatterAllowsIt() { - const string Pattern = "{name}"; + const string Pattern = "{name, fake}"; // Note the missing "name" variable. var args = new Dictionary (); - var requests = new[] - { - new FormatterRequest( - new Literal(0, 5, 1, 7, "name"), - "name", - null, - null), - }; - - this.collectionMock.Setup(x => x.GetEnumerator()).Returns(() => requests.AsEnumerable().GetEnumerator()); - this.collectionMock.Setup(x => x.Count).Returns(requests.Length); - this.collectionMock.Setup(x => x[It.IsAny()]).Returns((int i) => requests[i]); - this.patternParserMock.Setup(x => x.Parse(It.IsAny())).Returns(this.collectionMock.Object); - this.libraryMock.Setup(x => x.GetFormatter(It.IsAny())).Returns(formatterMock2.Object); - this.formatterMock2.SetupGet(x => x.VariableMustExist).Returns(false); - this.formatterMock2.Setup(x => x.Format(It.IsAny(), It.IsAny(), - It.IsAny>(), null, It.IsAny())).Returns("formatted"); + + var library = new FormatterLibrary(); + library.Add(new TestFormatter(variableMustExist: false, formatterName: "fake")); + var subject = new MessageFormatter(new PatternParser(), library, useCache: false); + + var actual = subject.FormatMessage(Pattern, args); - Assert.Equal("formatted",subject.FormatMessage(Pattern, args)); + Assert.Equal("formatted", actual); + } + + #endregion + + #region Fakes + + private class TestFormatter : IFormatter + { + private readonly string formatterName; + + public TestFormatter(bool variableMustExist, string formatterName) + { + this.VariableMustExist = variableMustExist; + this.formatterName = formatterName; + } - this.formatterMock2.VerifyAll(); + public bool VariableMustExist { get; } + + public bool CanFormat(FormatterRequest request) => request.FormatterName == this.formatterName; + + public string Format(string locale, FormatterRequest request, IDictionary args, object? value, + IMessageFormatter messageFormatter) + { + return "formatted"; + } } #endregion diff --git a/src/Jeffijoe.MessageFormat.Tests/MessageFormatterUsingRealParserTests.cs b/src/Jeffijoe.MessageFormat.Tests/MessageFormatterUsingRealParserTests.cs index a9c5e0d..c6c3dd1 100644 --- a/src/Jeffijoe.MessageFormat.Tests/MessageFormatterUsingRealParserTests.cs +++ b/src/Jeffijoe.MessageFormat.Tests/MessageFormatterUsingRealParserTests.cs @@ -10,8 +10,6 @@ using Jeffijoe.MessageFormat.Parsing; using Jeffijoe.MessageFormat.Tests.TestHelpers; -using Moq; - using Xunit; using Xunit.Abstractions; @@ -58,7 +56,7 @@ public MessageFormatterUsingRealParserTests(ITestOutputHelper outputHelper) /// The expected. /// [Theory] - [InlineData(@"Hi, I'm {name}, and it's still {name, plural, whatever + [InlineData(@"Hi, I'm {name}, and it's still {name, fake, whatever i do what i want @@ -73,18 +71,16 @@ whatchu gonna do? }, ok?", "Hi, I'm Jeff, and it's still Jeff, ok?")] public void FormatMessage_using_real_parser_and_library_mock(string source, string expected) { - var mockLibary = new Mock(); - var dummyFormatter = new Mock(); + var library = new FormatterLibrary(); + var dummyFormatter = new FakeFormatter(canFormat:true, formatResult: "Jeff"); + library.Add(dummyFormatter); var subject = new MessageFormatter( new PatternParser(new LiteralParser()), - mockLibary.Object, + library, false); var args = new Dictionary(); args.Add("name", "Jeff"); - dummyFormatter.Setup(x => x.Format("en", It.IsAny(), args, "Jeff", subject)) - .Returns("Jeff"); - mockLibary.Setup(x => x.GetFormatter(It.IsAny())).Returns(dummyFormatter.Object); // Warm up Benchmark.Start("Warm-up", this.outputHelper); diff --git a/src/Jeffijoe.MessageFormat.Tests/Parsing/PatternParserParseTests.cs b/src/Jeffijoe.MessageFormat.Tests/Parsing/PatternParserParseTests.cs index f25cbbb..ebebbbe 100644 --- a/src/Jeffijoe.MessageFormat.Tests/Parsing/PatternParserParseTests.cs +++ b/src/Jeffijoe.MessageFormat.Tests/Parsing/PatternParserParseTests.cs @@ -9,8 +9,6 @@ using Jeffijoe.MessageFormat.Parsing; using Jeffijoe.MessageFormat.Tests.TestHelpers; -using Moq; - using Xunit; using Xunit.Abstractions; @@ -72,13 +70,9 @@ public PatternParserParseTests(ITestOutputHelper outputHelper) "stuff {dawg, select, {name is '{'{name}'}'}}")] public void Parse(string source, string expectedKey, string expectedFormat, string expectedArgs) { - var literalParserMock = new Mock(); + var literalParser = FakeLiteralParser.Of(source); var sb = new StringBuilder(source); - literalParserMock.Setup(x => x.ParseLiterals(sb)); - literalParserMock.Setup(x => x.ParseLiterals(sb)) - .Returns(new[] { new Literal(0, source.Length, 1, 1, source) }); - - var subject = new PatternParser(literalParserMock.Object); + var subject = new PatternParser(literalParser); // Warm up (JIT) Benchmark.Start("Parsing formatter patterns (first time before JIT)", this.outputHelper); @@ -100,12 +94,20 @@ public void Parse(string source, string expectedKey, string expectedFormat, stri [Fact] public void Parse_exits_early_when_no_literals_have_been_found() { - var literalParserMock = new Mock(); - var subject = new PatternParser(literalParserMock.Object); - literalParserMock.Setup(x => x.ParseLiterals(It.IsAny())).Returns(new Literal[0]); + var subject = new PatternParser(); Assert.Empty(subject.Parse(new StringBuilder())); } - + + /// + /// The parse_throws_when_only_whitespace_is_present_in_section + /// + [Fact] + public void Parse_throws_when_only_whitespace_is_present_in_section() + { + var subject = new PatternParser(); + Assert.Throws(() => subject.Parse(new StringBuilder("{ }"))); + } + #endregion } } \ No newline at end of file diff --git a/src/Jeffijoe.MessageFormat.Tests/TestHelpers/FakeFormatter.cs b/src/Jeffijoe.MessageFormat.Tests/TestHelpers/FakeFormatter.cs new file mode 100644 index 0000000..be56434 --- /dev/null +++ b/src/Jeffijoe.MessageFormat.Tests/TestHelpers/FakeFormatter.cs @@ -0,0 +1,49 @@ +using System.Collections.Generic; +using Jeffijoe.MessageFormat.Formatting; + +namespace Jeffijoe.MessageFormat.Tests.TestHelpers +{ + /// + /// Fake formatter used for testing. + /// + internal class FakeFormatter : IFormatter + { + /// + /// What to return when is called. + /// + private readonly string formatResult; + + /// + /// Whether we should announce that we can format the input. + /// + private bool canFormat; + + /// + /// Initializes a new instance of the class. + /// + /// Whether to return true for . + /// The result to return. + public FakeFormatter(bool canFormat = false, string formatResult = "formatted") + { + this.canFormat = canFormat; + this.formatResult = formatResult; + } + + /// + public bool VariableMustExist => false; + + /// + public bool CanFormat(FormatterRequest request) => this.canFormat; + + /// + /// Sets the value of what returns. + /// + /// + public void SetCanFormat(bool value) => this.canFormat = value; + + /// + public string Format(string locale, FormatterRequest request, IDictionary args, object? value, + IMessageFormatter messageFormatter) => + formatResult; + } +} \ No newline at end of file diff --git a/src/Jeffijoe.MessageFormat.Tests/TestHelpers/FakeLiteralParser.cs b/src/Jeffijoe.MessageFormat.Tests/TestHelpers/FakeLiteralParser.cs new file mode 100644 index 0000000..255abae --- /dev/null +++ b/src/Jeffijoe.MessageFormat.Tests/TestHelpers/FakeLiteralParser.cs @@ -0,0 +1,41 @@ +using System.Collections.Generic; +using System.Text; +using Jeffijoe.MessageFormat.Parsing; + +namespace Jeffijoe.MessageFormat.Tests.TestHelpers +{ + /// + /// Fake literal parser. + /// + internal class FakeLiteralParser : ILiteralParser + { + /// + /// The literal to return. + /// + private readonly Literal literal; + + /// + /// Initializes a new instance of the class. + /// + /// + public FakeLiteralParser(Literal literal) + { + this.literal = literal; + } + + /// + public IEnumerable ParseLiterals(StringBuilder sb) + { + yield return literal; + } + + /// + /// Creates a fake literal parser that returns a single literal with + /// the specified inner text. + /// + /// + /// + public static ILiteralParser Of(string innerText) => + new FakeLiteralParser(new Literal(0, innerText.Length, 1, 1, innerText)); + } +} \ No newline at end of file diff --git a/src/Jeffijoe.MessageFormat.Tests/TestHelpers/FakeMessageFormatter.cs b/src/Jeffijoe.MessageFormat.Tests/TestHelpers/FakeMessageFormatter.cs new file mode 100644 index 0000000..e9490c2 --- /dev/null +++ b/src/Jeffijoe.MessageFormat.Tests/TestHelpers/FakeMessageFormatter.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; + +namespace Jeffijoe.MessageFormat.Tests.TestHelpers +{ + /// + /// Used for testing. Will just pass through the input pattern. + /// + internal class FakeMessageFormatter : IMessageFormatter + { + public string FormatMessage(string pattern, IDictionary argsMap) => pattern; + + public string FormatMessage(string pattern, object args) => pattern; + } +} diff --git a/src/Jeffijoe.MessageFormat.Tests/TestHelpers/TrackingPatternParser.cs b/src/Jeffijoe.MessageFormat.Tests/TestHelpers/TrackingPatternParser.cs new file mode 100644 index 0000000..8b1caaf --- /dev/null +++ b/src/Jeffijoe.MessageFormat.Tests/TestHelpers/TrackingPatternParser.cs @@ -0,0 +1,36 @@ +using System.Text; +using Jeffijoe.MessageFormat.Parsing; + +namespace Jeffijoe.MessageFormat.Tests.TestHelpers +{ + /// + /// Tracks the amount of times Parse is called. + /// + internal class TrackingPatternParser : IPatternParser + { + /// + /// The real parser. + /// + private readonly PatternParser parser; + + /// + /// Initializes a new instance of the class. + /// + public TrackingPatternParser() + { + parser = new PatternParser(); + } + + /// + /// The amount of times Parse was called. + /// + public int ParseCount { get; private set; } + + /// + public IFormatterRequestCollection Parse(StringBuilder source) + { + ParseCount++; + return parser.Parse(source); + } + } +} \ No newline at end of file diff --git a/src/Jeffijoe.MessageFormat/Formatting/BaseFormatter.cs b/src/Jeffijoe.MessageFormat/Formatting/BaseFormatter.cs index f039d88..e773c5c 100644 --- a/src/Jeffijoe.MessageFormat/Formatting/BaseFormatter.cs +++ b/src/Jeffijoe.MessageFormat/Formatting/BaseFormatter.cs @@ -266,15 +266,6 @@ protected internal IEnumerable ParseKeyedBlocks(FormatterRequest req foundWhitespaceAfterKey = false; continue; } - - if (braceBalance < 0) - { - throw new MalformedLiteralException( - "Expected '{', but found '}' - essentially this means there are more close braces than there are open braces.", - 0, - 0, - request.FormatterArguments); - } } // If we are inside a block, append to the block buffer diff --git a/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralFormatter.cs b/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralFormatter.cs index c9469fb..dbaa883 100644 --- a/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralFormatter.cs +++ b/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralFormatter.cs @@ -106,31 +106,6 @@ public string Format( return formatted; } - private static PluralContext CreatePluralContext(object? value, double offset) - { - if (offset == 0) - { - if (value is string v) - { - return new PluralContext(v); - } - - if (value is int i) - { - return new PluralContext(i); - } - - if (value is decimal d) - { - return new PluralContext(d); - } - - return new PluralContext(Convert.ToDouble(value)); - } - - return new PluralContext(Convert.ToDouble(value) - offset); - } - #endregion #region Methods @@ -238,6 +213,7 @@ internal string ReplaceNumberLiterals(string pluralized, double n) if (c == EscapeChar) { + // Append it anyway because the escae sb.Append(EscapeChar); if (i == pluralized.Length - 1) @@ -265,7 +241,7 @@ internal string ReplaceNumberLiterals(string pluralized, double n) continue; } - if (nextChar == '{' || nextChar == '}' || nextChar == '#') + if (nextChar is '{' or '}' or '#') { sb.Append(nextChar); insideEscapeSequence = true; @@ -334,6 +310,38 @@ private void AddStandardPluralizers() }); } + /// + /// Creates a for the specified value. + /// + /// + /// + /// + [ExcludeFromCodeCoverage] + private static PluralContext CreatePluralContext(object? value, double offset) + { + if (offset == 0) + { + if (value is string v) + { + return new PluralContext(v); + } + + if (value is int i) + { + return new PluralContext(i); + } + + if (value is decimal d) + { + return new PluralContext(d); + } + + return new PluralContext(Convert.ToDouble(value)); + } + + return new PluralContext(Convert.ToDouble(value) - offset); + } + #endregion } } \ No newline at end of file diff --git a/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralRulesMetadata.cs b/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralRulesMetadata.cs index 305870d..db8b44b 100644 --- a/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralRulesMetadata.cs +++ b/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralRulesMetadata.cs @@ -1,5 +1,6 @@ namespace Jeffijoe.MessageFormat.Formatting.Formatters { + [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] internal static partial class PluralRulesMetadata { public static partial bool TryGetRuleByLocale(string locale, out ContextPluralizer contextPluralizer); diff --git a/src/Jeffijoe.MessageFormat/Formatting/Formatters/SelectFormatter.cs b/src/Jeffijoe.MessageFormat/Formatting/Formatters/SelectFormatter.cs index 5f0c44d..ab9e9a6 100644 --- a/src/Jeffijoe.MessageFormat/Formatting/Formatters/SelectFormatter.cs +++ b/src/Jeffijoe.MessageFormat/Formatting/Formatters/SelectFormatter.cs @@ -19,6 +19,7 @@ public class SelectFormatter : BaseFormatter, IFormatter /// /// This formatter requires the input variable to exist. /// + [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] public bool VariableMustExist => true; #endregion diff --git a/src/Jeffijoe.MessageFormat/Formatting/ParsedArguments.cs b/src/Jeffijoe.MessageFormat/Formatting/ParsedArguments.cs index 1955d8d..0246762 100644 --- a/src/Jeffijoe.MessageFormat/Formatting/ParsedArguments.cs +++ b/src/Jeffijoe.MessageFormat/Formatting/ParsedArguments.cs @@ -27,16 +27,6 @@ public class ParsedArguments /// public ParsedArguments(IEnumerable keyedBlocks, IEnumerable extensions) { - if (keyedBlocks == null) - { - throw new ArgumentNullException("keyedBlocks"); - } - - if (extensions == null) - { - throw new ArgumentNullException("extensions"); - } - this.KeyedBlocks = keyedBlocks.ToList(); this.Extensions = extensions.ToList(); } diff --git a/src/Jeffijoe.MessageFormat/MessageFormatter.cs b/src/Jeffijoe.MessageFormat/MessageFormatter.cs index e02a5b1..9ec577b 100644 --- a/src/Jeffijoe.MessageFormat/MessageFormatter.cs +++ b/src/Jeffijoe.MessageFormat/MessageFormatter.cs @@ -8,7 +8,6 @@ using System.Collections.Concurrent; using System.Linq; using System.Text; - using Jeffijoe.MessageFormat.Formatting; using Jeffijoe.MessageFormat.Formatting.Formatters; using Jeffijoe.MessageFormat.Helpers; @@ -113,10 +112,7 @@ internal MessageFormatter( /// public IFormatterLibrary Formatters { - get - { - return this.library; - } + get { return this.library; } } /// @@ -249,7 +245,7 @@ public string FormatMessage(string pattern, IDictionary args) } // And we're done. - return this.UnescapeLiterals(sourceBuilder); + return MessageFormatter.UnescapeLiterals(sourceBuilder); } finally { @@ -287,7 +283,7 @@ public string FormatMessage(string pattern, object args) /// /// The . /// - protected internal string UnescapeLiterals(StringBuilder sourceBuilder) + internal static string UnescapeLiterals(StringBuilder sourceBuilder) { // If the block is empty, do nothing. if (sourceBuilder.Length == 0) @@ -296,8 +292,6 @@ protected internal string UnescapeLiterals(StringBuilder sourceBuilder) } const char EscapingChar = '\''; - const char OpenBrace = '{'; - const char CloseBrace = '}'; if (!sourceBuilder.Contains(EscapingChar)) { @@ -305,7 +299,6 @@ protected internal string UnescapeLiterals(StringBuilder sourceBuilder) } var length = sourceBuilder.Length; - var braceBalance = 0; var insideEscapeSequence = false; var dest = StringBuilderPool.Get(); @@ -318,54 +311,39 @@ protected internal string UnescapeLiterals(StringBuilder sourceBuilder) if (c == EscapingChar) { - if (braceBalance == 0) + if (i == length - 1) { - if (i == length - 1) - { - if (!insideEscapeSequence) - dest.Append(EscapingChar); - continue; - } - - var nextChar = sourceBuilder[i + 1]; - if (nextChar == EscapingChar) - { + if (!insideEscapeSequence) dest.Append(EscapingChar); - ++i; - continue; - } - - if (insideEscapeSequence) - { - insideEscapeSequence = false; - continue; - } - - if (nextChar == '{' || nextChar == '}' || nextChar == '#') - { - dest.Append(nextChar); - insideEscapeSequence = true; - ++i; - continue; - } + continue; + } + var nextChar = sourceBuilder[i + 1]; + if (nextChar == EscapingChar) + { dest.Append(EscapingChar); + ++i; + continue; + } + + if (insideEscapeSequence) + { + insideEscapeSequence = false; continue; } - } - else if (insideEscapeSequence) - { - // fall through to append - } - else if (c == OpenBrace) - { - braceBalance++; - } - else if (c == CloseBrace) - { - braceBalance--; - } + if (nextChar == '{' || nextChar == '}' || nextChar == '#') + { + dest.Append(nextChar); + insideEscapeSequence = true; + ++i; + continue; + } + + dest.Append(EscapingChar); + continue; + } + dest.Append(c); } @@ -409,6 +387,6 @@ private IFormatterRequestCollection ParseRequests(string pattern, StringBuilder return requests; } -#endregion + #endregion } } \ No newline at end of file diff --git a/src/Jeffijoe.MessageFormat/MessageFormatterException.cs b/src/Jeffijoe.MessageFormat/MessageFormatterException.cs index 1c666ef..8239338 100644 --- a/src/Jeffijoe.MessageFormat/MessageFormatterException.cs +++ b/src/Jeffijoe.MessageFormat/MessageFormatterException.cs @@ -25,21 +25,6 @@ public MessageFormatterException(string message) { } - /// - /// Initializes a new instance of the class. - /// - /// - /// The error message that explains the reason for the exception. - /// - /// - /// The exception that is the cause of the current exception, or a null reference (Nothing in - /// Visual Basic) if no inner exception is specified. - /// - public MessageFormatterException(string message, Exception innerException) - : base(message, innerException) - { - } - #endregion } } \ No newline at end of file diff --git a/src/Jeffijoe.MessageFormat/Parsing/PatternParser.cs b/src/Jeffijoe.MessageFormat/Parsing/PatternParser.cs index aede0a5..f4c74ef 100644 --- a/src/Jeffijoe.MessageFormat/Parsing/PatternParser.cs +++ b/src/Jeffijoe.MessageFormat/Parsing/PatternParser.cs @@ -27,6 +27,13 @@ public class PatternParser : IPatternParser #region Constructors and Destructors + /// + /// Initializes a new instance of the class. + /// + public PatternParser() : this(new LiteralParser()) + { + } + /// /// Initializes a new instance of the class. /// @@ -35,11 +42,6 @@ public class PatternParser : IPatternParser /// public PatternParser(ILiteralParser literalParser) { - if (literalParser == null) - { - throw new ArgumentNullException("literalParser"); - } - this.literalParser = literalParser; } diff --git a/src/Jeffijoe.MessageFormat/Parsing/UnbalancedBracesException.cs b/src/Jeffijoe.MessageFormat/Parsing/UnbalancedBracesException.cs index 3b84085..957849e 100644 --- a/src/Jeffijoe.MessageFormat/Parsing/UnbalancedBracesException.cs +++ b/src/Jeffijoe.MessageFormat/Parsing/UnbalancedBracesException.cs @@ -71,11 +71,6 @@ internal UnbalancedBracesException(int openBraceCount, int closeBraceCount) /// private static string BuildMessage(int openBraceCount, int closeBraceCount) { - if (openBraceCount == closeBraceCount) - { - throw new ArgumentException("Bracket counter was 0, which would indicate success."); - } - if (openBraceCount > closeBraceCount) { return "There are " + (openBraceCount - closeBraceCount) From 8f80ee9863809004df9a8b44e1ece65f922bf55a Mon Sep 17 00:00:00 2001 From: Jeff Hansen Date: Fri, 23 Dec 2022 11:18:35 +0100 Subject: [PATCH 54/98] Dotnet 7 support + allow IReadOnlyDictionary as args Closes #29 --- .github/workflows/ci.yml | 2 +- .../Jeffijoe.MessageFormat.MetadataGenerator.csproj | 2 +- .../Plural/Parsing/AST/LeftOperand.cs | 2 +- .../Plural/Parsing/AST/NumberOperand.cs | 2 +- .../Plural/Parsing/AST/RangeOperand.cs | 2 +- .../Plural/Parsing/AST/VariableOperand.cs | 2 +- .../Plural/Parsing/PluralParser.cs | 4 ++-- .../Jeffijoe.MessageFormat.Tests.csproj | 2 +- .../MessageFormatterCachingTests.cs | 1 - .../MessageFormatterTests.cs | 4 ++-- .../TestHelpers/FakeFormatter.cs | 8 ++++++-- .../TestHelpers/FakeMessageFormatter.cs | 4 ++-- .../TestHelpers/TrackingPatternParser.cs | 2 +- .../Formatting/Formatters/PluralFormatter.cs | 5 ++--- .../Formatting/Formatters/SelectFormatter.cs | 7 +++---- .../Formatting/Formatters/VariableFormatter.cs | 5 ++--- src/Jeffijoe.MessageFormat/Formatting/IFormatter.cs | 8 ++++---- .../Formatting/ParsedArguments.cs | 1 - src/Jeffijoe.MessageFormat/IMessageFormatter.cs | 2 +- .../Jeffijoe.MessageFormat.csproj | 2 +- src/Jeffijoe.MessageFormat/MessageFormatter.cs | 10 +++++----- 21 files changed, 38 insertions(+), 39 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3ca23e7..8dec1e0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,7 +25,7 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v1 with: - dotnet-version: 6.0.x + dotnet-version: 7.0.x - name: Install dependencies working-directory: ./src diff --git a/src/Jeffijoe.MessageFormat.MetadataGenerator/Jeffijoe.MessageFormat.MetadataGenerator.csproj b/src/Jeffijoe.MessageFormat.MetadataGenerator/Jeffijoe.MessageFormat.MetadataGenerator.csproj index b5e494d..c419da9 100644 --- a/src/Jeffijoe.MessageFormat.MetadataGenerator/Jeffijoe.MessageFormat.MetadataGenerator.csproj +++ b/src/Jeffijoe.MessageFormat.MetadataGenerator/Jeffijoe.MessageFormat.MetadataGenerator.csproj @@ -5,7 +5,7 @@ ../Jeffijoe.MessageFormat/MessageFormat.snk default enable - net5.0;net6.0;netstandard2.0;netstandard2.1 + net6.0;netstandard2.0;netstandard2.1 diff --git a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/LeftOperand.cs b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/LeftOperand.cs index 7229eb6..f116bc0 100644 --- a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/LeftOperand.cs +++ b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/LeftOperand.cs @@ -11,7 +11,7 @@ public ModuloOperand(OperandSymbol operandSymbol, int modValue) public OperandSymbol Operand { get; } public int ModValue { get; } - public override bool Equals(object obj) + public override bool Equals(object? obj) { if (obj is ModuloOperand op) return op.Operand == Operand && op.ModValue == ModValue; diff --git a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/NumberOperand.cs b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/NumberOperand.cs index 9f5bbdc..986a322 100644 --- a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/NumberOperand.cs +++ b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/NumberOperand.cs @@ -9,7 +9,7 @@ public NumberOperand(int number) public int Number { get; } - public override bool Equals(object obj) + public override bool Equals(object? obj) { if (obj is NumberOperand n) return n.Number == Number; diff --git a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/RangeOperand.cs b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/RangeOperand.cs index 150dadc..50ca1ec 100644 --- a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/RangeOperand.cs +++ b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/RangeOperand.cs @@ -11,7 +11,7 @@ public RangeOperand(int start, int end) public int Start { get; } public int End { get; } - public override bool Equals(object obj) + public override bool Equals(object? obj) { if (obj is RangeOperand n) return n.Start == Start && n.End == End; diff --git a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/VariableOperand.cs b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/VariableOperand.cs index 51e476e..ec3ee7c 100644 --- a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/VariableOperand.cs +++ b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/VariableOperand.cs @@ -9,7 +9,7 @@ public VariableOperand(OperandSymbol operand) public OperandSymbol Operand { get; } - public override bool Equals(object obj) + public override bool Equals(object? obj) { if (obj is VariableOperand op) return op.Operand == Operand; diff --git a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/PluralParser.cs b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/PluralParser.cs index d29084e..ddbed96 100644 --- a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/PluralParser.cs +++ b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/PluralParser.cs @@ -43,7 +43,7 @@ public IEnumerable Parse() private PluralRule? ParseSingleRule(XmlNode rule) { - var locales = rule.Attributes!["locales"].Value.Split(' '); + var locales = rule.Attributes!["locales"]!.Value.Split(' '); if (locales.All(l => _excludedLocales.Contains(l))) { @@ -55,7 +55,7 @@ public IEnumerable Parse() { if (condition.Name == "pluralRule") { - var count = condition.Attributes!["count"].Value; + var count = condition.Attributes!["count"]!.Value; // Ignore other, because other is basically everything else except for the conditions present if (count == "other") diff --git a/src/Jeffijoe.MessageFormat.Tests/Jeffijoe.MessageFormat.Tests.csproj b/src/Jeffijoe.MessageFormat.Tests/Jeffijoe.MessageFormat.Tests.csproj index c2405df..a715cb7 100644 --- a/src/Jeffijoe.MessageFormat.Tests/Jeffijoe.MessageFormat.Tests.csproj +++ b/src/Jeffijoe.MessageFormat.Tests/Jeffijoe.MessageFormat.Tests.csproj @@ -6,7 +6,7 @@ False 9 enable - net6.0 + net7.0 diff --git a/src/Jeffijoe.MessageFormat.Tests/MessageFormatterCachingTests.cs b/src/Jeffijoe.MessageFormat.Tests/MessageFormatterCachingTests.cs index 7b676b1..6215df9 100644 --- a/src/Jeffijoe.MessageFormat.Tests/MessageFormatterCachingTests.cs +++ b/src/Jeffijoe.MessageFormat.Tests/MessageFormatterCachingTests.cs @@ -10,7 +10,6 @@ using Jeffijoe.MessageFormat.Formatting; using Jeffijoe.MessageFormat.Helpers; -using Jeffijoe.MessageFormat.Parsing; using Jeffijoe.MessageFormat.Tests.TestHelpers; using Xunit; diff --git a/src/Jeffijoe.MessageFormat.Tests/MessageFormatterTests.cs b/src/Jeffijoe.MessageFormat.Tests/MessageFormatterTests.cs index edae297..0ff94e2 100644 --- a/src/Jeffijoe.MessageFormat.Tests/MessageFormatterTests.cs +++ b/src/Jeffijoe.MessageFormat.Tests/MessageFormatterTests.cs @@ -29,7 +29,7 @@ public void FormatMessage() { const string Pattern = "{name} has {messages, plural, other {# messages}}."; const string Expected = "Jeff has 123 messages."; - var args = new Dictionary { { "name", "Jeff" }, { "messages", 123} }; + IReadOnlyDictionary args = new Dictionary { { "name", "Jeff" }, { "messages", 123} }; var actual = MessageFormatter.Format(Pattern, args); @@ -110,7 +110,7 @@ public TestFormatter(bool variableMustExist, string formatterName) public bool CanFormat(FormatterRequest request) => request.FormatterName == this.formatterName; - public string Format(string locale, FormatterRequest request, IDictionary args, object? value, + public string Format(string locale, FormatterRequest request, IReadOnlyDictionary args, object? value, IMessageFormatter messageFormatter) { return "formatted"; diff --git a/src/Jeffijoe.MessageFormat.Tests/TestHelpers/FakeFormatter.cs b/src/Jeffijoe.MessageFormat.Tests/TestHelpers/FakeFormatter.cs index be56434..4dd5a81 100644 --- a/src/Jeffijoe.MessageFormat.Tests/TestHelpers/FakeFormatter.cs +++ b/src/Jeffijoe.MessageFormat.Tests/TestHelpers/FakeFormatter.cs @@ -28,7 +28,7 @@ public FakeFormatter(bool canFormat = false, string formatResult = "formatted") this.canFormat = canFormat; this.formatResult = formatResult; } - + /// public bool VariableMustExist => false; @@ -42,7 +42,11 @@ public FakeFormatter(bool canFormat = false, string formatResult = "formatted") public void SetCanFormat(bool value) => this.canFormat = value; /// - public string Format(string locale, FormatterRequest request, IDictionary args, object? value, + public string Format( + string locale, + FormatterRequest request, + IReadOnlyDictionary args, + object? value, IMessageFormatter messageFormatter) => formatResult; } diff --git a/src/Jeffijoe.MessageFormat.Tests/TestHelpers/FakeMessageFormatter.cs b/src/Jeffijoe.MessageFormat.Tests/TestHelpers/FakeMessageFormatter.cs index e9490c2..b0ee1af 100644 --- a/src/Jeffijoe.MessageFormat.Tests/TestHelpers/FakeMessageFormatter.cs +++ b/src/Jeffijoe.MessageFormat.Tests/TestHelpers/FakeMessageFormatter.cs @@ -7,8 +7,8 @@ namespace Jeffijoe.MessageFormat.Tests.TestHelpers /// internal class FakeMessageFormatter : IMessageFormatter { - public string FormatMessage(string pattern, IDictionary argsMap) => pattern; + public string FormatMessage(string pattern, IReadOnlyDictionary argsMap) => pattern; public string FormatMessage(string pattern, object args) => pattern; } -} +} \ No newline at end of file diff --git a/src/Jeffijoe.MessageFormat.Tests/TestHelpers/TrackingPatternParser.cs b/src/Jeffijoe.MessageFormat.Tests/TestHelpers/TrackingPatternParser.cs index 8b1caaf..c2d6be4 100644 --- a/src/Jeffijoe.MessageFormat.Tests/TestHelpers/TrackingPatternParser.cs +++ b/src/Jeffijoe.MessageFormat.Tests/TestHelpers/TrackingPatternParser.cs @@ -26,7 +26,7 @@ public TrackingPatternParser() /// public int ParseCount { get; private set; } - /// + /// public IFormatterRequestCollection Parse(StringBuilder source) { ParseCount++; diff --git a/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralFormatter.cs b/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralFormatter.cs index dbaa883..1bb88fc 100644 --- a/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralFormatter.cs +++ b/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralFormatter.cs @@ -84,10 +84,9 @@ public bool CanFormat(FormatterRequest request) /// /// The . /// - public string Format( - string locale, + public string Format(string locale, FormatterRequest request, - IDictionary args, + IReadOnlyDictionary args, object? value, IMessageFormatter messageFormatter) { diff --git a/src/Jeffijoe.MessageFormat/Formatting/Formatters/SelectFormatter.cs b/src/Jeffijoe.MessageFormat/Formatting/Formatters/SelectFormatter.cs index ab9e9a6..4f35b9b 100644 --- a/src/Jeffijoe.MessageFormat/Formatting/Formatters/SelectFormatter.cs +++ b/src/Jeffijoe.MessageFormat/Formatting/Formatters/SelectFormatter.cs @@ -19,7 +19,7 @@ public class SelectFormatter : BaseFormatter, IFormatter /// /// This formatter requires the input variable to exist. /// - [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + [ExcludeFromCodeCoverage] public bool VariableMustExist => true; #endregion @@ -59,10 +59,9 @@ public bool CanFormat(FormatterRequest request) /// 'other' option not found in pattern, and variable was not present in collection. [SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1126:PrefixCallsCorrectly", Justification = "Reviewed. Suppression is OK here.")] - public string Format( - string locale, + public string Format(string locale, FormatterRequest request, - IDictionary args, + IReadOnlyDictionary args, object? value, IMessageFormatter messageFormatter) { diff --git a/src/Jeffijoe.MessageFormat/Formatting/Formatters/VariableFormatter.cs b/src/Jeffijoe.MessageFormat/Formatting/Formatters/VariableFormatter.cs index 4fb9f6b..bf8231b 100644 --- a/src/Jeffijoe.MessageFormat/Formatting/Formatters/VariableFormatter.cs +++ b/src/Jeffijoe.MessageFormat/Formatting/Formatters/VariableFormatter.cs @@ -60,10 +60,9 @@ public bool CanFormat(FormatterRequest request) /// /// The . /// - public string Format( - string locale, + public string Format(string locale, FormatterRequest request, - IDictionary args, + IReadOnlyDictionary args, object? value, IMessageFormatter messageFormatter) { diff --git a/src/Jeffijoe.MessageFormat/Formatting/IFormatter.cs b/src/Jeffijoe.MessageFormat/Formatting/IFormatter.cs index 49b0d85..7ea2976 100644 --- a/src/Jeffijoe.MessageFormat/Formatting/IFormatter.cs +++ b/src/Jeffijoe.MessageFormat/Formatting/IFormatter.cs @@ -13,7 +13,7 @@ namespace Jeffijoe.MessageFormat.Formatting public interface IFormatter { #region Public Properties - + /// /// Each Formatter must declare whether or not an input variable is required to exist. /// Most of the time that is the case. @@ -21,7 +21,7 @@ public interface IFormatter bool VariableMustExist { get; } #endregion - + #region Public Methods and Operators /// @@ -50,9 +50,9 @@ public interface IFormatter /// The . /// string Format( - string locale, + string locale, FormatterRequest request, - IDictionary args, + IReadOnlyDictionary args, object? value, IMessageFormatter messageFormatter); diff --git a/src/Jeffijoe.MessageFormat/Formatting/ParsedArguments.cs b/src/Jeffijoe.MessageFormat/Formatting/ParsedArguments.cs index 0246762..2fa1b3a 100644 --- a/src/Jeffijoe.MessageFormat/Formatting/ParsedArguments.cs +++ b/src/Jeffijoe.MessageFormat/Formatting/ParsedArguments.cs @@ -3,7 +3,6 @@ // Author: Jeff Hansen // Copyright (C) Jeff Hansen 2014. All rights reserved. -using System; using System.Collections.Generic; using System.Linq; diff --git a/src/Jeffijoe.MessageFormat/IMessageFormatter.cs b/src/Jeffijoe.MessageFormat/IMessageFormatter.cs index a3947d1..db2fa5d 100644 --- a/src/Jeffijoe.MessageFormat/IMessageFormatter.cs +++ b/src/Jeffijoe.MessageFormat/IMessageFormatter.cs @@ -26,7 +26,7 @@ public interface IMessageFormatter /// /// The . /// - string FormatMessage(string pattern, IDictionary argsMap); + string FormatMessage(string pattern, IReadOnlyDictionary argsMap); /// /// Formats the message, and uses reflection to create a dictionary of property values from the specified object. diff --git a/src/Jeffijoe.MessageFormat/Jeffijoe.MessageFormat.csproj b/src/Jeffijoe.MessageFormat/Jeffijoe.MessageFormat.csproj index 1418745..af4e302 100644 --- a/src/Jeffijoe.MessageFormat/Jeffijoe.MessageFormat.csproj +++ b/src/Jeffijoe.MessageFormat/Jeffijoe.MessageFormat.csproj @@ -10,7 +10,7 @@ https://github.com/jeffijoe/messageformat.net latest enable - net5.0;net6.0;netstandard2.0;netstandard2.1 + net6.0;net7.0;netstandard2.0;netstandard2.1 diff --git a/src/Jeffijoe.MessageFormat/MessageFormatter.cs b/src/Jeffijoe.MessageFormat/MessageFormatter.cs index 9ec577b..1cd24d0 100644 --- a/src/Jeffijoe.MessageFormat/MessageFormatter.cs +++ b/src/Jeffijoe.MessageFormat/MessageFormatter.cs @@ -146,7 +146,7 @@ public IDictionary? Pluralizers /// Formats the specified pattern with the specified data. /// /// - /// This method calls + /// This method calls /// on a singleton instance using a lock. /// Do not use in a tight loop, as a lock is being used to ensure thread safety. /// @@ -159,7 +159,7 @@ public IDictionary? Pluralizers /// /// The formatted message. /// - public static string Format(string pattern, IDictionary data) + public static string Format(string pattern, IReadOnlyDictionary data) { lock (Lock) { @@ -203,7 +203,7 @@ public static string Format(string pattern, object data) /// /// The . /// - public string FormatMessage(string pattern, IDictionary args) + public string FormatMessage(string pattern, IReadOnlyDictionary args) { /* * We are assuming the formatters are ordered correctly @@ -231,7 +231,7 @@ public string FormatMessage(string pattern, IDictionary args) var result = formatter.Format(this.Locale, request, args, value, this); // First, we remove the literal from the source. - Literal sourceLiteral = request.SourceLiteral; + var sourceLiteral = request.SourceLiteral; // +1 because we want to include the last index. var length = (sourceLiteral.EndIndex - sourceLiteral.StartIndex) + 1; @@ -343,7 +343,7 @@ internal static string UnescapeLiterals(StringBuilder sourceBuilder) dest.Append(EscapingChar); continue; } - + dest.Append(c); } From 1a0bc2970d384df3ff7449f2a0b3b7054dd0acb8 Mon Sep 17 00:00:00 2001 From: Jeff Hansen Date: Sun, 1 Jan 2023 16:41:19 +0100 Subject: [PATCH 55/98] Fix `IDictionary` support Closes #31 --- .../MessageFormatterIssues.cs | 20 +++++++ .../IMessageFormatter.cs | 14 ----- .../MessageFormatter.cs | 19 +------ .../MessageFormatterExtensions.cs | 53 +++++++++++++++++++ 4 files changed, 74 insertions(+), 32 deletions(-) create mode 100644 src/Jeffijoe.MessageFormat/MessageFormatterExtensions.cs diff --git a/src/Jeffijoe.MessageFormat.Tests/MessageFormatterIssues.cs b/src/Jeffijoe.MessageFormat.Tests/MessageFormatterIssues.cs index f6da2d2..1b725f0 100644 --- a/src/Jeffijoe.MessageFormat.Tests/MessageFormatterIssues.cs +++ b/src/Jeffijoe.MessageFormat.Tests/MessageFormatterIssues.cs @@ -4,6 +4,7 @@ // Author: Jeff Hansen // Copyright (C) Jeff Hansen 2015. All rights reserved. +using System.Collections.Generic; using Xunit; namespace Jeffijoe.MessageFormat.Tests @@ -36,5 +37,24 @@ public void Issue27_WhiteSpace_in_identifiers_is_ignored() Assert.Equal("2 things", result); } + + [Fact] + public void Issue31_IDictionary_interface_support() + { + var subject = new MessageFormatter(locale: "en-US"); + + IDictionary idict = new Dictionary + { + ["string"] = "value" + }; + + IDictionary idictNullable = new Dictionary + { + ["string"] = "value" + }; + + Assert.Equal("value", subject.FormatMessage("{string}", idict)); + Assert.Equal("value", subject.FormatMessage("{string}", idictNullable!)); + } } } \ No newline at end of file diff --git a/src/Jeffijoe.MessageFormat/IMessageFormatter.cs b/src/Jeffijoe.MessageFormat/IMessageFormatter.cs index db2fa5d..2bd9eb9 100644 --- a/src/Jeffijoe.MessageFormat/IMessageFormatter.cs +++ b/src/Jeffijoe.MessageFormat/IMessageFormatter.cs @@ -28,20 +28,6 @@ public interface IMessageFormatter /// string FormatMessage(string pattern, IReadOnlyDictionary argsMap); - /// - /// Formats the message, and uses reflection to create a dictionary of property values from the specified object. - /// - /// - /// The pattern. - /// - /// - /// The arguments. - /// - /// - /// The . - /// - string FormatMessage(string pattern, object args); - #endregion } } \ No newline at end of file diff --git a/src/Jeffijoe.MessageFormat/MessageFormatter.cs b/src/Jeffijoe.MessageFormat/MessageFormatter.cs index 1cd24d0..fd6693d 100644 --- a/src/Jeffijoe.MessageFormat/MessageFormatter.cs +++ b/src/Jeffijoe.MessageFormat/MessageFormatter.cs @@ -171,7 +171,7 @@ public static string Format(string pattern, IReadOnlyDictionary /// Formats the specified pattern with the specified data. /// /// This method calls - /// + /// /// on a singleton instance using a lock. /// Do not use in a tight loop, as a lock is being used to ensure thread safety. /// @@ -253,23 +253,6 @@ public string FormatMessage(string pattern, IReadOnlyDictionary } } - /// - /// Formats the message, and uses reflection to create a dictionary of property values from the specified object. - /// - /// - /// The pattern. - /// - /// - /// The arguments. - /// - /// - /// The . - /// - public string FormatMessage(string pattern, object args) - { - return this.FormatMessage(pattern, args.ToDictionary()); - } - #endregion #region Methods diff --git a/src/Jeffijoe.MessageFormat/MessageFormatterExtensions.cs b/src/Jeffijoe.MessageFormat/MessageFormatterExtensions.cs new file mode 100644 index 0000000..1e44038 --- /dev/null +++ b/src/Jeffijoe.MessageFormat/MessageFormatterExtensions.cs @@ -0,0 +1,53 @@ +using System.Collections.Generic; +using Jeffijoe.MessageFormat.Helpers; + +namespace Jeffijoe.MessageFormat; + +/// +/// Extensions for . +/// +public static class MessageFormatterExtensions +{ + /// + /// Formats the message using the specified . + /// + /// + /// The formatter. + /// + /// + /// The pattern. + /// + /// + /// The arguments. + /// + /// + /// The . + /// + public static string FormatMessage( + this IMessageFormatter formatter, + string pattern, + IDictionary args) + { + return formatter.FormatMessage(pattern, (IReadOnlyDictionary)args); + } + + /// + /// Formats the message, and uses reflection to create a dictionary of property values from the specified object. + /// + /// + /// The formatter. + /// + /// + /// The pattern. + /// + /// + /// The arguments. + /// + /// + /// The . + /// + public static string FormatMessage(this IMessageFormatter formatter, string pattern, object args) + { + return formatter.FormatMessage(pattern, args.ToDictionary()); + } +} \ No newline at end of file From 4967ef4efdc3db40394cfe5d48b83eafac268b82 Mon Sep 17 00:00:00 2001 From: Jeff Hansen Date: Tue, 22 Aug 2023 12:59:20 -0400 Subject: [PATCH 56/98] Fix issue where newlines were being trimmed This was originally intentional, but apparently MessageFormat.js doesn't do this so I removed it. --- .../MessageFormatterIssues.cs | 16 ++++++++++++++++ .../Parsing/LiteralParserTests.cs | 10 ++++++++-- .../Parsing/LiteralParser.cs | 15 +++++++++------ .../Parsing/PatternParser.cs | 2 ++ 4 files changed, 35 insertions(+), 8 deletions(-) diff --git a/src/Jeffijoe.MessageFormat.Tests/MessageFormatterIssues.cs b/src/Jeffijoe.MessageFormat.Tests/MessageFormatterIssues.cs index 1b725f0..2214674 100644 --- a/src/Jeffijoe.MessageFormat.Tests/MessageFormatterIssues.cs +++ b/src/Jeffijoe.MessageFormat.Tests/MessageFormatterIssues.cs @@ -56,5 +56,21 @@ public void Issue31_IDictionary_interface_support() Assert.Equal("value", subject.FormatMessage("{string}", idict)); Assert.Equal("value", subject.FormatMessage("{string}", idictNullable!)); } + + [Fact] + public void Issue34_Newlines_are_stripped() + { + var subject = new MessageFormatter(locale: "en-US"); + + const string Expected = "Single text which will not change.\nSummary:\nAccepted\nData:\n-X\n-Y\n-Z"; + + var result = subject.FormatMessage( + "Single text which will not change.\nSummary:{acceptedData, select, NONE {} other {\nAccepted\nData:{acceptedData}}}", + new + { + acceptedData = "\n-X\n-Y\n-Z" + }); + Assert.Equal(Expected, result); + } } } \ No newline at end of file diff --git a/src/Jeffijoe.MessageFormat.Tests/Parsing/LiteralParserTests.cs b/src/Jeffijoe.MessageFormat.Tests/Parsing/LiteralParserTests.cs index 61fcb65..fbb44c5 100644 --- a/src/Jeffijoe.MessageFormat.Tests/Parsing/LiteralParserTests.cs +++ b/src/Jeffijoe.MessageFormat.Tests/Parsing/LiteralParserTests.cs @@ -122,11 +122,17 @@ public void ParseLiterals_unclosed_escape_sequence( [InlineData(@"{ sweet -}, right?", new[] { 0, 9 }, @"sweet")] +}, right?", new[] { 0, 9 }, @" +sweet + +")] [InlineData(@"{ '{sweet}' -}, right?", new[] { 0, 13 }, @"'{sweet}'")] +}, right?", new[] { 0, 13 }, @" +'{sweet}' + +")] public void ParseLiterals_position_and_inner_text(string source, int[] position, string expectedInnerText) { var sb = new StringBuilder(source); diff --git a/src/Jeffijoe.MessageFormat/Parsing/LiteralParser.cs b/src/Jeffijoe.MessageFormat/Parsing/LiteralParser.cs index 1bf0f51..a434090 100644 --- a/src/Jeffijoe.MessageFormat/Parsing/LiteralParser.cs +++ b/src/Jeffijoe.MessageFormat/Parsing/LiteralParser.cs @@ -52,20 +52,23 @@ public IEnumerable ParseLiterals(StringBuilder sb) for (var i = 0; i < sb.Length; i++) { var c = sb[i]; + + if (c == Cr) + { + continue; + } + if (c == Lf) { lineNumber++; columnNumber = 0; - continue; + } - - if (c == Cr) + else { - continue; + columnNumber++; } - columnNumber++; - if (c == EscapingChar) { if (i == sb.Length - 1) diff --git a/src/Jeffijoe.MessageFormat/Parsing/PatternParser.cs b/src/Jeffijoe.MessageFormat/Parsing/PatternParser.cs index f4c74ef..1aa0524 100644 --- a/src/Jeffijoe.MessageFormat/Parsing/PatternParser.cs +++ b/src/Jeffijoe.MessageFormat/Parsing/PatternParser.cs @@ -3,7 +3,9 @@ // Author: Jeff Hansen // Copyright (C) Jeff Hansen 2014. All rights reserved. +#if NET5_0_OR_GREATER using System; +#endif using System.Linq; using System.Text; using Jeffijoe.MessageFormat.Formatting; From 71c285ff6b0a409477c06f75676068178d44617e Mon Sep 17 00:00:00 2001 From: Jeff Hansen Date: Tue, 22 Aug 2023 13:02:35 -0400 Subject: [PATCH 57/98] Update packages --- .../Jeffijoe.MessageFormat.Tests.csproj | 12 ++++++------ .../Jeffijoe.MessageFormat.csproj | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Jeffijoe.MessageFormat.Tests/Jeffijoe.MessageFormat.Tests.csproj b/src/Jeffijoe.MessageFormat.Tests/Jeffijoe.MessageFormat.Tests.csproj index a715cb7..33d9aef 100644 --- a/src/Jeffijoe.MessageFormat.Tests/Jeffijoe.MessageFormat.Tests.csproj +++ b/src/Jeffijoe.MessageFormat.Tests/Jeffijoe.MessageFormat.Tests.csproj @@ -11,13 +11,13 @@ - - + + - - - - + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Jeffijoe.MessageFormat/Jeffijoe.MessageFormat.csproj b/src/Jeffijoe.MessageFormat/Jeffijoe.MessageFormat.csproj index af4e302..8302318 100644 --- a/src/Jeffijoe.MessageFormat/Jeffijoe.MessageFormat.csproj +++ b/src/Jeffijoe.MessageFormat/Jeffijoe.MessageFormat.csproj @@ -19,7 +19,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive From 4c2a79e82a63405cf852b04c4b907bc9cf75c076 Mon Sep 17 00:00:00 2001 From: Jeff Hansen Date: Sun, 1 Jan 2023 16:21:46 +0100 Subject: [PATCH 58/98] Add basic support for date. time and number formatting Closes #17 --- .github/workflows/ci.yml | 2 +- README.md | 56 +- .../Formatters/DateFormatterTests.cs | 70 ++ .../Formatters/NumberFormatterTests.cs | 150 +++++ .../Formatters/TimeFormatterTests.cs | 75 +++ .../TestHelpers/FakeMessageFormatter.cs | 2 + .../CustomValueFormatters.cs | 209 ++++++ .../Formatting/FormatterLibrary.cs | 3 + .../Formatters/BaseValueFormatter.cs | 55 ++ .../Formatting/Formatters/DateFormatter.cs | 43 ++ .../Formatting/Formatters/NumberFormatter.cs | 61 ++ .../Formatting/Formatters/SelectFormatter.cs | 2 +- .../Formatting/Formatters/TimeFormatter.cs | 43 ++ .../UnsupportedFormatStyleException.cs | 87 +++ .../IMessageFormatter.cs | 9 + .../MessageFormatter.cs | 616 +++++++++--------- 16 files changed, 1177 insertions(+), 306 deletions(-) create mode 100644 src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/DateFormatterTests.cs create mode 100644 src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/NumberFormatterTests.cs create mode 100644 src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/TimeFormatterTests.cs create mode 100644 src/Jeffijoe.MessageFormat/CustomValueFormatters.cs create mode 100644 src/Jeffijoe.MessageFormat/Formatting/Formatters/BaseValueFormatter.cs create mode 100644 src/Jeffijoe.MessageFormat/Formatting/Formatters/DateFormatter.cs create mode 100644 src/Jeffijoe.MessageFormat/Formatting/Formatters/NumberFormatter.cs create mode 100644 src/Jeffijoe.MessageFormat/Formatting/Formatters/TimeFormatter.cs create mode 100644 src/Jeffijoe.MessageFormat/Formatting/UnsupportedFormatStyleException.cs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8dec1e0..7f42f50 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,7 +23,7 @@ jobs: fetch-depth: 100 - name: Setup .NET - uses: actions/setup-dotnet@v1 + uses: actions/setup-dotnet@v3 with: dotnet-version: 7.0.x diff --git a/README.md b/README.md index 6f08c95..24c22a2 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ #### - better UI strings. -![Build](https://github.com/jeffijoe/messageformat.net/workflows/.NET%20Core/badge.svg) +[![Build & Test](https://github.com/jeffijoe/messageformat.net/actions/workflows/ci.yml/badge.svg)](https://github.com/jeffijoe/messageformat.net/actions/workflows/ci.yml) This is an implementation of the ICU Message Format in .NET. For official information about the format, go to: http://userguide.icu-project.org/formatparse/messages @@ -61,8 +61,6 @@ Install-Package MessageFormat * **It's fast.** Everything is hand-written; no parser-generators, *not even regular expressions*. * **It's portable.** The library is targeting **.NET Standard 2.0**. -* **It's compatible with other implementations.** I've been peeking a bit at the [MessageFormat.js][0] library to make sure - the results would be the same. * **It's (relatively) small**. For a .NET library, ~25kb is not a lot. * **It's very white-space tolerant.** You can structure your blocks so they are more readable - look at the example above. * **Nesting is supported.** You can nest your blocks as you please, there's no special structure required to do this, just ensure your braces match. @@ -74,7 +72,7 @@ Install-Package MessageFormat and if you are reusing the same instance of `MessageFormatter`, the formatter will cache the tokens of each pattern (nested, too), so it won't have to spend CPU time to parse out literals every time. I benchmarked it, and on my monster machine, it didn't make much of a difference (10000 iterations). -* **Built-in pluralization formatters**. Generated from the [CLDR pluralization rule data](http://cldr.unicode.org/index/downloads). +* **Built-in pluralization formatters**. Generated from the [CLDR pluralization rule data][plural-cldr]. ## Performance @@ -85,14 +83,61 @@ and about 3 seconds (3236ms) without it. **These results are with a debug build, ## Supported formats -Basically, it should be able to do anything that [MessageFormat.js][0] can do. +MessageFormat.NET supports the most commonly used formats: * Select Format: `{gender, select, male{He likes} female{She likes} other{They like}} cheeseburgers` * Plural Format: `There {msgCount, plural, zero {are no unread messages} one {is 1 unread message} other{are # unread messages}}.` (where `#` is the actual number, with the offset (if any) subtracted). * Simple variable replacement: `Your name is {name}` +* Numbers: `Your age is {age, number}` +* Dates: `You were born {birthday, date}` +* Time: `The time is {now, time}` + +You can also specify a _predefined style_, for example `{birthday, date, short}`. The supported predefined styles are: + +* For the `number` format: `integer`, `currency`, `percent` +* For the `date` format: `short`, `full` +* For the `time` format: `short`, `medium` + +These are currently mapped to the built-in .NET format specifiers. This package does not ship with +any locale data beyond the pluralizers that are generated based on [CLDR data][plural-cldr], so if you wish +to provide your own localized formatting, read the section below. + +## Customize formatting + +If you wish to control exactly how `number`, `date` and `time` are formatted, you can either: +* Derive `CustomValueFormatter` and override the format methods +* Instantiate a `CustomValueFormatters` and assign a lambda to the desired properties +Then pass it in as the `customValueFormatter` parameter to `new MessageFormatter`. + +**Example**: A custom formatter that allows the use of .NET's formatting tokens. This is for illustration purposes only and +is not recommended for use in real apps. + +```csharp +// This is using the lambda-based approach. +var custom = new CustomValueFormatters +{ + // The formatter must set the `formatted` out parameter and return `true` + // If the formatter returns `false`, the built-in formatting is used. + Number = (CultureInfo _, object? value, string? style, out string? formatted) => + { + formatted = string.Format($"{{0:{style}}}", value); + return true; + } +}; + +// Create a MessageFormatter with the custom value formatter. +var formatter = new MessageFormatter(locale: "en-US", customValueFormatter: custom); + +// Format a message. +var message = formatter.FormatMessage("{value, number, $0.0}", new { value = 23 }); +// "$23.0" +``` ## Adding your own pluralizer functions +> Since MessageFormat 5.0, pluralizers based on the [official CLDR data][plural-cldr] ship +> with the package, so this is no longer needed. + Same thing as with [MessageFormat.js][0], you can add your own pluralizer function. The `Pluralizers` property is a `IDictionary`, so you can remove the built-in ones if you want. @@ -159,3 +204,4 @@ You can find me on Twitter: [@jeffijoe][1]. [0]: https://github.com/SlexAxton/messageformat.js [1]: https://twitter.com/jeffijoe + [plural-cldr]: https://cldr.unicode.org/index/downloads \ No newline at end of file diff --git a/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/DateFormatterTests.cs b/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/DateFormatterTests.cs new file mode 100644 index 0000000..d85242f --- /dev/null +++ b/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/DateFormatterTests.cs @@ -0,0 +1,70 @@ +using System; +using System.Globalization; +using Jeffijoe.MessageFormat.Formatting; +using Xunit; + +namespace Jeffijoe.MessageFormat.Tests.Formatting.Formatters +{ + public class DateFormatterTests + { + [Theory] + [InlineData("en-US", "1994-09-06T15:00:00Z", "9/6/1994")] + [InlineData("da-DK", "1994-09-06T15:00:00Z", "06.09.1994")] + public void DateFormatter_Short(string locale, string dateStr, string expected) + { + var mf = new MessageFormatter(locale: locale); + var actual = mf.FormatMessage("{value, date}", new + { + value = DateTimeOffset.Parse(dateStr) + }); + + Assert.Equal(expected, actual); + } + + [Theory] + [InlineData("en-US", "1994-09-06T15:00:00Z", "Tuesday, September 6, 1994")] + [InlineData("da-DK", "1994-09-06T15:00:00Z", "tirsdag den 6. september 1994")] + public void DateFormatter_Full(string locale, string dateStr, string expected) + { + var mf = new MessageFormatter(locale: locale); + var actual = mf.FormatMessage("{value, date, full}", new + { + value = DateTimeOffset.Parse(dateStr) + }); + + Assert.Equal(expected, actual); + } + + [Fact] + public void DateFormatter_UnsupportedStyle() + { + var mf = new MessageFormatter(); + Assert.Throws( + () => mf.FormatMessage("{value, date, long}", new + { + value = DateTimeOffset.UtcNow + })); + } + + [Fact] + public void DateFormatter_Custom() + { + var formatter = new CustomValueFormatters + { + Date = (CultureInfo _, object? value, string? _, out string? formatted) => + { + // This is just a test, you probably shouldn't be doing this in real workloads. + formatted = $"{value:MMMM d 'in the year' yyyy}"; + return true; + } + }; + var mf = new MessageFormatter(locale: "en-US", customValueFormatter: formatter); + var actual = mf.FormatMessage("{value, date, long}", new + { + value = DateTimeOffset.Parse("1994-09-06T15:00:00Z") + }); + + Assert.Equal("September 6 in the year 1994", actual); + } + } +} \ No newline at end of file diff --git a/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/NumberFormatterTests.cs b/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/NumberFormatterTests.cs new file mode 100644 index 0000000..c689b7e --- /dev/null +++ b/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/NumberFormatterTests.cs @@ -0,0 +1,150 @@ +using System.Globalization; +using Jeffijoe.MessageFormat.Formatting; +using Xunit; + +namespace Jeffijoe.MessageFormat.Tests.Formatting.Formatters +{ + public class NumberFormatterTests + { + [Theory] + [InlineData(69, "69")] + [InlineData(69.420, "69.42")] + [InlineData(123_456.789, "123,456.789")] + [InlineData(1234567.1234567, "1,234,567.123")] + public void NumberFormatter_Default(decimal number, string expected) + { + var mf = new MessageFormatter(locale: "en-US"); + // NOTE: The whitespace at the end is on purpose to cover whitespace tolerance in parsing. + var actual = mf.FormatMessage("{value, number }", new + { + value = number + }); + + Assert.Equal(expected, actual); + } + + [Theory] + [InlineData(69, "69.0000")] + [InlineData(69.420, "69.4200")] + public void NumberFormatter_Decimal_CustomFormat(decimal number, string expected) + { + var formatters = new CustomValueFormatters + { + Number = (CultureInfo _, object? value, string? style, out string? formatted) => + { + formatted = string.Format($"{{0:{style}}}", value); + return true; + } + }; + var mf = new MessageFormatter(locale: "en-US", customValueFormatter: formatters); + + var actual = mf.FormatMessage("{value, number, 0.0000}", new + { + value = number + }); + + Assert.Equal(expected, actual); + } + + [Theory] + [InlineData(0.2, "20%")] + [InlineData(1.2, "120%")] + [InlineData(1234567.1234567, "123,456,712%")] + public void NumberFormatter_Percent(decimal number, string expected) + { + var mf = new MessageFormatter(locale: "en-US"); + + // NOTE: The inconsistent whitespace in the pattern is to cover whitespace tolerance in parsing. + var actual = mf.FormatMessage("{value, number,percent}", new + { + value = number + }); + + Assert.Equal(expected, actual); + } + + [Theory] + [InlineData(0.2, "0")] + [InlineData(1.2, "1")] + [InlineData(2.7, "3")] + [InlineData("2.7", "3")] + [InlineData("a string", "a string")] + [InlineData(true, "True")] + public void NumberFormatter_Integer(object? value, string expected) + { + var mf = new MessageFormatter(locale: "en-US"); + var actual = mf.FormatMessage("{value, number, integer}", new + { + value + }); + + Assert.Equal(expected, actual); + } + + [Theory] + [InlineData("en-US", 20, "$20.00")] + [InlineData("en-US", 99.99, "$99.99")] + [InlineData("da-DK", 99.99, "99,99 kr.")] + public void NumberFormatter_Currency(string locale, decimal number, string expected) + { + var mf = new MessageFormatter(locale: locale); + + // NOTE: The inconsistent whitespace in the pattern is to cover whitespace tolerance in parsing. + var actual = mf.FormatMessage("{value, number, currency }", new + { + value = number + }); + + Assert.Equal(expected, actual); + } + + [Fact] + public void NumberFormatter_ThrowsIfStyleIsNotSupported() + { + const decimal Number = 12.34m; + var mf = new MessageFormatter(locale: "en-US"); + var ex = Assert.Throws(() => + mf.FormatMessage($"{{value, number, wow}}", + new + { + value = Number + })); + Assert.Equal("value", ex.Variable); + Assert.Equal("number", ex.Format); + Assert.Equal("wow", ex.Style); + } + + [Fact] + public void NumberFormatter_BadInput_FallsBackToRegularFormat() + { + var mf = new MessageFormatter(locale: "en-US"); + + { + var actual = mf.FormatMessage($"{{value, number, currency}}", new + { + value = "a lot of money" + }); + + Assert.Equal("a lot of money", actual); + } + + { + var actual = mf.FormatMessage($"{{value, number, integer}}", new + { + value = "a lot of money" + }); + + Assert.Equal("a lot of money", actual); + } + + { + var actual = mf.FormatMessage($"{{value, number, integer}}", new + { + value = true + }); + + Assert.Equal("True", actual); + } + } + } +} \ No newline at end of file diff --git a/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/TimeFormatterTests.cs b/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/TimeFormatterTests.cs new file mode 100644 index 0000000..acddc92 --- /dev/null +++ b/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/TimeFormatterTests.cs @@ -0,0 +1,75 @@ +using System; +using System.Globalization; +using Jeffijoe.MessageFormat.Formatting; +using Xunit; + +namespace Jeffijoe.MessageFormat.Tests.Formatting.Formatters +{ + public class TimeFormatterTests + { + [Theory] + [InlineData("en-US", "1994-09-06T15:01:23Z", "3:01 PM")] + [InlineData("da-DK", "1994-09-06T15:01:23Z", "15.01")] + public void TimeFormatter_Short(string locale, string dateStr, string expected) + { + var mf = new MessageFormatter(locale: locale); + var actual = mf.FormatMessage("{value, time, short}", new + { + value = DateTimeOffset.Parse(dateStr) + }); + + // Replacing all whitespace due to a difference in formatting on macOS vs Linux. + expected = expected.Replace(" ", ""); + actual = actual.Replace(" ", ""); + Assert.Equal(expected, actual); + } + + [Theory] + [InlineData("en-US", "1994-09-06T15:01:23Z", "3:01:23 PM")] + [InlineData("da-DK", "1994-09-06T15:01:23Z", "15.01.23")] + public void TimeFormatter_Default(string locale, string dateStr, string expected) + { + var mf = new MessageFormatter(locale: locale); + var actual = mf.FormatMessage("{value, time}", new + { + value = DateTimeOffset.Parse(dateStr) + }); + + // Replacing all whitespace due to a difference in formatting on macOS vs Linux. + expected = expected.Replace(" ", ""); + actual = actual.Replace(" ", ""); + Assert.Equal(expected, actual); + } + + [Fact] + public void TimeFormatter_UnsupportedStyle() + { + var mf = new MessageFormatter(); + Assert.Throws( + () => mf.FormatMessage("{value, time, lol}", new + { + value = DateTimeOffset.UtcNow + })); + } + + [Fact] + public void TimeFormatter_Custom() + { + var formatter = new CustomValueFormatters + { + Time = (CultureInfo _, object? value, string? _, out string? formatted) => + { + formatted = $"{value:hmm} nice"; + return true; + } + }; + var mf = new MessageFormatter(locale: "en-US", customValueFormatter: formatter); + var actual = mf.FormatMessage("{value, time, long}", new + { + value = DateTimeOffset.Parse("1994-09-06T16:20:09Z") + }); + + Assert.Equal("420 nice", actual); + } + } +} \ No newline at end of file diff --git a/src/Jeffijoe.MessageFormat.Tests/TestHelpers/FakeMessageFormatter.cs b/src/Jeffijoe.MessageFormat.Tests/TestHelpers/FakeMessageFormatter.cs index b0ee1af..00a573d 100644 --- a/src/Jeffijoe.MessageFormat.Tests/TestHelpers/FakeMessageFormatter.cs +++ b/src/Jeffijoe.MessageFormat.Tests/TestHelpers/FakeMessageFormatter.cs @@ -7,6 +7,8 @@ namespace Jeffijoe.MessageFormat.Tests.TestHelpers /// internal class FakeMessageFormatter : IMessageFormatter { + public CustomValueFormatter? CustomValueFormatter { get; set; } + public string FormatMessage(string pattern, IReadOnlyDictionary argsMap) => pattern; public string FormatMessage(string pattern, object args) => pattern; diff --git a/src/Jeffijoe.MessageFormat/CustomValueFormatters.cs b/src/Jeffijoe.MessageFormat/CustomValueFormatters.cs new file mode 100644 index 0000000..a2bd68f --- /dev/null +++ b/src/Jeffijoe.MessageFormat/CustomValueFormatters.cs @@ -0,0 +1,209 @@ +using System.Diagnostics.CodeAnalysis; +using System.Globalization; + +namespace Jeffijoe.MessageFormat; + +/// +/// Attempts to format a date. +/// +/// +/// The culture. +/// +/// +/// The value to format. +/// +/// +/// The requested style, if any. +/// +/// +/// Output for setting the formatted result. +/// +/// +/// true if able to format the value; false otherwise. +/// +public delegate bool TryFormatDate( + CultureInfo culture, + object? value, + string? style, + out string? formatted); + +/// +/// Attempts to format a time. +/// +/// +/// The culture. +/// +/// +/// The value to format. +/// +/// +/// The requested style, if any. +/// +/// +/// Output for setting the formatted result. +/// +/// +/// true if able to format the value; false otherwise. +/// +public delegate bool TryFormatTime( + CultureInfo culture, + object? value, + string? style, + out string? formatted); + +/// +/// Attempts to format a number. +/// +/// +/// The culture. +/// +/// +/// The value to format. +/// +/// +/// The requested style, if any. +/// +/// +/// Output for setting the formatted result. +/// +/// +/// true if able to format the value; false otherwise. +/// +public delegate bool TryFormatNumber( + CultureInfo culture, + object? value, + string? style, + out string? formatted); + +/// +/// Base class that can be extended to provide custom formatting +/// for values. +/// +public abstract class CustomValueFormatter +{ + /// + /// Attempts to format a date. + /// + /// + /// The culture. + /// + /// + /// The value to format. + /// + /// + /// The requested style, if any. + /// + /// + /// Output for setting the formatted result. + /// + /// + /// true if able to format the value; false otherwise. + /// + [ExcludeFromCodeCoverage] + public virtual bool TryFormatDate( + CultureInfo culture, + object? value, + string? style, + out string? formatted) + { + formatted = null; + return false; + } + + /// + /// Attempts to format a time. + /// + /// + /// The culture. + /// + /// + /// The value to format. + /// + /// + /// The requested style, if any. + /// + /// + /// Output for setting the formatted result. + /// + /// + /// true if able to format the value; false otherwise. + /// + [ExcludeFromCodeCoverage] + public virtual bool TryFormatTime( + CultureInfo culture, + object? value, + string? style, + out string? formatted) + { + formatted = null; + return false; + } + + /// + /// Attempts to format a number. + /// + /// + /// The culture. + /// + /// + /// The value to format. + /// + /// + /// The requested style, if any. + /// + /// + /// Output for setting the formatted result. + /// + /// + /// true if able to format the value; false otherwise. + /// + [ExcludeFromCodeCoverage] + public virtual bool TryFormatNumber( + CultureInfo culture, + object? value, + string? style, + out string? formatted) + { + formatted = null; + return false; + } +} + +/// +/// Delegates the formatting calls to the configured function properties. +/// +public sealed class CustomValueFormatters : CustomValueFormatter +{ + /// + /// Formatter for dates. + /// + public TryFormatDate? Date { get; set; } + + /// + /// Formatter for times. + /// + public TryFormatDate? Time { get; set; } + + /// + /// Formatter for numbers. + /// + public TryFormatNumber? Number { get; set; } + + /// + public override bool TryFormatDate(CultureInfo culture, object? value, string? style, + out string? formatted) => + this.Date?.Invoke(culture, value, style, out formatted) ?? + base.TryFormatDate(culture, value, style, out formatted); + + /// + public override bool TryFormatTime(CultureInfo culture, object? value, string? style, + out string? formatted) => + this.Time?.Invoke(culture, value, style, out formatted) ?? + base.TryFormatTime(culture, value, style, out formatted); + + /// + public override bool TryFormatNumber(CultureInfo culture, object? value, string? style, + out string? formatted) => + this.Number?.Invoke(culture, value, style, out formatted) ?? + base.TryFormatNumber(culture, value, style, out formatted); +} \ No newline at end of file diff --git a/src/Jeffijoe.MessageFormat/Formatting/FormatterLibrary.cs b/src/Jeffijoe.MessageFormat/Formatting/FormatterLibrary.cs index 699815e..b0eb9b8 100644 --- a/src/Jeffijoe.MessageFormat/Formatting/FormatterLibrary.cs +++ b/src/Jeffijoe.MessageFormat/Formatting/FormatterLibrary.cs @@ -23,6 +23,9 @@ public FormatterLibrary() this.Add(new VariableFormatter()); this.Add(new SelectFormatter()); this.Add(new PluralFormatter()); + this.Add(new NumberFormatter()); + this.Add(new DateFormatter()); + this.Add(new TimeFormatter()); } #endregion diff --git a/src/Jeffijoe.MessageFormat/Formatting/Formatters/BaseValueFormatter.cs b/src/Jeffijoe.MessageFormat/Formatting/Formatters/BaseValueFormatter.cs new file mode 100644 index 0000000..221e223 --- /dev/null +++ b/src/Jeffijoe.MessageFormat/Formatting/Formatters/BaseValueFormatter.cs @@ -0,0 +1,55 @@ +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; + +namespace Jeffijoe.MessageFormat.Formatting.Formatters; + +/// +/// Base formatter for values such as numbers, dates, times, etc. +/// +public abstract class BaseValueFormatter : BaseFormatter, IFormatter +{ + /// + /// Initializes a new instance of the class. + /// + protected BaseValueFormatter() + { + } + + /// + [ExcludeFromCodeCoverage] + public bool VariableMustExist => true; + + /// + public abstract bool CanFormat(FormatterRequest request); + + /// + /// Formats the value using the given style. + /// + /// + /// + /// + /// + /// + /// + protected abstract string FormatValue(CultureInfo culture, CustomValueFormatter? customValueFormatter, + string variable, string style, object? value); + + /// + public string Format( + string locale, + FormatterRequest request, + IReadOnlyDictionary args, + object? value, + IMessageFormatter messageFormatter) + { + var formatterArgs = request.FormatterArguments!; + var culture = CultureInfo.GetCultureInfo(locale); + return FormatValue( + culture: culture, + customValueFormatter: messageFormatter.CustomValueFormatter, + variable: request.Variable, + style: formatterArgs, + value: value); + } +} \ No newline at end of file diff --git a/src/Jeffijoe.MessageFormat/Formatting/Formatters/DateFormatter.cs b/src/Jeffijoe.MessageFormat/Formatting/Formatters/DateFormatter.cs new file mode 100644 index 0000000..b053de7 --- /dev/null +++ b/src/Jeffijoe.MessageFormat/Formatting/Formatters/DateFormatter.cs @@ -0,0 +1,43 @@ +using System.Globalization; + +namespace Jeffijoe.MessageFormat.Formatting.Formatters; + +/// +/// Formatter for dates. +/// +public class DateFormatter : BaseValueFormatter, IFormatter +{ + /// + /// Name of this formatter. + /// + private const string FormatterName = "date"; + + /// + public override bool CanFormat(FormatterRequest request) => + request.FormatterName == FormatterName; + + /// + protected override string FormatValue( + CultureInfo culture, + CustomValueFormatter? customValueFormatter, + string variable, + string style, + object? value) + { + if (customValueFormatter?.TryFormatDate(culture, value, style, out var formatted) == true) + { + // When the formatter returns `true`, the string will be set. + return formatted!; + } + + return style switch + { + "" or "short" => string.Format(culture, "{0:d}", value), + "full" => string.Format(culture, "{0:D}", value), + _ => throw new UnsupportedFormatStyleException( + variable: variable, + format: FormatterName, + style: style) + }; + } +} \ No newline at end of file diff --git a/src/Jeffijoe.MessageFormat/Formatting/Formatters/NumberFormatter.cs b/src/Jeffijoe.MessageFormat/Formatting/Formatters/NumberFormatter.cs new file mode 100644 index 0000000..2c3c4d3 --- /dev/null +++ b/src/Jeffijoe.MessageFormat/Formatting/Formatters/NumberFormatter.cs @@ -0,0 +1,61 @@ +using System; +using System.Globalization; + +namespace Jeffijoe.MessageFormat.Formatting.Formatters; + +/// +/// Formatter for numbers. +/// +public class NumberFormatter : BaseValueFormatter, IFormatter +{ + /// + /// Name of this formatter. + /// + private const string FormatterName = "number"; + + /// + public override bool CanFormat(FormatterRequest request) => + request.FormatterName == FormatterName; + + /// + protected override string FormatValue( + CultureInfo culture, + CustomValueFormatter? customValueFormatter, + string variable, + string style, + object? value) + { + if (customValueFormatter?.TryFormatNumber(culture, value, style, out var formatted) == true) + { + // When the formatter returns `true`, the string will be set. + return formatted!; + } + + return style switch + { + "" => string.Format(culture, "{0:#,##0.###}", value), + "integer" => FormatInteger(culture, value), + "currency" => string.Format(culture, "{0:C}", value), + "percent" => string.Format(culture, "{0:P0}", value), + _ => throw new UnsupportedFormatStyleException( + variable: variable, + format: FormatterName, + style: style) + }; + } + + /// + /// Attempts to format as an integer by first converting the value to + /// an integer. Otherwise prints the value as-is. + /// + /// + /// + /// + private static string FormatInteger(IFormatProvider cultureInfo, object? value) => + value switch + { + decimal or float or double => string.Format(cultureInfo, "{0}", Convert.ToInt64(value)), + string s => decimal.TryParse(s, out var parsed) ? FormatInteger(cultureInfo, parsed) : s, + _ => string.Format(cultureInfo, "{0}", value) + }; +} \ No newline at end of file diff --git a/src/Jeffijoe.MessageFormat/Formatting/Formatters/SelectFormatter.cs b/src/Jeffijoe.MessageFormat/Formatting/Formatters/SelectFormatter.cs index 4f35b9b..24d2f15 100644 --- a/src/Jeffijoe.MessageFormat/Formatting/Formatters/SelectFormatter.cs +++ b/src/Jeffijoe.MessageFormat/Formatting/Formatters/SelectFormatter.cs @@ -46,7 +46,7 @@ public bool CanFormat(FormatterRequest request) /// Using the specified parameters and arguments, a formatted string shall be returned. /// The is being provided as well, to enable /// nested formatting. This is only called if returns true. - /// The argswill always contain the . + /// The args will always contain the . /// /// The locale being used. It is up to the formatter what they do with this information. /// The parameters. diff --git a/src/Jeffijoe.MessageFormat/Formatting/Formatters/TimeFormatter.cs b/src/Jeffijoe.MessageFormat/Formatting/Formatters/TimeFormatter.cs new file mode 100644 index 0000000..be6aa8a --- /dev/null +++ b/src/Jeffijoe.MessageFormat/Formatting/Formatters/TimeFormatter.cs @@ -0,0 +1,43 @@ +using System.Globalization; + +namespace Jeffijoe.MessageFormat.Formatting.Formatters; + +/// +/// Formatter for times. +/// +public class TimeFormatter : BaseValueFormatter, IFormatter +{ + /// + /// Name of this formatter. + /// + private const string FormatterName = "time"; + + /// + public override bool CanFormat(FormatterRequest request) => + request.FormatterName == FormatterName; + + /// + protected override string FormatValue( + CultureInfo culture, + CustomValueFormatter? customValueFormatter, + string variable, + string style, + object? value) + { + if (customValueFormatter?.TryFormatTime(culture, value, style, out var formatted) == true) + { + // When the formatter returns `true`, the string will be set. + return formatted!; + } + + return style switch + { + "" or "medium" => string.Format(culture, "{0:T}", value), + "short" => string.Format(culture, "{0:t}", value), + _ => throw new UnsupportedFormatStyleException( + variable: variable, + format: FormatterName, + style: style) + }; + } +} \ No newline at end of file diff --git a/src/Jeffijoe.MessageFormat/Formatting/UnsupportedFormatStyleException.cs b/src/Jeffijoe.MessageFormat/Formatting/UnsupportedFormatStyleException.cs new file mode 100644 index 0000000..1e0a0ac --- /dev/null +++ b/src/Jeffijoe.MessageFormat/Formatting/UnsupportedFormatStyleException.cs @@ -0,0 +1,87 @@ +namespace Jeffijoe.MessageFormat.Formatting; + +/// +/// Thrown when formatter is unable to apply the given style. +/// +public class UnsupportedFormatStyleException : MessageFormatterException +{ + #region Constructors and Destructors + + /// + /// Initializes a new instance of the class. + /// + /// + /// The variable. + /// + /// + /// The format. + /// + /// + /// The style that was not supported. + /// + public UnsupportedFormatStyleException( + string variable, + string format, + string style) + : base(BuildMessage(variable, format, style)) + { + this.Variable = variable; + this.Format = format; + this.Style = style; + } + + #endregion + + #region Public Properties + + /// + /// Gets the name of the missing variable. + /// + /// + /// The missing variable. + /// + public string Variable { get; private set; } + + /// + /// Gets the format that attempted to apply the style. + /// + /// + /// The format. + /// + public string Format { get; private set; } + + /// + /// Gets the style that could not be applied. + /// + /// + /// The style. + /// + public string Style { get; private set; } + + #endregion + + #region Methods + + /// + /// Builds the message. + /// + /// + /// The variable. + /// + /// + /// The format. + /// + /// + /// The style that was not supported. + /// + /// + /// The . + /// + private static string BuildMessage( + string variable, + string format, + string style) => + $"The variable '{variable}' could not be formatted as a '{format}' because the style '{style}' is not supported."; + + #endregion +} \ No newline at end of file diff --git a/src/Jeffijoe.MessageFormat/IMessageFormatter.cs b/src/Jeffijoe.MessageFormat/IMessageFormatter.cs index 2bd9eb9..d50a7ba 100644 --- a/src/Jeffijoe.MessageFormat/IMessageFormatter.cs +++ b/src/Jeffijoe.MessageFormat/IMessageFormatter.cs @@ -12,6 +12,15 @@ namespace Jeffijoe.MessageFormat /// public interface IMessageFormatter { + #region Public properties + + /// + /// The custom value formatter to use for formats like `number`, `date`, `time` etc. + /// + CustomValueFormatter? CustomValueFormatter { get; } + + #endregion + #region Public Methods and Operators /// diff --git a/src/Jeffijoe.MessageFormat/MessageFormatter.cs b/src/Jeffijoe.MessageFormat/MessageFormatter.cs index fd6693d..234b9d6 100644 --- a/src/Jeffijoe.MessageFormat/MessageFormatter.cs +++ b/src/Jeffijoe.MessageFormat/MessageFormatter.cs @@ -13,363 +13,381 @@ using Jeffijoe.MessageFormat.Helpers; using Jeffijoe.MessageFormat.Parsing; -namespace Jeffijoe.MessageFormat +namespace Jeffijoe.MessageFormat; + +/// +/// The magical Message Formatter. +/// +public class MessageFormatter : IMessageFormatter { + #region Static Fields + + /// + /// The instance of MessageFormatter, with the default locale + cache settings. + /// + private static readonly IMessageFormatter Instance = new MessageFormatter(); + + /// + /// The lock object. + /// + private static readonly object Lock = new object(); + + #endregion + + #region Fields + + /// + /// Pattern cache. If enabled, should speed up formatting the same pattern multiple times, + /// regardless of arguments. + /// + private readonly ConcurrentDictionary? cache; + + /// + /// The formatter library. + /// + private readonly IFormatterLibrary library; + /// - /// The magical Message Formatter. + /// The pattern parser /// - public class MessageFormatter : IMessageFormatter + private readonly IPatternParser patternParser; + + #endregion + + #region Constructors and Destructors + + /// + /// Initializes a new instance of the class. + /// + /// + /// The use Cache. + /// + /// + /// The locale. + /// + /// + /// The custom value formatter to use. Can be null. + /// + public MessageFormatter(bool useCache = true, string locale = "en", + CustomValueFormatter? customValueFormatter = null) + : this( + patternParser: new PatternParser(new LiteralParser()), + library: new FormatterLibrary(), + useCache: useCache, + locale: locale, + customValueFormatter: customValueFormatter) { - #region Static Fields - - /// - /// The instance of MessageFormatter, with the default locale + cache settings. - /// - private static readonly IMessageFormatter Instance = new MessageFormatter(); - - /// - /// The lock object. - /// - private static readonly object Lock = new object(); - - #endregion - - #region Fields - - /// - /// Pattern cache. If enabled, should speed up formatting the same pattern multiple times, - /// regardless of arguments. - /// - private readonly ConcurrentDictionary? cache; - - /// - /// The formatter library. - /// - private readonly IFormatterLibrary library; - - /// - /// The pattern parser - /// - private readonly IPatternParser patternParser; - - #endregion - - #region Constructors and Destructors - - /// - /// Initializes a new instance of the class. - /// - /// - /// The use Cache. - /// - /// - /// The locale. - /// - public MessageFormatter(bool useCache = true, string locale = "en") - : this(new PatternParser(new LiteralParser()), new FormatterLibrary(), useCache, locale) - { - } + } - /// - /// Initializes a new instance of the class. - /// - /// - /// The pattern parser. - /// - /// - /// The library. - /// - /// - /// if set to true uses the cache. - /// - /// - /// The locale to use. Formatters may need this. - /// - internal MessageFormatter( - IPatternParser patternParser, - IFormatterLibrary library, - bool useCache, - string locale = "en") + /// + /// Initializes a new instance of the class. + /// + /// + /// The pattern parser. + /// + /// + /// The library. + /// + /// + /// if set to true uses the cache. + /// + /// + /// The locale to use. Formatters may need this. + /// + /// + /// The custom value formatter to use. Can be null. + /// + internal MessageFormatter( + IPatternParser patternParser, + IFormatterLibrary library, + bool useCache, + string locale = "en", + CustomValueFormatter? customValueFormatter = null) + { + this.patternParser = patternParser ?? throw new ArgumentNullException("patternParser"); + this.library = library ?? throw new ArgumentNullException("library"); + this.CustomValueFormatter = customValueFormatter; + this.Locale = locale; + if (useCache) { - this.patternParser = patternParser ?? throw new ArgumentNullException("patternParser"); - this.library = library ?? throw new ArgumentNullException("library"); - this.Locale = locale; - if (useCache) - { - this.cache = new ConcurrentDictionary(); - } + this.cache = new ConcurrentDictionary(); } + } - #endregion + #endregion - #region Public Properties + #region Public Properties - /// - /// Gets the formatters library, where you can add your own formatters if you want. - /// - /// - /// The formatters. - /// - public IFormatterLibrary Formatters - { - get { return this.library; } - } + /// + /// The custom value formatter to use for formats like `number`, `date`, `time` etc. + /// + public CustomValueFormatter? CustomValueFormatter { get; private set; } + + /// + /// Gets the formatters library, where you can add your own formatters if you want. + /// + /// + /// The formatters. + /// + public IFormatterLibrary Formatters + { + get { return this.library; } + } - /// - /// Gets or sets the locale. - /// - /// - /// The locale. - /// - public string Locale { get; set; } - - /// - /// Gets the pluralizers dictionary from the , if set. Key is the locale. - /// - /// - /// The pluralizers, or null if the plural formatter has not been added. - /// - public IDictionary? Pluralizers + /// + /// Gets or sets the locale. + /// + /// + /// The locale. + /// + public string Locale { get; set; } + + /// + /// Gets the pluralizers dictionary from the , if set. Key is the locale. + /// + /// + /// The pluralizers, or null if the plural formatter has not been added. + /// + public IDictionary? Pluralizers + { + get { - get - { - var pluralFormatter = this.Formatters.OfType().FirstOrDefault(); - return pluralFormatter?.Pluralizers; - } + var pluralFormatter = this.Formatters.OfType().FirstOrDefault(); + return pluralFormatter?.Pluralizers; } + } + + #endregion - #endregion - - #region Public Methods and Operators - - /// - /// Formats the specified pattern with the specified data. - /// - /// - /// This method calls - /// on a singleton instance using a lock. - /// Do not use in a tight loop, as a lock is being used to ensure thread safety. - /// - /// - /// The pattern. - /// - /// - /// The data. - /// - /// - /// The formatted message. - /// - public static string Format(string pattern, IReadOnlyDictionary data) + #region Public Methods and Operators + + /// + /// Formats the specified pattern with the specified data. + /// + /// + /// This method calls + /// on a singleton instance using a lock. + /// Do not use in a tight loop, as a lock is being used to ensure thread safety. + /// + /// + /// The pattern. + /// + /// + /// The data. + /// + /// + /// The formatted message. + /// + public static string Format(string pattern, IReadOnlyDictionary data) + { + lock (Lock) { - lock (Lock) - { - return Instance.FormatMessage(pattern, data); - } + return Instance.FormatMessage(pattern, data); } + } - /// - /// Formats the specified pattern with the specified data. - /// - /// This method calls - /// - /// on a singleton instance using a lock. - /// Do not use in a tight loop, as a lock is being used to ensure thread safety. - /// - /// The pattern. - /// - /// - /// The data. - /// - /// - /// The formatted message. - /// - public static string Format(string pattern, object data) + /// + /// Formats the specified pattern with the specified data. + /// + /// This method calls + /// + /// on a singleton instance using a lock. + /// Do not use in a tight loop, as a lock is being used to ensure thread safety. + /// + /// The pattern. + /// + /// + /// The data. + /// + /// + /// The formatted message. + /// + public static string Format(string pattern, object data) + { + lock (Lock) { - lock (Lock) - { - return Instance.FormatMessage(pattern, data); - } + return Instance.FormatMessage(pattern, data); } + } - /// - /// Formats the message with the specified arguments. It's so magical. - /// - /// - /// The pattern. - /// - /// - /// The arguments. - /// - /// - /// The . - /// - public string FormatMessage(string pattern, IReadOnlyDictionary args) + /// + /// Formats the message with the specified arguments. It's so magical. + /// + /// + /// The pattern. + /// + /// + /// The arguments. + /// + /// + /// The . + /// + public string FormatMessage(string pattern, IReadOnlyDictionary args) + { + /* + * We are assuming the formatters are ordered correctly + * - that is, from left to right, string-wise. + */ + var sourceBuilder = StringBuilderPool.Get(); + + try { - /* - * We are assuming the formatters are ordered correctly - * - that is, from left to right, string-wise. - */ - var sourceBuilder = StringBuilderPool.Get(); + sourceBuilder.Append(pattern); + var requests = this.ParseRequests(pattern, sourceBuilder); - try + for (int i = 0; i < requests.Count; i++) { - sourceBuilder.Append(pattern); - var requests = this.ParseRequests(pattern, sourceBuilder); + var request = requests[i]; - for (int i = 0; i < requests.Count; i++) - { - var request = requests[i]; - - var formatter = this.Formatters.GetFormatter(request); - - if (args.TryGetValue(request.Variable, out var value) == false && formatter.VariableMustExist) - { - throw new VariableNotFoundException(request.Variable); - } + var formatter = this.Formatters.GetFormatter(request); - // Double dispatch, yeah! - var result = formatter.Format(this.Locale, request, args, value, this); + if (args.TryGetValue(request.Variable, out var value) == false && formatter.VariableMustExist) + { + throw new VariableNotFoundException(request.Variable); + } - // First, we remove the literal from the source. - var sourceLiteral = request.SourceLiteral; + // Double dispatch, yeah! + var result = formatter.Format(this.Locale, request, args, value, this); - // +1 because we want to include the last index. - var length = (sourceLiteral.EndIndex - sourceLiteral.StartIndex) + 1; - sourceBuilder.Remove(sourceLiteral.StartIndex, length); + // First, we remove the literal from the source. + var sourceLiteral = request.SourceLiteral; - // Now, we inject the result. - sourceBuilder.Insert(sourceLiteral.StartIndex, result); + // +1 because we want to include the last index. + var length = (sourceLiteral.EndIndex - sourceLiteral.StartIndex) + 1; + sourceBuilder.Remove(sourceLiteral.StartIndex, length); - // The next requests will want to know what happened. - requests.ShiftIndices(i, result.Length); - } + // Now, we inject the result. + sourceBuilder.Insert(sourceLiteral.StartIndex, result); - // And we're done. - return MessageFormatter.UnescapeLiterals(sourceBuilder); - } - finally - { - StringBuilderPool.Return(sourceBuilder); + // The next requests will want to know what happened. + requests.ShiftIndices(i, result.Length); } + + // And we're done. + return MessageFormatter.UnescapeLiterals(sourceBuilder); } + finally + { + StringBuilderPool.Return(sourceBuilder); + } + } - #endregion + #endregion - #region Methods + #region Methods - /// - /// Unescapes the literals from the source builder, and returns a new instance with literals unescaped. - /// - /// - /// The source builder. - /// - /// - /// The . - /// - internal static string UnescapeLiterals(StringBuilder sourceBuilder) + /// + /// Unescapes the literals from the source builder, and returns a new instance with literals unescaped. + /// + /// + /// The source builder. + /// + /// + /// The . + /// + internal static string UnescapeLiterals(StringBuilder sourceBuilder) + { + // If the block is empty, do nothing. + if (sourceBuilder.Length == 0) { - // If the block is empty, do nothing. - if (sourceBuilder.Length == 0) - { - return string.Empty; - } + return string.Empty; + } - const char EscapingChar = '\''; + const char EscapingChar = '\''; - if (!sourceBuilder.Contains(EscapingChar)) - { - return sourceBuilder.ToString(); - } + if (!sourceBuilder.Contains(EscapingChar)) + { + return sourceBuilder.ToString(); + } - var length = sourceBuilder.Length; - var insideEscapeSequence = false; + var length = sourceBuilder.Length; + var insideEscapeSequence = false; - var dest = StringBuilderPool.Get(); + var dest = StringBuilderPool.Get(); - try + try + { + for (int i = 0; i < length; i++) { - for (int i = 0; i < length; i++) - { - var c = sourceBuilder[i]; + var c = sourceBuilder[i]; - if (c == EscapingChar) + if (c == EscapingChar) + { + if (i == length - 1) { - if (i == length - 1) - { - if (!insideEscapeSequence) - dest.Append(EscapingChar); - continue; - } - - var nextChar = sourceBuilder[i + 1]; - if (nextChar == EscapingChar) - { + if (!insideEscapeSequence) dest.Append(EscapingChar); - ++i; - continue; - } - - if (insideEscapeSequence) - { - insideEscapeSequence = false; - continue; - } - - if (nextChar == '{' || nextChar == '}' || nextChar == '#') - { - dest.Append(nextChar); - insideEscapeSequence = true; - ++i; - continue; - } + continue; + } + var nextChar = sourceBuilder[i + 1]; + if (nextChar == EscapingChar) + { dest.Append(EscapingChar); + ++i; + continue; + } + + if (insideEscapeSequence) + { + insideEscapeSequence = false; continue; } - dest.Append(c); + if (nextChar == '{' || nextChar == '}' || nextChar == '#') + { + dest.Append(nextChar); + insideEscapeSequence = true; + ++i; + continue; + } + + dest.Append(EscapingChar); + continue; } - return dest.ToString(); + dest.Append(c); } - finally - { - StringBuilderPool.Return(dest); - } - } - /// - /// Parses the requests, using the cache if enabled and applicable. - /// - /// - /// The pattern. - /// - /// - /// The source builder. - /// - /// - /// The . - /// - private IFormatterRequestCollection ParseRequests(string pattern, StringBuilder sourceBuilder) + return dest.ToString(); + } + finally { - // If we are not using the cache, just parse them straight away. - if (this.cache == null) - { - return this.patternParser.Parse(sourceBuilder); - } - - // If we have a cached result from this pattern, clone it and return the clone. - if (this.cache.TryGetValue(pattern, out var cached)) - { - return cached.Clone(); - } + StringBuilderPool.Return(dest); + } + } - var requests = this.patternParser.Parse(sourceBuilder); - this.cache?.TryAdd(pattern, requests.Clone()); + /// + /// Parses the requests, using the cache if enabled and applicable. + /// + /// + /// The pattern. + /// + /// + /// The source builder. + /// + /// + /// The . + /// + private IFormatterRequestCollection ParseRequests(string pattern, StringBuilder sourceBuilder) + { + // If we are not using the cache, just parse them straight away. + if (this.cache == null) + { + return this.patternParser.Parse(sourceBuilder); + } - return requests; + // If we have a cached result from this pattern, clone it and return the clone. + if (this.cache.TryGetValue(pattern, out var cached)) + { + return cached.Clone(); } - #endregion + var requests = this.patternParser.Parse(sourceBuilder); + this.cache?.TryAdd(pattern, requests.Clone()); + + return requests; } + + #endregion } \ No newline at end of file From 4bb8c5fdb396cdc5f8606263cd13fad1a4bf8d02 Mon Sep 17 00:00:00 2001 From: Jeff Hansen Date: Wed, 11 Oct 2023 05:14:58 -0400 Subject: [PATCH 59/98] Update plurals.xml from latest CLDR release --- .../data/plurals.xml | 67 +++++++++++-------- 1 file changed, 38 insertions(+), 29 deletions(-) diff --git a/src/Jeffijoe.MessageFormat.MetadataGenerator/data/plurals.xml b/src/Jeffijoe.MessageFormat.MetadataGenerator/data/plurals.xml index e118c2e..49c49ea 100644 --- a/src/Jeffijoe.MessageFormat.MetadataGenerator/data/plurals.xml +++ b/src/Jeffijoe.MessageFormat.MetadataGenerator/data/plurals.xml @@ -1,9 +1,10 @@ @@ -12,7 +13,7 @@ For terms of use, see http://www.unicode.org/copyright.html - + @integer 0~15, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, … @@ -26,11 +27,7 @@ For terms of use, see http://www.unicode.org/copyright.html i = 0,1 @integer 0, 1 @decimal 0.0~1.5 @integer 2~17, 100, 1000, 10000, 100000, 1000000, … @decimal 2.0~3.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, … - - i = 0..1 @integer 0, 1 @decimal 0.0~1.5 - @integer 2~17, 100, 1000, 10000, 100000, 1000000, … @decimal 2.0~3.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, … - - + i = 1 and v = 0 @integer 1 @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, … @@ -46,7 +43,7 @@ For terms of use, see http://www.unicode.org/copyright.html n = 0..1 or n = 11..99 @integer 0, 1, 11~24 @decimal 0.0, 1.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0, 17.0, 18.0, 19.0, 20.0, 21.0, 22.0, 23.0, 24.0 @integer 2~10, 100~106, 1000, 10000, 100000, 1000000, … @decimal 0.1~0.9, 1.1~1.7, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, … - + n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000 @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, … @@ -55,8 +52,8 @@ For terms of use, see http://www.unicode.org/copyright.html @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0, 2.0~3.4, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, … - t = 0 and i % 10 = 1 and i % 100 != 11 or t != 0 @integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, … @decimal 0.1~1.6, 10.1, 100.1, 1000.1, … - @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, … + t = 0 and i % 10 = 1 and i % 100 != 11 or t % 10 = 1 and t % 100 != 11 @integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, … @decimal 0.1, 1.0, 1.1, 2.1, 3.1, 4.1, 5.1, 6.1, 7.1, 10.1, 100.1, 1000.1, … + @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0, 0.2~0.9, 1.2~1.8, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, … v = 0 and i % 10 = 1 and i % 100 != 11 or f % 10 = 1 and f % 100 != 11 @integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, … @decimal 0.1, 1.1, 2.1, 3.1, 4.1, 5.1, 6.1, 7.1, 10.1, 100.1, 1000.1, … @@ -87,6 +84,11 @@ For terms of use, see http://www.unicode.org/copyright.html + + i = 1 and v = 0 or i = 0 and v != 0 @integer 1 @decimal 0.0~0.9, 0.00~0.05 + i = 2 and v = 0 @integer 2 + @integer 0, 3~17, 100, 1000, 10000, 100000, 1000000, … @decimal 1.0~2.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, … + n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000 n = 2 @integer 2 @decimal 2.0, 2.00, 2.000, 2.0000 @@ -102,7 +104,7 @@ For terms of use, see http://www.unicode.org/copyright.html i = 1 and v = 0 @integer 1 - v != 0 or n = 0 or n % 100 = 2..19 @integer 0, 2~16, 102, 1002, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, … + v != 0 or n = 0 or n != 1 and n % 100 = 1..19 @integer 0, 2~16, 101, 1001, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, … @integer 20~35, 100, 1000, 10000, 100000, 1000000, … @@ -115,8 +117,23 @@ For terms of use, see http://www.unicode.org/copyright.html i = 0,1 @integer 0, 1 @decimal 0.0~1.5 - e = 0 and i != 0 and i % 1000000 = 0 and v = 0 or e != 0..5 @integer 1000000, 1e6, 2e6, 3e6, 4e6, 5e6, 6e6, … @decimal 1.0000001e6, 1.1e6, 2.0000001e6, 2.1e6, 3.0000001e6, 3.1e6, … - @integer 2~17, 100, 1000, 10000, 100000, 1e3, 2e3, 3e3, 4e3, 5e3, 6e3, … @decimal 2.0~3.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, 1.0001e3, 1.1e3, 2.0001e3, 2.1e3, 3.0001e3, 3.1e3, … + e = 0 and i != 0 and i % 1000000 = 0 and v = 0 or e != 0..5 @integer 1000000, 1c6, 2c6, 3c6, 4c6, 5c6, 6c6, … @decimal 1.0000001c6, 1.1c6, 2.0000001c6, 2.1c6, 3.0000001c6, 3.1c6, … + @integer 2~17, 100, 1000, 10000, 100000, 1c3, 2c3, 3c3, 4c3, 5c3, 6c3, … @decimal 2.0~3.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, 1.0001c3, 1.1c3, 2.0001c3, 2.1c3, 3.0001c3, 3.1c3, … + + + i = 0..1 @integer 0, 1 @decimal 0.0~1.5 + e = 0 and i != 0 and i % 1000000 = 0 and v = 0 or e != 0..5 @integer 1000000, 1c6, 2c6, 3c6, 4c6, 5c6, 6c6, … @decimal 1.0000001c6, 1.1c6, 2.0000001c6, 2.1c6, 3.0000001c6, 3.1c6, … + @integer 2~17, 100, 1000, 10000, 100000, 1c3, 2c3, 3c3, 4c3, 5c3, 6c3, … @decimal 2.0~3.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, 1.0001c3, 1.1c3, 2.0001c3, 2.1c3, 3.0001c3, 3.1c3, … + + + i = 1 and v = 0 @integer 1 + e = 0 and i != 0 and i % 1000000 = 0 and v = 0 or e != 0..5 @integer 1000000, 1c6, 2c6, 3c6, 4c6, 5c6, 6c6, … @decimal 1.0000001c6, 1.1c6, 2.0000001c6, 2.1c6, 3.0000001c6, 3.1c6, … + @integer 0, 2~16, 100, 1000, 10000, 100000, 1c3, 2c3, 3c3, 4c3, 5c3, 6c3, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, 1.0001c3, 1.1c3, 2.0001c3, 2.1c3, 3.0001c3, 3.1c3, … + + + n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000 + e = 0 and i != 0 and i % 1000000 = 0 and v = 0 or e != 0..5 @integer 1000000, 1c6, 2c6, 3c6, 4c6, 5c6, 6c6, … @decimal 1.0000001c6, 1.1c6, 2.0000001c6, 2.1c6, 3.0000001c6, 3.1c6, … + @integer 0, 2~16, 100, 1000, 10000, 100000, 1c3, 2c3, 3c3, 4c3, 5c3, 6c3, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, 1.0001c3, 1.1c3, 2.0001c3, 2.1c3, 3.0001c3, 3.1c3, … @@ -140,15 +157,6 @@ For terms of use, see http://www.unicode.org/copyright.html @integer 0, 5~19, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0, 0.5~1.0, 1.5~2.0, 2.5~2.7, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, … - - - - i = 1 and v = 0 @integer 1 - i = 2 and v = 0 @integer 2 - v = 0 and n != 0..10 and n % 10 = 0 @integer 20, 30, 40, 50, 60, 70, 80, 90, 100, 1000, 10000, 100000, 1000000, … - @integer 0, 3~17, 101, 1001, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, … - - @@ -175,12 +183,6 @@ For terms of use, see http://www.unicode.org/copyright.html f != 0 @decimal 0.1~0.9, 1.1~1.7, 10.1, 100.1, 1000.1, … @integer 0, 10~20, 30, 40, 50, 60, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0, 10.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, … - - n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000 - n = 0 or n % 100 = 2..10 @integer 0, 2~10, 102~107, 1002, … @decimal 0.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 10.0, 102.0, 1002.0, … - n % 100 = 11..19 @integer 11~19, 111~117, 1011, … @decimal 11.0, 12.0, 13.0, 14.0, 15.0, 16.0, 17.0, 18.0, 111.0, 1011.0, … - @integer 20~35, 100, 1000, 10000, 100000, 1000000, … @decimal 0.1~0.9, 1.1~1.7, 10.1, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, … - v = 0 and i % 10 = 1 and i % 100 != 11 @integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, … v = 0 and i % 10 = 2..4 and i % 100 != 12..14 @integer 2~4, 22~24, 32~34, 42~44, 52~54, 62, 102, 1002, … @@ -197,6 +199,13 @@ For terms of use, see http://www.unicode.org/copyright.html n != 0 and n % 1000000 = 0 @integer 1000000, … @decimal 1000000.0, 1000000.00, 1000000.000, 1000000.0000, … @integer 0, 5~8, 10~20, 100, 1000, 10000, 100000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, … + + n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000 + n = 2 @integer 2 @decimal 2.0, 2.00, 2.000, 2.0000 + n = 0 or n % 100 = 3..10 @integer 0, 3~10, 103~109, 1003, … @decimal 0.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 103.0, 1003.0, … + n % 100 = 11..19 @integer 11~19, 111~117, 1011, … @decimal 11.0, 12.0, 13.0, 14.0, 15.0, 16.0, 17.0, 18.0, 111.0, 1011.0, … + @integer 20~35, 100, 1000, 10000, 100000, 1000000, … @decimal 0.1~0.9, 1.1~1.7, 10.1, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, … + n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000 n = 2 @integer 2 @decimal 2.0, 2.00, 2.000, 2.0000 From aec7e82670f2f0286c5f884e0ebf4d94f64116fa Mon Sep 17 00:00:00 2001 From: Bruno Juchli Date: Fri, 5 Jan 2024 06:16:13 +0100 Subject: [PATCH 60/98] Update README - fix Link Fixes link to https://unicode-org.github.io/icu/userguide/format_parse/messages/ --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 24c22a2..afbbe36 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ [![Build & Test](https://github.com/jeffijoe/messageformat.net/actions/workflows/ci.yml/badge.svg)](https://github.com/jeffijoe/messageformat.net/actions/workflows/ci.yml) This is an implementation of the ICU Message Format in .NET. For official information about the format, go to: -http://userguide.icu-project.org/formatparse/messages +https://unicode-org.github.io/icu/userguide/format_parse/messages/ ## Quickstart @@ -204,4 +204,4 @@ You can find me on Twitter: [@jeffijoe][1]. [0]: https://github.com/SlexAxton/messageformat.js [1]: https://twitter.com/jeffijoe - [plural-cldr]: https://cldr.unicode.org/index/downloads \ No newline at end of file + [plural-cldr]: https://cldr.unicode.org/index/downloads From 0773a4ae7a0abbc26969f68fbae09bec8fc7e553 Mon Sep 17 00:00:00 2001 From: Frooxius Date: Mon, 6 Nov 2023 10:25:00 -0600 Subject: [PATCH 61/98] Fix source generator Source generators must use NetStandard 2.0 as per documentation. Having .NET 6 and .NetStandard 2.1 included has been causing VS2022 to load the wrong version and not run the source generator properly --- .../Jeffijoe.MessageFormat.MetadataGenerator.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Jeffijoe.MessageFormat.MetadataGenerator/Jeffijoe.MessageFormat.MetadataGenerator.csproj b/src/Jeffijoe.MessageFormat.MetadataGenerator/Jeffijoe.MessageFormat.MetadataGenerator.csproj index c419da9..f4a0f3a 100644 --- a/src/Jeffijoe.MessageFormat.MetadataGenerator/Jeffijoe.MessageFormat.MetadataGenerator.csproj +++ b/src/Jeffijoe.MessageFormat.MetadataGenerator/Jeffijoe.MessageFormat.MetadataGenerator.csproj @@ -5,7 +5,7 @@ ../Jeffijoe.MessageFormat/MessageFormat.snk default enable - net6.0;netstandard2.0;netstandard2.1 + netstandard2.0 From c6fb960df90363da075b4bcb5461c0d1bcf24552 Mon Sep 17 00:00:00 2001 From: Frooxius Date: Mon, 6 Nov 2023 10:32:38 -0600 Subject: [PATCH 62/98] Fix DateFormatter_Custom --- .../Formatting/Formatters/DateFormatterTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/DateFormatterTests.cs b/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/DateFormatterTests.cs index d85242f..8410bfe 100644 --- a/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/DateFormatterTests.cs +++ b/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/DateFormatterTests.cs @@ -51,10 +51,10 @@ public void DateFormatter_Custom() { var formatter = new CustomValueFormatters { - Date = (CultureInfo _, object? value, string? _, out string? formatted) => + Date = (CultureInfo culture, object? value, string? _, out string? formatted) => { // This is just a test, you probably shouldn't be doing this in real workloads. - formatted = $"{value:MMMM d 'in the year' yyyy}"; + formatted = ((FormattableString)$"{value:MMMM d 'in the year' yyyy}").ToString(culture); return true; } }; From 935e36b92434b584dcee4a4df7b5ac6f0396e6de Mon Sep 17 00:00:00 2001 From: Frooxius Date: Mon, 6 Nov 2023 10:34:46 -0600 Subject: [PATCH 63/98] Fix NumberFormatter_Decimal_CustomFormat --- .../Formatting/Formatters/NumberFormatterTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/NumberFormatterTests.cs b/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/NumberFormatterTests.cs index c689b7e..f3f81e3 100644 --- a/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/NumberFormatterTests.cs +++ b/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/NumberFormatterTests.cs @@ -30,9 +30,9 @@ public void NumberFormatter_Decimal_CustomFormat(decimal number, string expected { var formatters = new CustomValueFormatters { - Number = (CultureInfo _, object? value, string? style, out string? formatted) => + Number = (CultureInfo culture, object? value, string? style, out string? formatted) => { - formatted = string.Format($"{{0:{style}}}", value); + formatted = string.Format(culture, $"{{0:{style}}}", value); return true; } }; From a61d87b7fba035b3fee1cd2569b227b53a6a97d3 Mon Sep 17 00:00:00 2001 From: Frooxius Date: Mon, 6 Nov 2023 10:41:17 -0600 Subject: [PATCH 64/98] Fix NumberFormatter_Integer --- .../Formatting/Formatters/NumberFormatter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Jeffijoe.MessageFormat/Formatting/Formatters/NumberFormatter.cs b/src/Jeffijoe.MessageFormat/Formatting/Formatters/NumberFormatter.cs index 2c3c4d3..aef1e10 100644 --- a/src/Jeffijoe.MessageFormat/Formatting/Formatters/NumberFormatter.cs +++ b/src/Jeffijoe.MessageFormat/Formatting/Formatters/NumberFormatter.cs @@ -55,7 +55,7 @@ private static string FormatInteger(IFormatProvider cultureInfo, object? value) value switch { decimal or float or double => string.Format(cultureInfo, "{0}", Convert.ToInt64(value)), - string s => decimal.TryParse(s, out var parsed) ? FormatInteger(cultureInfo, parsed) : s, + string s => decimal.TryParse(s, NumberStyles.Any, cultureInfo, out var parsed) ? FormatInteger(cultureInfo, parsed) : s, _ => string.Format(cultureInfo, "{0}", value) }; } \ No newline at end of file From e85b2e6c2cdebff0437a9cd50b87da0d6e76f6b3 Mon Sep 17 00:00:00 2001 From: Frooxius Date: Mon, 6 Nov 2023 10:53:55 -0600 Subject: [PATCH 65/98] Fix ParseLiterals_position_and_inner_text --- .../Parsing/LiteralParserTests.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Jeffijoe.MessageFormat.Tests/Parsing/LiteralParserTests.cs b/src/Jeffijoe.MessageFormat.Tests/Parsing/LiteralParserTests.cs index fbb44c5..8be5277 100644 --- a/src/Jeffijoe.MessageFormat.Tests/Parsing/LiteralParserTests.cs +++ b/src/Jeffijoe.MessageFormat.Tests/Parsing/LiteralParserTests.cs @@ -4,6 +4,7 @@ // Author: Jeff Hansen // Copyright (C) Jeff Hansen 2015. All rights reserved. +using System; using System.Linq; using System.Text; @@ -135,6 +136,11 @@ public void ParseLiterals_unclosed_escape_sequence( ")] public void ParseLiterals_position_and_inner_text(string source, int[] position, string expectedInnerText) { + // It seems that depending on platform this is compiled on, the actual representation of new lines in the + // string literals can differ, which can make this test fail due to differences. + // This will normalize those changes. + expectedInnerText = expectedInnerText.Replace("\r\n", "\n"); + var sb = new StringBuilder(source); var subject = new LiteralParser(); var actual = subject.ParseLiterals(sb); From 40fc8b0998faa3fd0d1b6361e8b25c7e8b342626 Mon Sep 17 00:00:00 2001 From: Jeff Hansen Date: Sat, 28 Sep 2024 18:38:36 -0400 Subject: [PATCH 66/98] Fix target framework --- src/.idea/.idea.MessageFormat/.idea/projectSettingsUpdater.xml | 2 +- .../Jeffijoe.MessageFormat.MetadataGenerator.csproj | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/.idea/.idea.MessageFormat/.idea/projectSettingsUpdater.xml b/src/.idea/.idea.MessageFormat/.idea/projectSettingsUpdater.xml index 4bb9f4d..86cc6c6 100644 --- a/src/.idea/.idea.MessageFormat/.idea/projectSettingsUpdater.xml +++ b/src/.idea/.idea.MessageFormat/.idea/projectSettingsUpdater.xml @@ -1,6 +1,6 @@ - \ No newline at end of file diff --git a/src/Jeffijoe.MessageFormat.MetadataGenerator/Jeffijoe.MessageFormat.MetadataGenerator.csproj b/src/Jeffijoe.MessageFormat.MetadataGenerator/Jeffijoe.MessageFormat.MetadataGenerator.csproj index f4a0f3a..8599668 100644 --- a/src/Jeffijoe.MessageFormat.MetadataGenerator/Jeffijoe.MessageFormat.MetadataGenerator.csproj +++ b/src/Jeffijoe.MessageFormat.MetadataGenerator/Jeffijoe.MessageFormat.MetadataGenerator.csproj @@ -5,7 +5,7 @@ ../Jeffijoe.MessageFormat/MessageFormat.snk default enable - netstandard2.0 + netstandard2.0 From 08b468f41b2ef84c828bc4025436e0e973f0cfaa Mon Sep 17 00:00:00 2001 From: Jeff Hansen Date: Sat, 28 Sep 2024 18:46:44 -0400 Subject: [PATCH 67/98] Update to .NET 8 + upgrade packages --- .github/workflows/ci.yml | 6 +++--- ...Jeffijoe.MessageFormat.MetadataGenerator.csproj | 5 +++-- .../Jeffijoe.MessageFormat.Tests.csproj | 14 +++++++------- .../MessageFormatterStringExtensionTests.cs | 2 +- .../Parsing/PatternParserGetKeyTests.cs | 2 +- .../Jeffijoe.MessageFormat.csproj | 9 +++------ 6 files changed, 18 insertions(+), 20 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7f42f50..8c07a19 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,14 +18,14 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 with: fetch-depth: 100 - name: Setup .NET - uses: actions/setup-dotnet@v3 + uses: actions/setup-dotnet@v4 with: - dotnet-version: 7.0.x + dotnet-version: 8.0.x - name: Install dependencies working-directory: ./src diff --git a/src/Jeffijoe.MessageFormat.MetadataGenerator/Jeffijoe.MessageFormat.MetadataGenerator.csproj b/src/Jeffijoe.MessageFormat.MetadataGenerator/Jeffijoe.MessageFormat.MetadataGenerator.csproj index 8599668..57486f9 100644 --- a/src/Jeffijoe.MessageFormat.MetadataGenerator/Jeffijoe.MessageFormat.MetadataGenerator.csproj +++ b/src/Jeffijoe.MessageFormat.MetadataGenerator/Jeffijoe.MessageFormat.MetadataGenerator.csproj @@ -6,6 +6,7 @@ default enable netstandard2.0 + true @@ -13,11 +14,11 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/src/Jeffijoe.MessageFormat.Tests/Jeffijoe.MessageFormat.Tests.csproj b/src/Jeffijoe.MessageFormat.Tests/Jeffijoe.MessageFormat.Tests.csproj index 33d9aef..2aed5cd 100644 --- a/src/Jeffijoe.MessageFormat.Tests/Jeffijoe.MessageFormat.Tests.csproj +++ b/src/Jeffijoe.MessageFormat.Tests/Jeffijoe.MessageFormat.Tests.csproj @@ -6,18 +6,18 @@ False 9 enable - net7.0 + net8.0 - - + + - - - - + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Jeffijoe.MessageFormat.Tests/MessageFormatterStringExtensionTests.cs b/src/Jeffijoe.MessageFormat.Tests/MessageFormatterStringExtensionTests.cs index 6271c51..4884687 100644 --- a/src/Jeffijoe.MessageFormat.Tests/MessageFormatterStringExtensionTests.cs +++ b/src/Jeffijoe.MessageFormat.Tests/MessageFormatterStringExtensionTests.cs @@ -32,7 +32,7 @@ public async Task FormatMessage_with_multiple_tasks() var t1 = Task.Run(() => MessageFormatter.Format(pattern, new { fileCount = 1 })); var t2 = Task.Run(() => MessageFormatter.Format(pattern, new { fileCount = 1 })); var t3 = Task.Run(() => MessageFormatter.Format(pattern, new { fileCount = 5 })); - await Task.WhenAll(t1, t2); + await Task.WhenAll(t1, t2, t3); Assert.Equal("Copying one file.", t1.Result); Assert.Equal("Copying one file.", t2.Result); diff --git a/src/Jeffijoe.MessageFormat.Tests/Parsing/PatternParserGetKeyTests.cs b/src/Jeffijoe.MessageFormat.Tests/Parsing/PatternParserGetKeyTests.cs index 7e5c79f..77914a4 100644 --- a/src/Jeffijoe.MessageFormat.Tests/Parsing/PatternParserGetKeyTests.cs +++ b/src/Jeffijoe.MessageFormat.Tests/Parsing/PatternParserGetKeyTests.cs @@ -136,7 +136,7 @@ public void ReadLiteralSection_throws_with_invalid_characters( [InlineData("SupDawg,yeah ", "yeah", 8)] [InlineData("SupDawg, ", null, 8)] [InlineData("SupDawg,", null, 8)] - public void ReadLiteralSection_with_offset(string source, string expected, int offset) + public void ReadLiteralSection_with_offset(string source, string? expected, int offset) { var literal = new Literal(10, 10, 1, 1, source); Assert.Equal(expected, PatternParser.ReadLiteralSection(literal, offset, true, out _)); diff --git a/src/Jeffijoe.MessageFormat/Jeffijoe.MessageFormat.csproj b/src/Jeffijoe.MessageFormat/Jeffijoe.MessageFormat.csproj index 8302318..2d26eee 100644 --- a/src/Jeffijoe.MessageFormat/Jeffijoe.MessageFormat.csproj +++ b/src/Jeffijoe.MessageFormat/Jeffijoe.MessageFormat.csproj @@ -10,7 +10,7 @@ https://github.com/jeffijoe/messageformat.net latest enable - net6.0;net7.0;netstandard2.0;netstandard2.1 + net6.0;net8.0;netstandard2.0;netstandard2.1 @@ -18,8 +18,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -29,9 +29,6 @@ - - - From 6ac96e5d9bb81e3aeefe525bb213cdea8c4a14e6 Mon Sep 17 00:00:00 2001 From: Jeff Hansen Date: Mon, 14 Oct 2024 08:22:24 -0400 Subject: [PATCH 68/98] Fix whitespace normalization in tests --- .../.idea/projectSettingsUpdater.xml | 1 + .../Formatters/TimeFormatterTests.cs | 20 ++++++++++++++----- .../Jeffijoe.MessageFormat.Tests.csproj | 2 +- src/MessageFormat.sln.DotSettings | 2 ++ 4 files changed, 19 insertions(+), 6 deletions(-) create mode 100644 src/MessageFormat.sln.DotSettings diff --git a/src/.idea/.idea.MessageFormat/.idea/projectSettingsUpdater.xml b/src/.idea/.idea.MessageFormat/.idea/projectSettingsUpdater.xml index 86cc6c6..64af657 100644 --- a/src/.idea/.idea.MessageFormat/.idea/projectSettingsUpdater.xml +++ b/src/.idea/.idea.MessageFormat/.idea/projectSettingsUpdater.xml @@ -1,6 +1,7 @@ + \ No newline at end of file diff --git a/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/TimeFormatterTests.cs b/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/TimeFormatterTests.cs index acddc92..f582b33 100644 --- a/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/TimeFormatterTests.cs +++ b/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/TimeFormatterTests.cs @@ -1,12 +1,14 @@ using System; using System.Globalization; +using System.Text.RegularExpressions; using Jeffijoe.MessageFormat.Formatting; using Xunit; namespace Jeffijoe.MessageFormat.Tests.Formatting.Formatters { - public class TimeFormatterTests + public partial class TimeFormatterTests { + [Theory] [InlineData("en-US", "1994-09-06T15:01:23Z", "3:01 PM")] [InlineData("da-DK", "1994-09-06T15:01:23Z", "15.01")] @@ -19,8 +21,8 @@ public void TimeFormatter_Short(string locale, string dateStr, string expected) }); // Replacing all whitespace due to a difference in formatting on macOS vs Linux. - expected = expected.Replace(" ", ""); - actual = actual.Replace(" ", ""); + expected = Normalize(expected); + actual = Normalize(actual); Assert.Equal(expected, actual); } @@ -36,8 +38,8 @@ public void TimeFormatter_Default(string locale, string dateStr, string expected }); // Replacing all whitespace due to a difference in formatting on macOS vs Linux. - expected = expected.Replace(" ", ""); - actual = actual.Replace(" ", ""); + expected = Normalize(expected); + actual = Normalize(actual); Assert.Equal(expected, actual); } @@ -71,5 +73,13 @@ public void TimeFormatter_Custom() Assert.Equal("420 nice", actual); } + + [GeneratedRegex("\\s")] + private static partial Regex WhitespaceRegex(); + + private static string Normalize(string input) + { + return WhitespaceRegex().Replace(input, string.Empty); + } } } \ No newline at end of file diff --git a/src/Jeffijoe.MessageFormat.Tests/Jeffijoe.MessageFormat.Tests.csproj b/src/Jeffijoe.MessageFormat.Tests/Jeffijoe.MessageFormat.Tests.csproj index 2aed5cd..a24f302 100644 --- a/src/Jeffijoe.MessageFormat.Tests/Jeffijoe.MessageFormat.Tests.csproj +++ b/src/Jeffijoe.MessageFormat.Tests/Jeffijoe.MessageFormat.Tests.csproj @@ -4,7 +4,7 @@ True MessageFormat.snk False - 9 + 12 enable net8.0 diff --git a/src/MessageFormat.sln.DotSettings b/src/MessageFormat.sln.DotSettings new file mode 100644 index 0000000..0fee803 --- /dev/null +++ b/src/MessageFormat.sln.DotSettings @@ -0,0 +1,2 @@ + + True \ No newline at end of file From b59363b238d464dccb100d786501591959f9a541 Mon Sep 17 00:00:00 2001 From: Jeff Hansen Date: Mon, 14 Oct 2024 08:37:02 -0400 Subject: [PATCH 69/98] Fix issue where urls were parsed as offsets Closes #45 --- .../MessageFormatterCachingTests.cs | 99 +------------------ .../Formatting/BaseFormatter.cs | 15 ++- .../Formatting/Formatters/SelectFormatter.cs | 2 +- 3 files changed, 14 insertions(+), 102 deletions(-) diff --git a/src/Jeffijoe.MessageFormat.Tests/MessageFormatterCachingTests.cs b/src/Jeffijoe.MessageFormat.Tests/MessageFormatterCachingTests.cs index 6215df9..69123b6 100644 --- a/src/Jeffijoe.MessageFormat.Tests/MessageFormatterCachingTests.cs +++ b/src/Jeffijoe.MessageFormat.Tests/MessageFormatterCachingTests.cs @@ -4,48 +4,17 @@ // Author: Jeff Hansen // Copyright (C) Jeff Hansen 2015. All rights reserved. -using System; -using System.Collections.Generic; -using System.Text; - using Jeffijoe.MessageFormat.Formatting; -using Jeffijoe.MessageFormat.Helpers; using Jeffijoe.MessageFormat.Tests.TestHelpers; - using Xunit; -using Xunit.Abstractions; namespace Jeffijoe.MessageFormat.Tests { /// /// The message formatter_caching_tests. /// - public class MessageFormatterCachingTests + public class MessageFormatterCachingTests() { - #region Fields - - /// - /// The output helper. - /// - private readonly ITestOutputHelper outputHelper; - - #endregion - - #region Constructors and Destructors - - /// - /// Initializes a new instance of the class. - /// - /// - /// The output helper. - /// - public MessageFormatterCachingTests(ITestOutputHelper outputHelper) - { - this.outputHelper = outputHelper; - } - - #endregion - #region Public Methods and Operators /// @@ -75,71 +44,7 @@ public void FormatMessage_caches_reused_pattern() Assert.Equal("Hi Ma'am!", actual); Assert.Equal(3, parser.ParseCount); } - - /// - /// The format message_with_cache_benchmark. - /// - [Fact] - public void FormatMessage_with_cache_benchmark() - { - var subject = new MessageFormatter(useCache: true); - this.Benchmark(subject); - } - - /// - /// The format message_without_cache_benchmark. - /// - [Fact] - public void FormatMessage_without_cache_benchmark() - { - var subject = new MessageFormatter(false); - this.Benchmark(subject); - } - - #endregion - - #region Methods - - /// - /// The benchmark. - /// - /// - /// The subject. - /// - private void Benchmark(MessageFormatter subject) - { - var pattern = "\r\n----\r\nOh {name}? And if we were " + "to surround {gender, select, " + "male {his} " - + "female {her}" + "} name with '{' and '}', it would look " - + "like '{'{name}'}'? Yeah, I know {gender, select, " + "male {him} " + "female {her}" - + "}. {gender, select, " + "male {He's}" + "female {She's}" + "} got {messageCount, plural, " - + "zero {no messages}" + "one {just one message}" + "=42 {a universal amount of messages}" - + "other {uuhm... let's see.. Oh yeah, # messages - and here's a pound: '#'}" + "}!"; - int iterations = 100000; - var args = new Dictionary[iterations]; - var rnd = new Random(); - for (int i = 0; i < iterations; i++) - { - var val = rnd.Next(50); - args[i] = - new - { - gender = val % 2 == 0 ? "male" : "female", - name = val % 2 == 0 ? "Jeff" : "Marcela", - messageCount = val - }.ToDictionary(); - } - - TestHelpers.Benchmark.Start("Formatting message " + iterations + " times, no warm-up.", this.outputHelper); - var output = new StringBuilder(); - for (int i = 0; i < iterations; i++) - { - output.AppendLine(subject.FormatMessage(pattern, args[i])); - } - - TestHelpers.Benchmark.End(this.outputHelper); - this.outputHelper.WriteLine(output.ToString()); - } - + #endregion } } \ No newline at end of file diff --git a/src/Jeffijoe.MessageFormat/Formatting/BaseFormatter.cs b/src/Jeffijoe.MessageFormat/Formatting/BaseFormatter.cs index e773c5c..b90e830 100644 --- a/src/Jeffijoe.MessageFormat/Formatting/BaseFormatter.cs +++ b/src/Jeffijoe.MessageFormat/Formatting/BaseFormatter.cs @@ -57,21 +57,22 @@ protected internal IEnumerable ParseExtensions(FormatterRequ return Enumerable.Empty(); } - int length = request.FormatterArguments.Length; + var length = request.FormatterArguments.Length; index = 0; const char Colon = ':'; - bool foundExtension = false; + const char OpenBrace = '{'; + var foundExtension = false; var extension = StringBuilderPool.Get(); var value = StringBuilderPool.Get(); try { - for (int i = 0; i < length; i++) + for (var i = 0; i < length; i++) { var c = request.FormatterArguments[i]; // Whitespace is tolerated at the beginning. - bool isWhiteSpace = char.IsWhiteSpace(c); + var isWhiteSpace = char.IsWhiteSpace(c); if (isWhiteSpace) { // We've reached the end @@ -106,6 +107,12 @@ protected internal IEnumerable ParseExtensions(FormatterRequ continue; } + if (c == OpenBrace) + { + // It's not an extension. + break; + } + extension.Append(c); } diff --git a/src/Jeffijoe.MessageFormat/Formatting/Formatters/SelectFormatter.cs b/src/Jeffijoe.MessageFormat/Formatting/Formatters/SelectFormatter.cs index 24d2f15..3c5c6f7 100644 --- a/src/Jeffijoe.MessageFormat/Formatting/Formatters/SelectFormatter.cs +++ b/src/Jeffijoe.MessageFormat/Formatting/Formatters/SelectFormatter.cs @@ -65,11 +65,11 @@ public string Format(string locale, object? value, IMessageFormatter messageFormatter) { + var str = Convert.ToString(value); var parsed = this.ParseArguments(request); KeyedBlock? other = null; foreach (var keyedBlock in parsed.KeyedBlocks) { - var str = Convert.ToString(value); if (str == keyedBlock.Key) { return messageFormatter.FormatMessage(keyedBlock.BlockText, args); From 0500f572b59e80b5360cea9f7e59e8ac817c4779 Mon Sep 17 00:00:00 2001 From: Jeff Hansen Date: Mon, 14 Oct 2024 08:38:48 -0400 Subject: [PATCH 70/98] Use file-scoped namespaces in solution --- .../Plural/Parsing/AST/Condition.cs | 27 +- .../Plural/Parsing/AST/ILeftOperand.cs | 7 +- .../Plural/Parsing/AST/IRightOperand.cs | 7 +- .../Plural/Parsing/AST/LeftOperand.cs | 39 +- .../Plural/Parsing/AST/NumberOperand.cs | 33 +- .../Plural/Parsing/AST/OperandSymbol.cs | 87 +- .../Plural/Parsing/AST/Operation.cs | 25 +- .../Plural/Parsing/AST/OrCondition.cs | 17 +- .../Plural/Parsing/AST/PluralRule.cs | 21 +- .../Plural/Parsing/AST/RangeOperand.cs | 39 +- .../Plural/Parsing/AST/Relation.cs | 11 +- .../Plural/Parsing/AST/VariableOperand.cs | 33 +- .../Parsing/InvalidCharacterException.cs | 11 +- .../Plural/Parsing/PluralParser.cs | 95 +- .../Plural/Parsing/RuleParser.cs | 349 +++--- .../Plural/PluralLanguagesGenerator.cs | 69 +- .../PluralRulesMetadataGenerator.cs | 139 ++- .../Plural/SourceGeneration/RuleGenerator.cs | 193 ++-- .../Formatting/BaseFormatterTests.cs | 597 ++++++----- .../Formatting/FormatterLibraryTests.cs | 57 +- .../Formatters/DateFormatterTests.cs | 101 +- .../Formatters/NumberFormatterTests.cs | 225 ++-- .../Formatters/PluralFormatterTests.cs | 195 ++-- .../Formatters/SelectFormatterTests.cs | 131 ++- .../Formatters/TimeFormatterTests.cs | 125 ++- .../Formatters/VariableFormatterTests.cs | 145 ++- .../Helpers/CharHelperTests.cs | 43 +- .../Helpers/ObjectHelperTests.cs | 175 ++- .../Helpers/StringBuilderHelperTests.cs | 133 ++- .../MessageFormatterCachingTests.cs | 67 +- .../MessageFormatterFullIntegrationTests.cs | 997 +++++++++--------- .../MessageFormatterIssues.cs | 125 ++- .../MessageFormatterStringExtensionTests.cs | 55 +- .../MessageFormatterTests.cs | 163 ++- .../MessageFormatterUsingRealParserTests.cs | 121 ++- .../GeneratedPluralRulesTests.cs | 143 ++- .../MetadataGenerator/ParserTests.cs | 445 ++++---- .../MetadataGenerator/PluralContextTests.cs | 205 ++-- .../PluralMetadataClassGeneratorTests.cs | 27 +- .../RuleSourceGeneratorTests.cs | 377 ++++--- .../FormatterRequestCollectionTests.cs | 139 ++- .../Parsing/LiteralParserTests.cs | 307 +++--- .../Parsing/LiteralTests.cs | 39 +- .../Parsing/PatternParserGetKeyTests.cs | 237 +++-- .../Parsing/PatternParserParseTests.cs | 169 ++- .../PatternParserWithRealLiteralParser.cs | 103 +- .../TestHelpers/Benchmark.cs | 83 +- .../TestHelpers/FakeFormatter.cs | 87 +- .../TestHelpers/FakeLiteralParser.cs | 59 +- .../TestHelpers/FakeMessageFormatter.cs | 19 +- .../TestHelpers/TrackingPatternParser.cs | 49 +- .../FormatterNotFoundException.cs | 81 +- .../Formatting/BaseFormatter.cs | 493 +++++---- .../Formatting/FormatterExtension.cs | 75 +- .../Formatting/FormatterLibrary.cs | 81 +- .../Formatting/FormatterRequest.cs | 149 ++- .../Formatting/Formatters/PluralContext.cs | 109 +- .../Formatting/Formatters/PluralFormatter.cs | 513 +++++---- .../Formatters/PluralRulesMetadata.cs | 13 +- .../Formatting/Formatters/Pluralizer.cs | 29 +- .../Formatting/Formatters/SelectFormatter.cs | 131 ++- .../Formatters/VariableFormatter.cs | 139 ++- .../Formatting/IFormatter.cs | 99 +- .../Formatting/IFormatterLibrary.cs | 35 +- .../Formatting/KeyedBlock.cs | 77 +- .../Formatting/ParsedArguments.cs | 81 +- .../Formatting/VariableNotFoundException.cs | 95 +- .../Helpers/CharHelper.cs | 61 +- .../Helpers/ObjectHelper.cs | 93 +- .../Helpers/StringBuilderHelper.cs | 197 ++-- .../IMessageFormatter.cs | 61 +- .../Jeffijoe.MessageFormat.csproj | 2 +- .../MessageFormatterException.cs | 33 +- .../Parsing/FormatterRequestCollection.cs | 89 +- .../Parsing/IFormatterRequestCollection.cs | 59 +- .../Parsing/ILiteralParser.cs | 35 +- .../Parsing/IPatternParser.cs | 37 +- src/Jeffijoe.MessageFormat/Parsing/Literal.cs | 215 ++-- .../Parsing/LiteralParser.cs | 267 +++-- .../Parsing/MalformedLiteralException.cs | 173 ++- .../Parsing/PatternParser.cs | 327 +++--- .../Parsing/UnbalancedBracesException.cs | 125 ++- .../StringBuilderPool.cs | 35 +- 83 files changed, 5544 insertions(+), 5610 deletions(-) diff --git a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/Condition.cs b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/Condition.cs index d714868..b3c0bea 100644 --- a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/Condition.cs +++ b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/Condition.cs @@ -1,22 +1,21 @@ using System.Collections.Generic; using System.Diagnostics; -namespace Jeffijoe.MessageFormat.MetadataGenerator.Plural.Parsing.AST +namespace Jeffijoe.MessageFormat.MetadataGenerator.Plural.Parsing.AST; + +[DebuggerDisplay("{{RuleDescription}}")] +public class Condition { - [DebuggerDisplay("{{RuleDescription}}")] - public class Condition + public Condition(string count, string ruleDescription, IReadOnlyList orConditions) { - public Condition(string count, string ruleDescription, IReadOnlyList orConditions) - { - Count = count; - RuleDescription = ruleDescription; - OrConditions = orConditions; - } + Count = count; + RuleDescription = ruleDescription; + OrConditions = orConditions; + } - public string Count { get; } + public string Count { get; } - public string RuleDescription { get; } + public string RuleDescription { get; } - public IReadOnlyList OrConditions { get; } - } -} + public IReadOnlyList OrConditions { get; } +} \ No newline at end of file diff --git a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/ILeftOperand.cs b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/ILeftOperand.cs index b37db90..401de53 100644 --- a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/ILeftOperand.cs +++ b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/ILeftOperand.cs @@ -1,6 +1,5 @@ -namespace Jeffijoe.MessageFormat.MetadataGenerator.Plural.Parsing.AST +namespace Jeffijoe.MessageFormat.MetadataGenerator.Plural.Parsing.AST; + +public interface ILeftOperand { - public interface ILeftOperand - { - } } \ No newline at end of file diff --git a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/IRightOperand.cs b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/IRightOperand.cs index 96eeffe..206fa66 100644 --- a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/IRightOperand.cs +++ b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/IRightOperand.cs @@ -1,6 +1,5 @@ -namespace Jeffijoe.MessageFormat.MetadataGenerator.Plural.Parsing.AST +namespace Jeffijoe.MessageFormat.MetadataGenerator.Plural.Parsing.AST; + +public interface IRightOperand { - public interface IRightOperand - { - } } \ No newline at end of file diff --git a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/LeftOperand.cs b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/LeftOperand.cs index f116bc0..f756372 100644 --- a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/LeftOperand.cs +++ b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/LeftOperand.cs @@ -1,27 +1,26 @@ -namespace Jeffijoe.MessageFormat.MetadataGenerator.Plural.Parsing.AST +namespace Jeffijoe.MessageFormat.MetadataGenerator.Plural.Parsing.AST; + +public class ModuloOperand : ILeftOperand { - public class ModuloOperand : ILeftOperand + public ModuloOperand(OperandSymbol operandSymbol, int modValue) { - public ModuloOperand(OperandSymbol operandSymbol, int modValue) - { - Operand = operandSymbol; - ModValue = modValue; - } + Operand = operandSymbol; + ModValue = modValue; + } - public OperandSymbol Operand { get; } - public int ModValue { get; } + public OperandSymbol Operand { get; } + public int ModValue { get; } - public override bool Equals(object? obj) - { - if (obj is ModuloOperand op) - return op.Operand == Operand && op.ModValue == ModValue; + public override bool Equals(object? obj) + { + if (obj is ModuloOperand op) + return op.Operand == Operand && op.ModValue == ModValue; - return this == obj; - } + return this == obj; + } - public override int GetHashCode() - { - return Operand.GetHashCode() + ModValue.GetHashCode(); - } + public override int GetHashCode() + { + return Operand.GetHashCode() + ModValue.GetHashCode(); } -} +} \ No newline at end of file diff --git a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/NumberOperand.cs b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/NumberOperand.cs index 986a322..0b9fe2f 100644 --- a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/NumberOperand.cs +++ b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/NumberOperand.cs @@ -1,25 +1,24 @@ -namespace Jeffijoe.MessageFormat.MetadataGenerator.Plural.Parsing.AST +namespace Jeffijoe.MessageFormat.MetadataGenerator.Plural.Parsing.AST; + +public class NumberOperand : IRightOperand { - public class NumberOperand : IRightOperand + public NumberOperand(int number) { - public NumberOperand(int number) - { - Number = number; - } + Number = number; + } - public int Number { get; } + public int Number { get; } - public override bool Equals(object? obj) - { - if (obj is NumberOperand n) - return n.Number == Number; + public override bool Equals(object? obj) + { + if (obj is NumberOperand n) + return n.Number == Number; - return this == obj; - } + return this == obj; + } - public override int GetHashCode() - { - return Number.GetHashCode(); - } + public override int GetHashCode() + { + return Number.GetHashCode(); } } \ No newline at end of file diff --git a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/OperandSymbol.cs b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/OperandSymbol.cs index 21f806a..c07856b 100644 --- a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/OperandSymbol.cs +++ b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/OperandSymbol.cs @@ -1,45 +1,44 @@ -namespace Jeffijoe.MessageFormat.MetadataGenerator.Plural.Parsing.AST +namespace Jeffijoe.MessageFormat.MetadataGenerator.Plural.Parsing.AST; + +public enum OperandSymbol { - public enum OperandSymbol - { - /// - /// n - absolute value of the source number. - /// - AbsoluteValue, - - /// - /// i - integer digits of n. - /// - IntegerDigits, - - /// - /// v - number of visible fraction digits in n, with trailing zeros. - /// - VisibleFractionDigitNumber, - - /// - /// w - number of visible fraction digits in n, without trailing zeros. - /// - VisibleFractionDigitNumberWithoutTrailingZeroes, - - /// - /// f - number of visible fraction digits in n, with trailing zeros. - /// - VisibleFractionDigits, - - /// - /// t - visible fraction digits in n, without trailing zeros. - /// - VisibleFractionDigitsWithoutTrailingZeroes, - - /// - /// c - compact decimal exponent value: exponent of the power of 10 used in compact decimal formatting. - /// - ExponentC, - - /// - /// e - currently, synonym for ‘c’. however, may be redefined in the future. - /// - ExponentE, - } -} + /// + /// n - absolute value of the source number. + /// + AbsoluteValue, + + /// + /// i - integer digits of n. + /// + IntegerDigits, + + /// + /// v - number of visible fraction digits in n, with trailing zeros. + /// + VisibleFractionDigitNumber, + + /// + /// w - number of visible fraction digits in n, without trailing zeros. + /// + VisibleFractionDigitNumberWithoutTrailingZeroes, + + /// + /// f - number of visible fraction digits in n, with trailing zeros. + /// + VisibleFractionDigits, + + /// + /// t - visible fraction digits in n, without trailing zeros. + /// + VisibleFractionDigitsWithoutTrailingZeroes, + + /// + /// c - compact decimal exponent value: exponent of the power of 10 used in compact decimal formatting. + /// + ExponentC, + + /// + /// e - currently, synonym for ‘c’. however, may be redefined in the future. + /// + ExponentE, +} \ No newline at end of file diff --git a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/Operation.cs b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/Operation.cs index 9dff3cb..4c00379 100644 --- a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/Operation.cs +++ b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/Operation.cs @@ -1,20 +1,19 @@ using System.Collections.Generic; -namespace Jeffijoe.MessageFormat.MetadataGenerator.Plural.Parsing.AST +namespace Jeffijoe.MessageFormat.MetadataGenerator.Plural.Parsing.AST; + +public class Operation { - public class Operation + public Operation(ILeftOperand operandLeft, Relation relation, IReadOnlyList operandRight) { - public Operation(ILeftOperand operandLeft, Relation relation, IReadOnlyList operandRight) - { - OperandLeft = operandLeft; - Relation = relation; - OperandRight = operandRight; - } + OperandLeft = operandLeft; + Relation = relation; + OperandRight = operandRight; + } - public ILeftOperand OperandLeft { get; } + public ILeftOperand OperandLeft { get; } - public Relation Relation { get; } + public Relation Relation { get; } - public IReadOnlyList OperandRight { get; } - } -} + public IReadOnlyList OperandRight { get; } +} \ No newline at end of file diff --git a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/OrCondition.cs b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/OrCondition.cs index 435f455..379e08d 100644 --- a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/OrCondition.cs +++ b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/OrCondition.cs @@ -1,14 +1,13 @@ using System.Collections.Generic; -namespace Jeffijoe.MessageFormat.MetadataGenerator.Plural.Parsing.AST +namespace Jeffijoe.MessageFormat.MetadataGenerator.Plural.Parsing.AST; + +public class OrCondition { - public class OrCondition + public OrCondition(IReadOnlyList andConditions) { - public OrCondition(IReadOnlyList andConditions) - { - AndConditions = andConditions; - } - - public IReadOnlyList AndConditions { get; } + AndConditions = andConditions; } -} + + public IReadOnlyList AndConditions { get; } +} \ No newline at end of file diff --git a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/PluralRule.cs b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/PluralRule.cs index 97461f1..54ba99e 100644 --- a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/PluralRule.cs +++ b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/PluralRule.cs @@ -1,17 +1,16 @@ using System.Collections.Generic; -namespace Jeffijoe.MessageFormat.MetadataGenerator.Plural.Parsing.AST +namespace Jeffijoe.MessageFormat.MetadataGenerator.Plural.Parsing.AST; + +public class PluralRule { - public class PluralRule + public PluralRule(string[] locales, IReadOnlyList conditions) { - public PluralRule(string[] locales, IReadOnlyList conditions) - { - Locales = locales; - Conditions = conditions; - } + Locales = locales; + Conditions = conditions; + } - public string[] Locales { get; } + public string[] Locales { get; } - public IReadOnlyList Conditions { get; } - } -} + public IReadOnlyList Conditions { get; } +} \ No newline at end of file diff --git a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/RangeOperand.cs b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/RangeOperand.cs index 50ca1ec..48c5ca5 100644 --- a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/RangeOperand.cs +++ b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/RangeOperand.cs @@ -1,27 +1,26 @@ -namespace Jeffijoe.MessageFormat.MetadataGenerator.Plural.Parsing.AST +namespace Jeffijoe.MessageFormat.MetadataGenerator.Plural.Parsing.AST; + +public class RangeOperand : IRightOperand { - public class RangeOperand : IRightOperand + public RangeOperand(int start, int end) { - public RangeOperand(int start, int end) - { - Start = start; - End = end; - } + Start = start; + End = end; + } - public int Start { get; } - public int End { get; } + public int Start { get; } + public int End { get; } - public override bool Equals(object? obj) - { - if (obj is RangeOperand n) - return n.Start == Start && n.End == End; + public override bool Equals(object? obj) + { + if (obj is RangeOperand n) + return n.Start == Start && n.End == End; - return this == obj; - } + return this == obj; + } - public override int GetHashCode() - { - return Start.GetHashCode() + End.GetHashCode(); - } + public override int GetHashCode() + { + return Start.GetHashCode() + End.GetHashCode(); } -} +} \ No newline at end of file diff --git a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/Relation.cs b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/Relation.cs index fb92b51..435501c 100644 --- a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/Relation.cs +++ b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/Relation.cs @@ -1,7 +1,6 @@ -namespace Jeffijoe.MessageFormat.MetadataGenerator.Plural.Parsing.AST +namespace Jeffijoe.MessageFormat.MetadataGenerator.Plural.Parsing.AST; + +public enum Relation { - public enum Relation - { - Equals, NotEquals - } -} + Equals, NotEquals +} \ No newline at end of file diff --git a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/VariableOperand.cs b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/VariableOperand.cs index ec3ee7c..c6a883f 100644 --- a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/VariableOperand.cs +++ b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/VariableOperand.cs @@ -1,25 +1,24 @@ -namespace Jeffijoe.MessageFormat.MetadataGenerator.Plural.Parsing.AST +namespace Jeffijoe.MessageFormat.MetadataGenerator.Plural.Parsing.AST; + +public class VariableOperand : ILeftOperand { - public class VariableOperand : ILeftOperand + public VariableOperand(OperandSymbol operand) { - public VariableOperand(OperandSymbol operand) - { - Operand = operand; - } + Operand = operand; + } - public OperandSymbol Operand { get; } + public OperandSymbol Operand { get; } - public override bool Equals(object? obj) - { - if (obj is VariableOperand op) - return op.Operand == Operand; + public override bool Equals(object? obj) + { + if (obj is VariableOperand op) + return op.Operand == Operand; - return this == obj; - } + return this == obj; + } - public override int GetHashCode() - { - return Operand.GetHashCode(); - } + public override int GetHashCode() + { + return Operand.GetHashCode(); } } \ No newline at end of file diff --git a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/InvalidCharacterException.cs b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/InvalidCharacterException.cs index d9d9f0c..396efb0 100644 --- a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/InvalidCharacterException.cs +++ b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/InvalidCharacterException.cs @@ -1,11 +1,10 @@ using System; -namespace Jeffijoe.MessageFormat.MetadataGenerator.Plural.Parsing +namespace Jeffijoe.MessageFormat.MetadataGenerator.Plural.Parsing; + +public class InvalidCharacterException : FormatException { - public class InvalidCharacterException : FormatException + public InvalidCharacterException(char character) : base($"Invalid format, do not recognise character '{character}'") { - public InvalidCharacterException(char character) : base($"Invalid format, do not recognise character '{character}'") - { - } } -} +} \ No newline at end of file diff --git a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/PluralParser.cs b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/PluralParser.cs index ddbed96..38f02cc 100644 --- a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/PluralParser.cs +++ b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/PluralParser.cs @@ -3,74 +3,73 @@ using System.Linq; using Jeffijoe.MessageFormat.MetadataGenerator.Plural.Parsing.AST; -namespace Jeffijoe.MessageFormat.MetadataGenerator.Plural.Parsing +namespace Jeffijoe.MessageFormat.MetadataGenerator.Plural.Parsing; + +public class PluralParser { - public class PluralParser - { - private readonly XmlDocument _rulesDocument; - private readonly HashSet _excludedLocales; + private readonly XmlDocument _rulesDocument; + private readonly HashSet _excludedLocales; - public PluralParser(XmlDocument rulesDocument, string[] excludedLocales) - { - _rulesDocument = rulesDocument; - _excludedLocales = new HashSet(excludedLocales); - } + public PluralParser(XmlDocument rulesDocument, string[] excludedLocales) + { + _rulesDocument = rulesDocument; + _excludedLocales = new HashSet(excludedLocales); + } - public IEnumerable Parse() - { - var root = _rulesDocument.DocumentElement!; + public IEnumerable Parse() + { + var root = _rulesDocument.DocumentElement!; - foreach(XmlNode dataElement in root.ChildNodes) + foreach(XmlNode dataElement in root.ChildNodes) + { + if (dataElement.Name != "plurals") { - if (dataElement.Name != "plurals") - { - continue; - } + continue; + } - foreach (XmlNode rule in dataElement.ChildNodes) + foreach (XmlNode rule in dataElement.ChildNodes) + { + if(rule.Name == "pluralRules") { - if(rule.Name == "pluralRules") + var parsed = ParseSingleRule(rule); + if (parsed != null) { - var parsed = ParseSingleRule(rule); - if (parsed != null) - { - yield return parsed; - } + yield return parsed; } } } } + } - private PluralRule? ParseSingleRule(XmlNode rule) - { - var locales = rule.Attributes!["locales"]!.Value.Split(' '); + private PluralRule? ParseSingleRule(XmlNode rule) + { + var locales = rule.Attributes!["locales"]!.Value.Split(' '); - if (locales.All(l => _excludedLocales.Contains(l))) - { - return null; - } + if (locales.All(l => _excludedLocales.Contains(l))) + { + return null; + } - var conditions = new List(); - foreach (XmlNode condition in rule.ChildNodes) + var conditions = new List(); + foreach (XmlNode condition in rule.ChildNodes) + { + if (condition.Name == "pluralRule") { - if (condition.Name == "pluralRule") - { - var count = condition.Attributes!["count"]!.Value; + var count = condition.Attributes!["count"]!.Value; - // Ignore other, because other is basically everything else except for the conditions present - if (count == "other") - continue; + // Ignore other, because other is basically everything else except for the conditions present + if (count == "other") + continue; - var ruleContent = condition.InnerText; + var ruleContent = condition.InnerText; - var ruleParser = new RuleParser(ruleContent); - var orConditions = ruleParser.ParseRuleContent(); + var ruleParser = new RuleParser(ruleContent); + var orConditions = ruleParser.ParseRuleContent(); - conditions.Add(new Condition(count, ruleContent, orConditions)); - } + conditions.Add(new Condition(count, ruleContent, orConditions)); } - - return new PluralRule(locales, conditions); } + + return new PluralRule(locales, conditions); } -} +} \ No newline at end of file diff --git a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/RuleParser.cs b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/RuleParser.cs index 44d4493..722ec07 100644 --- a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/RuleParser.cs +++ b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/RuleParser.cs @@ -2,253 +2,252 @@ using System.Collections.Generic; using Jeffijoe.MessageFormat.MetadataGenerator.Plural.Parsing.AST; -namespace Jeffijoe.MessageFormat.MetadataGenerator.Plural.Parsing +namespace Jeffijoe.MessageFormat.MetadataGenerator.Plural.Parsing; + +public class RuleParser { - public class RuleParser + private readonly string _ruleText; + private int _position; + + public RuleParser(string ruleText) { - private readonly string _ruleText; - private int _position; + _ruleText = ruleText; + } - public RuleParser(string ruleText) - { - _ruleText = ruleText; - } + public IReadOnlyList ParseRuleContent() + { + var conditions = new List(); - public IReadOnlyList ParseRuleContent() + while (!IsEnd) { - var conditions = new List(); - - while (!IsEnd) + if (PeekCurrentChar == '@') { - if (PeekCurrentChar == '@') - { - return conditions; - } + return conditions; + } - var condition = ParseOrCondition(); - conditions.Add(condition); + var condition = ParseOrCondition(); + conditions.Add(condition); - AdvanceWhitespace(); + AdvanceWhitespace(); - if (IsEnd) - { - return conditions; - } + if (IsEnd) + { + return conditions; + } - var character = ConsumeChar(); + var character = ConsumeChar(); - // This is where the samples start, we don't care about any of those. - if (character == '@') - { - return conditions; - } - - // We expect the next token to be "or" - var characterNext = ConsumeChar(); - if (character == 'o' && characterNext == 'r') - { - continue; - } + // This is where the samples start, we don't care about any of those. + if (character == '@') + { + return conditions; + } - throw new InvalidCharacterException(character); + // We expect the next token to be "or" + var characterNext = ConsumeChar(); + if (character == 'o' && characterNext == 'r') + { + continue; } - return conditions; + throw new InvalidCharacterException(character); } - private static readonly char NullCharacter = '\0'; + return conditions; + } + + private static readonly char NullCharacter = '\0'; - private char PeekCurrentChar => - _position < _ruleText.Length + private char PeekCurrentChar => + _position < _ruleText.Length ? _ruleText[_position] : NullCharacter; - private char PeekNextChar => - _position + 1 < _ruleText.Length + private char PeekNextChar => + _position + 1 < _ruleText.Length ? _ruleText[_position + 1] : NullCharacter; - private char PeekAt(int delta) - { - if (_position + delta >= _ruleText.Length) - return NullCharacter; + private char PeekAt(int delta) + { + if (_position + delta >= _ruleText.Length) + return NullCharacter; - return _ruleText[_position + delta]; - } + return _ruleText[_position + delta]; + } - private ReadOnlySpan ConsumeCharacters(int count) + private ReadOnlySpan ConsumeCharacters(int count) + { + if (_position + count > _ruleText.Length) { - if (_position + count > _ruleText.Length) - { - var characters = _ruleText.AsSpan(_position, _ruleText.Length - _position); - _position = _ruleText.Length; - return characters; - } - else - { - var characters = _ruleText.AsSpan(_position, count); - _position += count; - return characters; - } + var characters = _ruleText.AsSpan(_position, _ruleText.Length - _position); + _position = _ruleText.Length; + return characters; } - - private char ConsumeChar() + else { - if (IsEnd) - return NullCharacter; - - var character = PeekCurrentChar; - _position++; - return character; + var characters = _ruleText.AsSpan(_position, count); + _position += count; + return characters; } + } + + private char ConsumeChar() + { + if (IsEnd) + return NullCharacter; - private bool IsEnd => _position >= _ruleText.Length; + var character = PeekCurrentChar; + _position++; + return character; + } - private void AdvanceWhitespace() + private bool IsEnd => _position >= _ruleText.Length; + + private void AdvanceWhitespace() + { + while (!IsEnd && char.IsWhiteSpace(PeekCurrentChar)) { - while (!IsEnd && char.IsWhiteSpace(PeekCurrentChar)) - { - ConsumeChar(); - } + ConsumeChar(); } + } - private ILeftOperand ParseLeftOperand() + private ILeftOperand ParseLeftOperand() + { + var operandSymbol = ConsumeChar() switch { - var operandSymbol = ConsumeChar() switch - { - 'n' => OperandSymbol.AbsoluteValue, - 'i' => OperandSymbol.IntegerDigits, - 'v' => OperandSymbol.VisibleFractionDigitNumber, - 'w' => OperandSymbol.VisibleFractionDigitNumberWithoutTrailingZeroes, - 'f' => OperandSymbol.VisibleFractionDigits, - 't' => OperandSymbol.VisibleFractionDigitsWithoutTrailingZeroes, - 'c' => OperandSymbol.ExponentC, - 'e' => OperandSymbol.ExponentE, - var otherCharacter => throw new InvalidCharacterException(otherCharacter) - }; - + 'n' => OperandSymbol.AbsoluteValue, + 'i' => OperandSymbol.IntegerDigits, + 'v' => OperandSymbol.VisibleFractionDigitNumber, + 'w' => OperandSymbol.VisibleFractionDigitNumberWithoutTrailingZeroes, + 'f' => OperandSymbol.VisibleFractionDigits, + 't' => OperandSymbol.VisibleFractionDigitsWithoutTrailingZeroes, + 'c' => OperandSymbol.ExponentC, + 'e' => OperandSymbol.ExponentE, + var otherCharacter => throw new InvalidCharacterException(otherCharacter) + }; + + AdvanceWhitespace(); + + if(PeekCurrentChar == '%') + { + ConsumeChar(); AdvanceWhitespace(); - if(PeekCurrentChar == '%') - { - ConsumeChar(); - AdvanceWhitespace(); - - var number = ParseNumber(); - - return new ModuloOperand(operandSymbol, number); - } + var number = ParseNumber(); - return new VariableOperand(operandSymbol); + return new ModuloOperand(operandSymbol, number); } - private Operation ParseAndCondition() - { - AdvanceWhitespace(); - var leftOperand = ParseLeftOperand(); + return new VariableOperand(operandSymbol); + } + + private Operation ParseAndCondition() + { + AdvanceWhitespace(); + var leftOperand = ParseLeftOperand(); - AdvanceWhitespace(); - var firstRelationCharacter = ConsumeChar(); - var relation = firstRelationCharacter switch - { - '=' => Relation.Equals, - '!' when ConsumeChar() == '=' - => Relation.NotEquals, - var otherCharacter => throw new InvalidCharacterException(otherCharacter) - }; + AdvanceWhitespace(); + var firstRelationCharacter = ConsumeChar(); + var relation = firstRelationCharacter switch + { + '=' => Relation.Equals, + '!' when ConsumeChar() == '=' + => Relation.NotEquals, + var otherCharacter => throw new InvalidCharacterException(otherCharacter) + }; + + AdvanceWhitespace(); + var rightOperand = ParseRightOperand(); + return new Operation(leftOperand, relation, rightOperand); + } - AdvanceWhitespace(); - var rightOperand = ParseRightOperand(); - return new Operation(leftOperand, relation, rightOperand); - } + private IReadOnlyList ParseRightOperand() + { + var numbers = new List(); - private IReadOnlyList ParseRightOperand() + while (!IsEnd) { - var numbers = new List(); + AdvanceWhitespace(); - while (!IsEnd) + var number = ParseNumber(); + if (PeekCurrentChar == '.') { - AdvanceWhitespace(); - - var number = ParseNumber(); - if (PeekCurrentChar == '.') + if (PeekNextChar == '.') { - if (PeekNextChar == '.') - { - ConsumeCharacters(2); - AdvanceWhitespace(); - - var nextNumber = ParseNumber(); - numbers.Add(new RangeOperand(number, nextNumber)); - } - else - { - throw new InvalidCharacterException(PeekCurrentChar); - } + ConsumeCharacters(2); + AdvanceWhitespace(); + + var nextNumber = ParseNumber(); + numbers.Add(new RangeOperand(number, nextNumber)); } else { - numbers.Add(new NumberOperand(number)); - } - - if (PeekCurrentChar == ',') - { - ConsumeChar(); - AdvanceWhitespace(); - continue; + throw new InvalidCharacterException(PeekCurrentChar); } + } + else + { + numbers.Add(new NumberOperand(number)); + } - break; + if (PeekCurrentChar == ',') + { + ConsumeChar(); + AdvanceWhitespace(); + continue; } - return numbers; + break; } - private OrCondition ParseOrCondition() - { - var andWordSpan = "and".AsSpan(); + return numbers; + } - var andConditions = new List(); - while (!IsEnd) - { - var operation = ParseAndCondition(); - andConditions.Add(operation); + private OrCondition ParseOrCondition() + { + var andWordSpan = "and".AsSpan(); - AdvanceWhitespace(); + var andConditions = new List(); + while (!IsEnd) + { + var operation = ParseAndCondition(); + andConditions.Add(operation); - if (PeekCurrentChar == 'a') - { - var andWord = ConsumeCharacters(3); + AdvanceWhitespace(); + if (PeekCurrentChar == 'a') + { + var andWord = ConsumeCharacters(3); - if (andWord.SequenceEqual(andWordSpan)) - { - continue; - } - throw new InvalidCharacterException(andWord[0]); + if (andWord.SequenceEqual(andWordSpan)) + { + continue; } - return new OrCondition(andConditions); + throw new InvalidCharacterException(andWord[0]); } return new OrCondition(andConditions); } - private int ParseNumber() + return new OrCondition(andConditions); + } + + private int ParseNumber() + { + int numbersCount = 0; + while (!IsEnd && char.IsNumber(PeekAt(numbersCount))) { - int numbersCount = 0; - while (!IsEnd && char.IsNumber(PeekAt(numbersCount))) - { - numbersCount++; - } + numbersCount++; + } - var numberSpan = ConsumeCharacters(numbersCount); + var numberSpan = ConsumeCharacters(numbersCount); - var number = int.Parse(numberSpan.ToString()); + var number = int.Parse(numberSpan.ToString()); - return number; - } + return number; } -} +} \ No newline at end of file diff --git a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/PluralLanguagesGenerator.cs b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/PluralLanguagesGenerator.cs index 0d4fe2a..9d1e439 100644 --- a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/PluralLanguagesGenerator.cs +++ b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/PluralLanguagesGenerator.cs @@ -10,51 +10,50 @@ using System.Linq; using System.Xml; -namespace Jeffijoe.MessageFormat.MetadataGenerator.Plural +namespace Jeffijoe.MessageFormat.MetadataGenerator.Plural; + +[Generator] +public class PluralLanguagesGenerator : ISourceGenerator { - [Generator] - public class PluralLanguagesGenerator : ISourceGenerator + public void Execute(GeneratorExecutionContext context) { - public void Execute(GeneratorExecutionContext context) - { - var excludeLocales = ReadExcludeLocales(context); - var rules = GetRules(excludeLocales); - var generator = new PluralRulesMetadataGenerator(rules); - var sourceCode = generator.GenerateClass(); + var excludeLocales = ReadExcludeLocales(context); + var rules = GetRules(excludeLocales); + var generator = new PluralRulesMetadataGenerator(rules); + var sourceCode = generator.GenerateClass(); - context.AddSource("PluralRulesMetadata.Generated.cs", sourceCode); - } + context.AddSource("PluralRulesMetadata.Generated.cs", sourceCode); + } - private string[] ReadExcludeLocales(GeneratorExecutionContext context) + private string[] ReadExcludeLocales(GeneratorExecutionContext context) + { + if(context.AnalyzerConfigOptions.GlobalOptions.TryGetValue($"build_property.PluralLanguagesMetadataExcludeLocales", out var value)) { - if(context.AnalyzerConfigOptions.GlobalOptions.TryGetValue($"build_property.PluralLanguagesMetadataExcludeLocales", out var value)) - { - var locales = value.Trim().Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); - return locales; - } - - return Array.Empty(); + var locales = value.Trim().Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); + return locales; } - private IReadOnlyList GetRules(string[] excludedLocales) - { - using var rulesStream = GetRulesContentStream(); - var xml = new XmlDocument(); - xml.Load(rulesStream); + return Array.Empty(); + } - var parser = new PluralParser(xml, excludedLocales); - return parser.Parse().ToArray(); - } + private IReadOnlyList GetRules(string[] excludedLocales) + { + using var rulesStream = GetRulesContentStream(); + var xml = new XmlDocument(); + xml.Load(rulesStream); + var parser = new PluralParser(xml, excludedLocales); + return parser.Parse().ToArray(); + } - private Stream GetRulesContentStream() - { - return typeof(PluralLanguagesGenerator).Assembly.GetManifestResourceStream("Jeffijoe.MessageFormat.MetadataGenerator.data.plurals.xml")!; - } - public void Initialize(GeneratorInitializationContext context) - { + private Stream GetRulesContentStream() + { + return typeof(PluralLanguagesGenerator).Assembly.GetManifestResourceStream("Jeffijoe.MessageFormat.MetadataGenerator.data.plurals.xml")!; + } + + public void Initialize(GeneratorInitializationContext context) + { - } } -} +} \ No newline at end of file diff --git a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/SourceGeneration/PluralRulesMetadataGenerator.cs b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/SourceGeneration/PluralRulesMetadataGenerator.cs index 6c33015..e797b99 100644 --- a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/SourceGeneration/PluralRulesMetadataGenerator.cs +++ b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/SourceGeneration/PluralRulesMetadataGenerator.cs @@ -2,100 +2,99 @@ using System.Text; using Jeffijoe.MessageFormat.MetadataGenerator.Plural.Parsing.AST; -namespace Jeffijoe.MessageFormat.MetadataGenerator.Plural.SourceGeneration -{ - public class PluralRulesMetadataGenerator - { - private readonly IReadOnlyList _rules; - private readonly StringBuilder _sb; - private int _indent; - - public PluralRulesMetadataGenerator(IReadOnlyList rules) - { - _rules = rules; - _sb = new StringBuilder(); - } +namespace Jeffijoe.MessageFormat.MetadataGenerator.Plural.SourceGeneration; - public string GenerateClass() - { - WriteLine("using System;"); - WriteLine("using System.Collections.Generic;"); - - WriteLine("namespace Jeffijoe.MessageFormat.Formatting.Formatters"); - WriteLine("{"); - AddIndent(); +public class PluralRulesMetadataGenerator +{ + private readonly IReadOnlyList _rules; + private readonly StringBuilder _sb; + private int _indent; - WriteLine("internal static partial class PluralRulesMetadata"); - WriteLine("{"); - AddIndent(); + public PluralRulesMetadataGenerator(IReadOnlyList rules) + { + _rules = rules; + _sb = new StringBuilder(); + } - for (var ruleIdx = 0; ruleIdx < _rules.Count; ruleIdx++) - { - var rule = _rules[ruleIdx]; + public string GenerateClass() + { + WriteLine("using System;"); + WriteLine("using System.Collections.Generic;"); - var ruleGenerator = new RuleGenerator(rule); + WriteLine("namespace Jeffijoe.MessageFormat.Formatting.Formatters"); + WriteLine("{"); + AddIndent(); - foreach(var locale in rule.Locales) - { - WriteLine($"public static string Locale_{locale.ToUpper()}(PluralContext context) => Rule{ruleIdx}(context);"); - WriteLine(string.Empty); - } + WriteLine("internal static partial class PluralRulesMetadata"); + WriteLine("{"); + AddIndent(); - WriteLine($"private static string Rule{ruleIdx}(PluralContext context)"); - WriteLine("{"); - AddIndent(); + for (var ruleIdx = 0; ruleIdx < _rules.Count; ruleIdx++) + { + var rule = _rules[ruleIdx]; - ruleGenerator.WriteTo(_sb, _indent); + var ruleGenerator = new RuleGenerator(rule); - DecreaseIndent(); - WriteLine("}"); + foreach(var locale in rule.Locales) + { + WriteLine($"public static string Locale_{locale.ToUpper()}(PluralContext context) => Rule{ruleIdx}(context);"); WriteLine(string.Empty); } - WriteLine("private static readonly Dictionary Pluralizers = new Dictionary()"); + WriteLine($"private static string Rule{ruleIdx}(PluralContext context)"); WriteLine("{"); AddIndent(); - for (int ruleIdx = 0; ruleIdx < _rules.Count; ruleIdx++) - { - PluralRule rule = _rules[ruleIdx]; - foreach (var locale in rule.Locales) - { - WriteLine($"{{\"{locale}\", Rule{ruleIdx}}},"); - } + ruleGenerator.WriteTo(_sb, _indent); - WriteLine(string.Empty); + DecreaseIndent(); + WriteLine("}"); + WriteLine(string.Empty); + } + + WriteLine("private static readonly Dictionary Pluralizers = new Dictionary()"); + WriteLine("{"); + AddIndent(); + + for (int ruleIdx = 0; ruleIdx < _rules.Count; ruleIdx++) + { + PluralRule rule = _rules[ruleIdx]; + foreach (var locale in rule.Locales) + { + WriteLine($"{{\"{locale}\", Rule{ruleIdx}}},"); } - DecreaseIndent(); - WriteLine("};"); WriteLine(string.Empty); + } - WriteLine("public static partial bool TryGetRuleByLocale(string locale, out ContextPluralizer contextPluralizer)"); - WriteLine("{"); - AddIndent(); + DecreaseIndent(); + WriteLine("};"); + WriteLine(string.Empty); - WriteLine("return Pluralizers.TryGetValue(locale, out contextPluralizer);"); + WriteLine("public static partial bool TryGetRuleByLocale(string locale, out ContextPluralizer contextPluralizer)"); + WriteLine("{"); + AddIndent(); - DecreaseIndent(); - WriteLine("}"); + WriteLine("return Pluralizers.TryGetValue(locale, out contextPluralizer);"); - DecreaseIndent(); - WriteLine("}"); + DecreaseIndent(); + WriteLine("}"); - DecreaseIndent(); - WriteLine("}"); + DecreaseIndent(); + WriteLine("}"); - return _sb.ToString(); - } + DecreaseIndent(); + WriteLine("}"); - private void AddIndent() => _indent += 4; - private void DecreaseIndent() => _indent -= 4; + return _sb.ToString(); + } - private void WriteLine(string line) - { - _sb.Append(' ', _indent); - _sb.AppendLine(line); - } + private void AddIndent() => _indent += 4; + private void DecreaseIndent() => _indent -= 4; + + private void WriteLine(string line) + { + _sb.Append(' ', _indent); + _sb.AppendLine(line); } -} +} \ No newline at end of file diff --git a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/SourceGeneration/RuleGenerator.cs b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/SourceGeneration/RuleGenerator.cs index 0087ea6..88dcefa 100644 --- a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/SourceGeneration/RuleGenerator.cs +++ b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/SourceGeneration/RuleGenerator.cs @@ -2,134 +2,133 @@ using System.Text; using Jeffijoe.MessageFormat.MetadataGenerator.Plural.Parsing.AST; -namespace Jeffijoe.MessageFormat.MetadataGenerator.Plural.SourceGeneration +namespace Jeffijoe.MessageFormat.MetadataGenerator.Plural.SourceGeneration; + +public class RuleGenerator { - public class RuleGenerator + private readonly PluralRule _rule; + private int _innerIndent; + + public RuleGenerator(PluralRule rule) { - private readonly PluralRule _rule; - private int _innerIndent; + _rule = rule; + _innerIndent = 0; + } - public RuleGenerator(PluralRule rule) + public void WriteTo(StringBuilder builder, int indent) + { + foreach(var condition in _rule.Conditions) { - _rule = rule; - _innerIndent = 0; + WriteNext(condition, builder, indent); } - public void WriteTo(StringBuilder builder, int indent) - { - foreach(var condition in _rule.Conditions) - { - WriteNext(condition, builder, indent); - } + WriteOther(builder, indent); + } - WriteOther(builder, indent); - } + private void WriteOther(StringBuilder builder, int indent) + { + WriteLine(builder, "return \"other\";", indent); + } - private void WriteOther(StringBuilder builder, int indent) + private void WriteNext(Condition condition, StringBuilder builder, int indent) + { + if(condition.OrConditions.Count > 0) { - WriteLine(builder, "return \"other\";", indent); - } + builder.Append(' ', _innerIndent + indent); + builder.Append("if ("); - private void WriteNext(Condition condition, StringBuilder builder, int indent) - { - if(condition.OrConditions.Count > 0) + for (int orIdx = 0; orIdx < condition.OrConditions.Count; orIdx++) { - builder.Append(' ', _innerIndent + indent); - builder.Append("if ("); - - for (int orIdx = 0; orIdx < condition.OrConditions.Count; orIdx++) - { - OrCondition orCondition = condition.OrConditions[orIdx]; - var orIsLast = orIdx == condition.OrConditions.Count - 1; + OrCondition orCondition = condition.OrConditions[orIdx]; + var orIsLast = orIdx == condition.OrConditions.Count - 1; - WriteOrCondition(builder, orCondition); + WriteOrCondition(builder, orCondition); - if (!orIsLast) - { - builder.Append(" || "); - } + if (!orIsLast) + { + builder.Append(" || "); } + } - builder.AppendLine(")"); + builder.AppendLine(")"); - _innerIndent += 4; - WriteLine(builder, $"return \"{condition.Count}\";", indent); - _innerIndent -= 4; + _innerIndent += 4; + WriteLine(builder, $"return \"{condition.Count}\";", indent); + _innerIndent -= 4; - WriteLine(builder, string.Empty, indent); - } - else - { - throw new InvalidOperationException("Expected to have at least one or condition, but got none"); - } + WriteLine(builder, string.Empty, indent); + } + else + { + throw new InvalidOperationException("Expected to have at least one or condition, but got none"); } + } - private void WriteOrCondition(StringBuilder builder, OrCondition orCondition) + private void WriteOrCondition(StringBuilder builder, OrCondition orCondition) + { + for (int andIdx = 0; andIdx < orCondition.AndConditions.Count; andIdx++) { - for (int andIdx = 0; andIdx < orCondition.AndConditions.Count; andIdx++) + var andIsLast = andIdx == orCondition.AndConditions.Count - 1; + Operation andCondition = orCondition.AndConditions[andIdx]; + builder.Append('('); + + for (int innerOrIdx = 0; innerOrIdx < andCondition.OperandRight.Count; innerOrIdx++) { - var andIsLast = andIdx == orCondition.AndConditions.Count - 1; - Operation andCondition = orCondition.AndConditions[andIdx]; - builder.Append('('); + var isLast = innerOrIdx == andCondition.OperandRight.Count - 1; - for (int innerOrIdx = 0; innerOrIdx < andCondition.OperandRight.Count; innerOrIdx++) + var leftVariable = andCondition.OperandLeft switch { - var isLast = innerOrIdx == andCondition.OperandRight.Count - 1; - - var leftVariable = andCondition.OperandLeft switch - { - VariableOperand op => $"context.{OperandToVariable(op.Operand)}", - ModuloOperand op => $"context.{OperandToVariable(op.Operand)} % {op.ModValue}", - var otherOp => throw new InvalidOperationException($"Unknown operation {otherOp.GetType()}") - }; - - var line = andCondition.OperandRight[innerOrIdx] switch - { - RangeOperand range => andCondition.Relation == Relation.Equals - ? $"{leftVariable} >= {range.Start} && {leftVariable} <= {range.End}" - : $"({leftVariable} < {range.Start} || {leftVariable} > {range.End})", - NumberOperand number => andCondition.Relation == Relation.Equals - ? $"{leftVariable} == {number.Number}" - : $"{leftVariable} != {number.Number}", - var otherOperand => throw new InvalidOperationException($"Unknown right operand {otherOperand.GetType()}") - }; - - builder.Append(line); + VariableOperand op => $"context.{OperandToVariable(op.Operand)}", + ModuloOperand op => $"context.{OperandToVariable(op.Operand)} % {op.ModValue}", + var otherOp => throw new InvalidOperationException($"Unknown operation {otherOp.GetType()}") + }; - if (!isLast) - { - builder.Append(andCondition.Relation == Relation.Equals ? " || " : " && "); - } - } - builder.Append(')'); + var line = andCondition.OperandRight[innerOrIdx] switch + { + RangeOperand range => andCondition.Relation == Relation.Equals + ? $"{leftVariable} >= {range.Start} && {leftVariable} <= {range.End}" + : $"({leftVariable} < {range.Start} || {leftVariable} > {range.End})", + NumberOperand number => andCondition.Relation == Relation.Equals + ? $"{leftVariable} == {number.Number}" + : $"{leftVariable} != {number.Number}", + var otherOperand => throw new InvalidOperationException($"Unknown right operand {otherOperand.GetType()}") + }; + + builder.Append(line); - if (!andIsLast) + if (!isLast) { - builder.Append(" && "); + builder.Append(andCondition.Relation == Relation.Equals ? " || " : " && "); } } - } + builder.Append(')'); - private char OperandToVariable(OperandSymbol operand) - { - return operand switch + if (!andIsLast) { - OperandSymbol.AbsoluteValue => 'N', - OperandSymbol.IntegerDigits => 'I', - OperandSymbol.VisibleFractionDigitNumber => 'V', - OperandSymbol.VisibleFractionDigitNumberWithoutTrailingZeroes => 'W', - OperandSymbol.VisibleFractionDigits => 'F', - OperandSymbol.VisibleFractionDigitsWithoutTrailingZeroes => 'T', - OperandSymbol.ExponentC => 'C', - OperandSymbol.ExponentE => 'E', - _ => throw new InvalidOperationException($"Unknown variable {operand}") - }; + builder.Append(" && "); + } } + } - private void WriteLine(StringBuilder builder, string value, int indent) + private char OperandToVariable(OperandSymbol operand) + { + return operand switch { - builder.Append(' ', indent + _innerIndent); - builder.AppendLine(value); - } + OperandSymbol.AbsoluteValue => 'N', + OperandSymbol.IntegerDigits => 'I', + OperandSymbol.VisibleFractionDigitNumber => 'V', + OperandSymbol.VisibleFractionDigitNumberWithoutTrailingZeroes => 'W', + OperandSymbol.VisibleFractionDigits => 'F', + OperandSymbol.VisibleFractionDigitsWithoutTrailingZeroes => 'T', + OperandSymbol.ExponentC => 'C', + OperandSymbol.ExponentE => 'E', + _ => throw new InvalidOperationException($"Unknown variable {operand}") + }; + } + + private void WriteLine(StringBuilder builder, string value, int indent) + { + builder.Append(' ', indent + _innerIndent); + builder.AppendLine(value); } -} +} \ No newline at end of file diff --git a/src/Jeffijoe.MessageFormat.Tests/Formatting/BaseFormatterTests.cs b/src/Jeffijoe.MessageFormat.Tests/Formatting/BaseFormatterTests.cs index a2b8bed..5b5e9b6 100644 --- a/src/Jeffijoe.MessageFormat.Tests/Formatting/BaseFormatterTests.cs +++ b/src/Jeffijoe.MessageFormat.Tests/Formatting/BaseFormatterTests.cs @@ -13,361 +13,360 @@ using Xunit; using Xunit.Abstractions; -namespace Jeffijoe.MessageFormat.Tests.Formatting +namespace Jeffijoe.MessageFormat.Tests.Formatting; + +/// +/// The base formatter tests. +/// +public class BaseFormatterTests { + #region Fields + /// - /// The base formatter tests. + /// The output helper. /// - public class BaseFormatterTests - { - #region Fields + private readonly ITestOutputHelper outputHelper; - /// - /// The output helper. - /// - private readonly ITestOutputHelper outputHelper; + #endregion - #endregion + #region Constructors and Destructors - #region Constructors and Destructors - - /// - /// Initializes a new instance of the class. - /// - /// - /// The output helper. - /// - public BaseFormatterTests(ITestOutputHelper outputHelper) - { - this.outputHelper = outputHelper; - } + /// + /// Initializes a new instance of the class. + /// + /// + /// The output helper. + /// + public BaseFormatterTests(ITestOutputHelper outputHelper) + { + this.outputHelper = outputHelper; + } - #endregion + #endregion - #region Public Properties + #region Public Properties - /// - /// Gets the parse arguments_tests. - /// - public static IEnumerable ParseArgumentsTests + /// + /// Gets the parse arguments_tests. + /// + public static IEnumerable ParseArgumentsTests + { + get { - get - { - yield return - new object[] - { - "offset:1 test:1337 one {programmer} other{programmers}", - new[] { "offset", "test" }, - new[] { "1", "1337" }, - new[] { "one", "other" }, - new[] { "programmer", "programmers" } - }; - - yield return - new object[] - { - "offset:1 test:1337 one\\123 {programmer} other{programmers}", - new[] { "offset", "test" }, - new[] { "1", "1337" }, - new[] { "one\\123", "other" }, - new[] { "programmer", "programmers" } - }; - } + yield return + new object[] + { + "offset:1 test:1337 one {programmer} other{programmers}", + new[] { "offset", "test" }, + new[] { "1", "1337" }, + new[] { "one", "other" }, + new[] { "programmer", "programmers" } + }; + + yield return + new object[] + { + "offset:1 test:1337 one\\123 {programmer} other{programmers}", + new[] { "offset", "test" }, + new[] { "1", "1337" }, + new[] { "one\\123", "other" }, + new[] { "programmer", "programmers" } + }; } + } - /// - /// Gets the parse keyed blocks_tests. - /// - public static IEnumerable ParseKeyedBlocksTests + /// + /// Gets the parse keyed blocks_tests. + /// + public static IEnumerable ParseKeyedBlocksTests + { + get { - get - { - yield return - new object?[] - { - null, - Array.Empty(), - Array.Empty() - }; - yield return - new object[] - { - "male {he} female {she}unknown{they}", - new[] { "male", "female", "unknown" }, - new[] { "he", "she", "they" } - }; - yield return - new object[] - { - "zero {} other {wee}", - new[] { "zero", "other" }, - new[] { string.Empty, "wee" } - }; - yield return - new object[] - { - "male {''he''}", - new[] { "male"}, - new[] { "''he''" } - }; - yield return new object[] + yield return + new object?[] { - @" + null, + Array.Empty(), + Array.Empty() + }; + yield return + new object[] + { + "male {he} female {she}unknown{they}", + new[] { "male", "female", "unknown" }, + new[] { "he", "she", "they" } + }; + yield return + new object[] + { + "zero {} other {wee}", + new[] { "zero", "other" }, + new[] { string.Empty, "wee" } + }; + yield return + new object[] + { + "male {''he''}", + new[] { "male"}, + new[] { "''he''" } + }; + yield return new object[] + { + @" male {he} female {she} unknown {they} ", - new[] { "male", "female", "unknown" }, new[] { "he", "she", "they" } - }; - yield return new object[] - { - @" + new[] { "male", "female", "unknown" }, new[] { "he", "she", "they" } + }; + yield return new object[] + { + @" male {he} female {she{dawg}} unknown {they'{dawg}'} ", - new[] { "male", "female", "unknown" }, new[] { "he", "she{dawg}", @"they'{dawg}'" } - }; - } + new[] { "male", "female", "unknown" }, new[] { "he", "she{dawg}", @"they'{dawg}'" } + }; } + } - #endregion - - #region Public Methods and Operators - - /// - /// The parse arguments. - /// - /// - /// The args. - /// - /// - /// The extension keys. - /// - /// - /// The extension values. - /// - /// - /// The keys. - /// - /// - /// The blocks. - /// - [Theory] - [MemberData(nameof(ParseArgumentsTests))] - public void ParseArguments( - string args, - string[] extensionKeys, - string[] extensionValues, - string[] keys, - string[] blocks) - { - var subject = new BaseFormatterImpl(); - var req = new FormatterRequest(new Literal(1, 1, 1, 1, ""), string.Empty, null, args); - var actual = subject.ParseArguments(req); + #endregion - Assert.Equal(extensionKeys.Length, actual.Extensions.Count()); - Assert.Equal(keys.Length, actual.KeyedBlocks.Count()); + #region Public Methods and Operators - for (int i = 0; i < actual.Extensions.ToArray().Length; i++) - { - var extension = actual.Extensions.ToArray()[i]; - Assert.Equal(extensionKeys[i], extension.Extension); - Assert.Equal(extensionValues[i], extension.Value); - } + /// + /// The parse arguments. + /// + /// + /// The args. + /// + /// + /// The extension keys. + /// + /// + /// The extension values. + /// + /// + /// The keys. + /// + /// + /// The blocks. + /// + [Theory] + [MemberData(nameof(ParseArgumentsTests))] + public void ParseArguments( + string args, + string[] extensionKeys, + string[] extensionValues, + string[] keys, + string[] blocks) + { + var subject = new BaseFormatterImpl(); + var req = new FormatterRequest(new Literal(1, 1, 1, 1, ""), string.Empty, null, args); + var actual = subject.ParseArguments(req); - for (int i = 0; i < actual.KeyedBlocks.ToArray().Length; i++) - { - var block = actual.KeyedBlocks.ToArray()[i]; - Assert.Equal(keys[i], block.Key); - Assert.Equal(blocks[i], block.BlockText); - } - } + Assert.Equal(extensionKeys.Length, actual.Extensions.Count()); + Assert.Equal(keys.Length, actual.KeyedBlocks.Count()); - /// - /// The parse arguments_invalid. - /// - /// - /// The args. - /// - [Theory] - [InlineData("hello {{dawg}")] - [InlineData("hello {dawg}}")] - [InlineData("hello '{dawg}")] - [InlineData("hello {dawg'}")] - [InlineData("hello {dawg} {sweet}")] - [InlineData("hello {dawg} test{sweet}}")] - [InlineData("hello '{{dawg'}} test{sweet}")] - public void ParseArguments_invalid(string args) + for (int i = 0; i < actual.Extensions.ToArray().Length; i++) { - var subject = new BaseFormatterImpl(); - var req = new FormatterRequest(new Literal(1, 1, 1, 1, ""), string.Empty, null, args); - var ex = Assert.Throws(() => subject.ParseArguments(req)); - Assert.Equal(args, ex.SourceSnippet); - this.outputHelper.WriteLine(ex.Message); + var extension = actual.Extensions.ToArray()[i]; + Assert.Equal(extensionKeys[i], extension.Extension); + Assert.Equal(extensionValues[i], extension.Value); } - /// - /// The parse extensions. - /// - /// - /// The args. - /// - /// - /// The extension. - /// - /// - /// The value. - /// - /// - /// The expected index. - /// - [Theory] - [InlineData(" offset:3 boom", "offset", "3", 9)] - [InlineData("testie:dawg lel", "testie", "dawg", 11)] - public void ParseExtensions(string? args, string extension, string value, int expectedIndex) + for (int i = 0; i < actual.KeyedBlocks.ToArray().Length; i++) { - var subject = new BaseFormatterImpl(); - var req = new FormatterRequest(new Literal(1, 1, 1, 1, ""), string.Empty, null, args); + var block = actual.KeyedBlocks.ToArray()[i]; + Assert.Equal(keys[i], block.Key); + Assert.Equal(blocks[i], block.BlockText); + } + } - // Warmup - subject.ParseExtensions(req, out var index); + /// + /// The parse arguments_invalid. + /// + /// + /// The args. + /// + [Theory] + [InlineData("hello {{dawg}")] + [InlineData("hello {dawg}}")] + [InlineData("hello '{dawg}")] + [InlineData("hello {dawg'}")] + [InlineData("hello {dawg} {sweet}")] + [InlineData("hello {dawg} test{sweet}}")] + [InlineData("hello '{{dawg'}} test{sweet}")] + public void ParseArguments_invalid(string args) + { + var subject = new BaseFormatterImpl(); + var req = new FormatterRequest(new Literal(1, 1, 1, 1, ""), string.Empty, null, args); + var ex = Assert.Throws(() => subject.ParseArguments(req)); + Assert.Equal(args, ex.SourceSnippet); + this.outputHelper.WriteLine(ex.Message); + } - Benchmark.Start("Parsing extensions a few times (warmed up)", this.outputHelper); - for (int i = 0; i < 1000; i++) - { - subject.ParseExtensions(req, out index); - } + /// + /// The parse extensions. + /// + /// + /// The args. + /// + /// + /// The extension. + /// + /// + /// The value. + /// + /// + /// The expected index. + /// + [Theory] + [InlineData(" offset:3 boom", "offset", "3", 9)] + [InlineData("testie:dawg lel", "testie", "dawg", 11)] + public void ParseExtensions(string? args, string extension, string value, int expectedIndex) + { + var subject = new BaseFormatterImpl(); + var req = new FormatterRequest(new Literal(1, 1, 1, 1, ""), string.Empty, null, args); - Benchmark.End(this.outputHelper); + // Warmup + subject.ParseExtensions(req, out var index); - var actual = subject.ParseExtensions(req, out index).ToList(); - Assert.NotEmpty(actual); - var first = actual.First(); - Assert.Equal(extension, first.Extension); - Assert.Equal(value, first.Value); - Assert.Equal(expectedIndex, index); - } - - /// - /// The parse extensions returns empty collection when formatter arguments is null. - /// - [Fact] - public void ParseExtensions_returns_empty_collection_when_formatter_arguments_is_null() + Benchmark.Start("Parsing extensions a few times (warmed up)", this.outputHelper); + for (int i = 0; i < 1000; i++) { - string? args = null; - var subject = new BaseFormatterImpl(); - var req = new FormatterRequest(new Literal(1, 1, 1, 1, ""), string.Empty, null, args); - - var actual = subject.ParseExtensions(req, out var index); - - Assert.Empty(actual); - Assert.Equal(-1, index); + subject.ParseExtensions(req, out index); } - /// - /// The parse extensions_multiple. - /// - [Fact] - public void ParseExtensions_multiple() - { - var subject = new BaseFormatterImpl(); - var args = " offset:2 code:js "; - var expectedIndex = 17; + Benchmark.End(this.outputHelper); - var req = new FormatterRequest(new Literal(1, 1, 1, 1, ""), string.Empty, null, args); + var actual = subject.ParseExtensions(req, out index).ToList(); + Assert.NotEmpty(actual); + var first = actual.First(); + Assert.Equal(extension, first.Extension); + Assert.Equal(value, first.Value); + Assert.Equal(expectedIndex, index); + } + + /// + /// The parse extensions returns empty collection when formatter arguments is null. + /// + [Fact] + public void ParseExtensions_returns_empty_collection_when_formatter_arguments_is_null() + { + string? args = null; + var subject = new BaseFormatterImpl(); + var req = new FormatterRequest(new Literal(1, 1, 1, 1, ""), string.Empty, null, args); + + var actual = subject.ParseExtensions(req, out var index); - var actual = subject.ParseExtensions(req, out var index).ToList(); - Assert.NotEmpty(actual); - var result = actual.First(); - Assert.Equal("offset", result.Extension); - Assert.Equal("2", result.Value); + Assert.Empty(actual); + Assert.Equal(-1, index); + } - result = actual.ElementAt(1); - Assert.Equal("code", result.Extension); - Assert.Equal("js", result.Value); + /// + /// The parse extensions_multiple. + /// + [Fact] + public void ParseExtensions_multiple() + { + var subject = new BaseFormatterImpl(); + var args = " offset:2 code:js "; + var expectedIndex = 17; - Assert.Equal(expectedIndex, index); - } + var req = new FormatterRequest(new Literal(1, 1, 1, 1, ""), string.Empty, null, args); - /// - /// The parse keyed blocks. - /// - /// - /// The args. - /// - /// - /// The keys. - /// - /// - /// The values. - /// - [Theory] - [MemberData(nameof(ParseKeyedBlocksTests))] - public void ParseKeyedBlocks(string? args, string[] keys, string[] values) - { - var subject = new BaseFormatterImpl(); - var req = new FormatterRequest(new Literal(1, 1, 1, 1, ""), string.Empty, null, args); + var actual = subject.ParseExtensions(req, out var index).ToList(); + Assert.NotEmpty(actual); + var result = actual.First(); + Assert.Equal("offset", result.Extension); + Assert.Equal("2", result.Value); - // Warm-up - subject.ParseKeyedBlocks(req, 0); + result = actual.ElementAt(1); + Assert.Equal("code", result.Extension); + Assert.Equal("js", result.Value); - Benchmark.Start("Parsing keyed blocks..", this.outputHelper); - for (int i = 0; i < 10000; i++) - { - subject.ParseKeyedBlocks(req, 0); - } + Assert.Equal(expectedIndex, index); + } - Benchmark.End(this.outputHelper); + /// + /// The parse keyed blocks. + /// + /// + /// The args. + /// + /// + /// The keys. + /// + /// + /// The values. + /// + [Theory] + [MemberData(nameof(ParseKeyedBlocksTests))] + public void ParseKeyedBlocks(string? args, string[] keys, string[] values) + { + var subject = new BaseFormatterImpl(); + var req = new FormatterRequest(new Literal(1, 1, 1, 1, ""), string.Empty, null, args); - var actual = subject.ParseKeyedBlocks(req, 0).ToList(); - Assert.Equal(keys.Length, actual.Count()); - this.outputHelper.WriteLine("Input: " + args); - this.outputHelper.WriteLine("-----"); - for (int index = 0; index < actual.ToArray().Length; index++) - { - var keyedBlock = actual.ToArray()[index]; - var expectedKey = keys[index]; - var expectedValue = values[index]; - Assert.Equal(expectedKey, keyedBlock.Key); - Assert.Equal(expectedValue, keyedBlock.BlockText); - - this.outputHelper.WriteLine("Key: " + keyedBlock.Key); - this.outputHelper.WriteLine("Block: " + keyedBlock.BlockText); - } - } + // Warm-up + subject.ParseKeyedBlocks(req, 0); - /// - /// The parse keyed blocks unclosed_escape_sequence. - /// - /// - /// The args. - /// - [Theory] - [InlineData("male {he} other {'{they}")] - [InlineData("male {he} other {'# they}")] - [InlineData("male {he} other }")] - [InlineData("male {he} other {'")] - [InlineData("male {he} other {'{'")] - [InlineData("male{}} female{}")] - [InlineData("male haha")] - public void ParseKeyedBlocks_bad_formatting(string? args) + Benchmark.Start("Parsing keyed blocks..", this.outputHelper); + for (int i = 0; i < 10000; i++) { - var subject = new BaseFormatterImpl(); - var req = new FormatterRequest(new Literal(1, 1, 1, 1, ""), string.Empty, null, args); - - Assert.Throws(() => subject.ParseKeyedBlocks(req, 0)); + subject.ParseKeyedBlocks(req, 0); } - #endregion + Benchmark.End(this.outputHelper); - /// - /// The base formatter impl. - /// - private class BaseFormatterImpl : BaseFormatter + var actual = subject.ParseKeyedBlocks(req, 0).ToList(); + Assert.Equal(keys.Length, actual.Count()); + this.outputHelper.WriteLine("Input: " + args); + this.outputHelper.WriteLine("-----"); + for (int index = 0; index < actual.ToArray().Length; index++) { + var keyedBlock = actual.ToArray()[index]; + var expectedKey = keys[index]; + var expectedValue = values[index]; + Assert.Equal(expectedKey, keyedBlock.Key); + Assert.Equal(expectedValue, keyedBlock.BlockText); + + this.outputHelper.WriteLine("Key: " + keyedBlock.Key); + this.outputHelper.WriteLine("Block: " + keyedBlock.BlockText); } } + + /// + /// The parse keyed blocks unclosed_escape_sequence. + /// + /// + /// The args. + /// + [Theory] + [InlineData("male {he} other {'{they}")] + [InlineData("male {he} other {'# they}")] + [InlineData("male {he} other }")] + [InlineData("male {he} other {'")] + [InlineData("male {he} other {'{'")] + [InlineData("male{}} female{}")] + [InlineData("male haha")] + public void ParseKeyedBlocks_bad_formatting(string? args) + { + var subject = new BaseFormatterImpl(); + var req = new FormatterRequest(new Literal(1, 1, 1, 1, ""), string.Empty, null, args); + + Assert.Throws(() => subject.ParseKeyedBlocks(req, 0)); + } + + #endregion + + /// + /// The base formatter impl. + /// + private class BaseFormatterImpl : BaseFormatter + { + } } \ No newline at end of file diff --git a/src/Jeffijoe.MessageFormat.Tests/Formatting/FormatterLibraryTests.cs b/src/Jeffijoe.MessageFormat.Tests/Formatting/FormatterLibraryTests.cs index be95522..db33466 100644 --- a/src/Jeffijoe.MessageFormat.Tests/Formatting/FormatterLibraryTests.cs +++ b/src/Jeffijoe.MessageFormat.Tests/Formatting/FormatterLibraryTests.cs @@ -9,46 +9,45 @@ using Jeffijoe.MessageFormat.Tests.TestHelpers; using Xunit; -namespace Jeffijoe.MessageFormat.Tests.Formatting +namespace Jeffijoe.MessageFormat.Tests.Formatting; + +/// +/// The formatter library tests. +/// +public class FormatterLibraryTests { + #region Public Methods and Operators + /// - /// The formatter library tests. + /// The get formatter. /// - public class FormatterLibraryTests + [Fact] + public void GetFormatter() { - #region Public Methods and Operators - - /// - /// The get formatter. - /// - [Fact] - public void GetFormatter() - { - var subject = new FormatterLibrary(); + var subject = new FormatterLibrary(); - var req = new FormatterRequest(new Literal(1, 1, 1, 1, ""), "test", "dawg", null); - var formatter1 = new FakeFormatter(); - var formatter2 = new FakeFormatter(); + var req = new FormatterRequest(new Literal(1, 1, 1, 1, ""), "test", "dawg", null); + var formatter1 = new FakeFormatter(); + var formatter2 = new FakeFormatter(); - subject.Add(formatter1); - subject.Add(formatter2); + subject.Add(formatter1); + subject.Add(formatter2); - Assert.Throws(() => subject.GetFormatter(req)); + Assert.Throws(() => subject.GetFormatter(req)); - formatter2.SetCanFormat(true); + formatter2.SetCanFormat(true); - var actual = subject.GetFormatter(req); - Assert.Same(formatter2, actual); + var actual = subject.GetFormatter(req); + Assert.Same(formatter2, actual); - formatter1.SetCanFormat(true); - actual = subject.GetFormatter(req); - Assert.Same(formatter1, actual); - } + formatter1.SetCanFormat(true); + actual = subject.GetFormatter(req); + Assert.Same(formatter1, actual); + } - #endregion + #endregion - #region Fakes + #region Fakes - #endregion - } + #endregion } \ No newline at end of file diff --git a/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/DateFormatterTests.cs b/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/DateFormatterTests.cs index 8410bfe..c36168f 100644 --- a/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/DateFormatterTests.cs +++ b/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/DateFormatterTests.cs @@ -3,68 +3,67 @@ using Jeffijoe.MessageFormat.Formatting; using Xunit; -namespace Jeffijoe.MessageFormat.Tests.Formatting.Formatters +namespace Jeffijoe.MessageFormat.Tests.Formatting.Formatters; + +public class DateFormatterTests { - public class DateFormatterTests + [Theory] + [InlineData("en-US", "1994-09-06T15:00:00Z", "9/6/1994")] + [InlineData("da-DK", "1994-09-06T15:00:00Z", "06.09.1994")] + public void DateFormatter_Short(string locale, string dateStr, string expected) { - [Theory] - [InlineData("en-US", "1994-09-06T15:00:00Z", "9/6/1994")] - [InlineData("da-DK", "1994-09-06T15:00:00Z", "06.09.1994")] - public void DateFormatter_Short(string locale, string dateStr, string expected) + var mf = new MessageFormatter(locale: locale); + var actual = mf.FormatMessage("{value, date}", new { - var mf = new MessageFormatter(locale: locale); - var actual = mf.FormatMessage("{value, date}", new - { - value = DateTimeOffset.Parse(dateStr) - }); + value = DateTimeOffset.Parse(dateStr) + }); - Assert.Equal(expected, actual); - } + Assert.Equal(expected, actual); + } - [Theory] - [InlineData("en-US", "1994-09-06T15:00:00Z", "Tuesday, September 6, 1994")] - [InlineData("da-DK", "1994-09-06T15:00:00Z", "tirsdag den 6. september 1994")] - public void DateFormatter_Full(string locale, string dateStr, string expected) + [Theory] + [InlineData("en-US", "1994-09-06T15:00:00Z", "Tuesday, September 6, 1994")] + [InlineData("da-DK", "1994-09-06T15:00:00Z", "tirsdag den 6. september 1994")] + public void DateFormatter_Full(string locale, string dateStr, string expected) + { + var mf = new MessageFormatter(locale: locale); + var actual = mf.FormatMessage("{value, date, full}", new { - var mf = new MessageFormatter(locale: locale); - var actual = mf.FormatMessage("{value, date, full}", new - { - value = DateTimeOffset.Parse(dateStr) - }); + value = DateTimeOffset.Parse(dateStr) + }); - Assert.Equal(expected, actual); - } + Assert.Equal(expected, actual); + } - [Fact] - public void DateFormatter_UnsupportedStyle() - { - var mf = new MessageFormatter(); - Assert.Throws( - () => mf.FormatMessage("{value, date, long}", new - { - value = DateTimeOffset.UtcNow - })); - } + [Fact] + public void DateFormatter_UnsupportedStyle() + { + var mf = new MessageFormatter(); + Assert.Throws( + () => mf.FormatMessage("{value, date, long}", new + { + value = DateTimeOffset.UtcNow + })); + } - [Fact] - public void DateFormatter_Custom() + [Fact] + public void DateFormatter_Custom() + { + var formatter = new CustomValueFormatters { - var formatter = new CustomValueFormatters - { - Date = (CultureInfo culture, object? value, string? _, out string? formatted) => - { - // This is just a test, you probably shouldn't be doing this in real workloads. - formatted = ((FormattableString)$"{value:MMMM d 'in the year' yyyy}").ToString(culture); - return true; - } - }; - var mf = new MessageFormatter(locale: "en-US", customValueFormatter: formatter); - var actual = mf.FormatMessage("{value, date, long}", new + Date = (CultureInfo culture, object? value, string? _, out string? formatted) => { - value = DateTimeOffset.Parse("1994-09-06T15:00:00Z") - }); + // This is just a test, you probably shouldn't be doing this in real workloads. + formatted = ((FormattableString)$"{value:MMMM d 'in the year' yyyy}").ToString(culture); + return true; + } + }; + var mf = new MessageFormatter(locale: "en-US", customValueFormatter: formatter); + var actual = mf.FormatMessage("{value, date, long}", new + { + value = DateTimeOffset.Parse("1994-09-06T15:00:00Z") + }); - Assert.Equal("September 6 in the year 1994", actual); - } + Assert.Equal("September 6 in the year 1994", actual); } } \ No newline at end of file diff --git a/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/NumberFormatterTests.cs b/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/NumberFormatterTests.cs index f3f81e3..648cfab 100644 --- a/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/NumberFormatterTests.cs +++ b/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/NumberFormatterTests.cs @@ -2,149 +2,148 @@ using Jeffijoe.MessageFormat.Formatting; using Xunit; -namespace Jeffijoe.MessageFormat.Tests.Formatting.Formatters +namespace Jeffijoe.MessageFormat.Tests.Formatting.Formatters; + +public class NumberFormatterTests { - public class NumberFormatterTests + [Theory] + [InlineData(69, "69")] + [InlineData(69.420, "69.42")] + [InlineData(123_456.789, "123,456.789")] + [InlineData(1234567.1234567, "1,234,567.123")] + public void NumberFormatter_Default(decimal number, string expected) { - [Theory] - [InlineData(69, "69")] - [InlineData(69.420, "69.42")] - [InlineData(123_456.789, "123,456.789")] - [InlineData(1234567.1234567, "1,234,567.123")] - public void NumberFormatter_Default(decimal number, string expected) + var mf = new MessageFormatter(locale: "en-US"); + // NOTE: The whitespace at the end is on purpose to cover whitespace tolerance in parsing. + var actual = mf.FormatMessage("{value, number }", new { - var mf = new MessageFormatter(locale: "en-US"); - // NOTE: The whitespace at the end is on purpose to cover whitespace tolerance in parsing. - var actual = mf.FormatMessage("{value, number }", new - { - value = number - }); + value = number + }); - Assert.Equal(expected, actual); - } + Assert.Equal(expected, actual); + } - [Theory] - [InlineData(69, "69.0000")] - [InlineData(69.420, "69.4200")] - public void NumberFormatter_Decimal_CustomFormat(decimal number, string expected) + [Theory] + [InlineData(69, "69.0000")] + [InlineData(69.420, "69.4200")] + public void NumberFormatter_Decimal_CustomFormat(decimal number, string expected) + { + var formatters = new CustomValueFormatters { - var formatters = new CustomValueFormatters + Number = (CultureInfo culture, object? value, string? style, out string? formatted) => { - Number = (CultureInfo culture, object? value, string? style, out string? formatted) => - { - formatted = string.Format(culture, $"{{0:{style}}}", value); - return true; - } - }; - var mf = new MessageFormatter(locale: "en-US", customValueFormatter: formatters); + formatted = string.Format(culture, $"{{0:{style}}}", value); + return true; + } + }; + var mf = new MessageFormatter(locale: "en-US", customValueFormatter: formatters); - var actual = mf.FormatMessage("{value, number, 0.0000}", new - { - value = number - }); + var actual = mf.FormatMessage("{value, number, 0.0000}", new + { + value = number + }); - Assert.Equal(expected, actual); - } + Assert.Equal(expected, actual); + } - [Theory] - [InlineData(0.2, "20%")] - [InlineData(1.2, "120%")] - [InlineData(1234567.1234567, "123,456,712%")] - public void NumberFormatter_Percent(decimal number, string expected) - { - var mf = new MessageFormatter(locale: "en-US"); + [Theory] + [InlineData(0.2, "20%")] + [InlineData(1.2, "120%")] + [InlineData(1234567.1234567, "123,456,712%")] + public void NumberFormatter_Percent(decimal number, string expected) + { + var mf = new MessageFormatter(locale: "en-US"); - // NOTE: The inconsistent whitespace in the pattern is to cover whitespace tolerance in parsing. - var actual = mf.FormatMessage("{value, number,percent}", new - { - value = number - }); + // NOTE: The inconsistent whitespace in the pattern is to cover whitespace tolerance in parsing. + var actual = mf.FormatMessage("{value, number,percent}", new + { + value = number + }); - Assert.Equal(expected, actual); - } + Assert.Equal(expected, actual); + } - [Theory] - [InlineData(0.2, "0")] - [InlineData(1.2, "1")] - [InlineData(2.7, "3")] - [InlineData("2.7", "3")] - [InlineData("a string", "a string")] - [InlineData(true, "True")] - public void NumberFormatter_Integer(object? value, string expected) + [Theory] + [InlineData(0.2, "0")] + [InlineData(1.2, "1")] + [InlineData(2.7, "3")] + [InlineData("2.7", "3")] + [InlineData("a string", "a string")] + [InlineData(true, "True")] + public void NumberFormatter_Integer(object? value, string expected) + { + var mf = new MessageFormatter(locale: "en-US"); + var actual = mf.FormatMessage("{value, number, integer}", new { - var mf = new MessageFormatter(locale: "en-US"); - var actual = mf.FormatMessage("{value, number, integer}", new - { - value - }); + value + }); - Assert.Equal(expected, actual); - } + Assert.Equal(expected, actual); + } - [Theory] - [InlineData("en-US", 20, "$20.00")] - [InlineData("en-US", 99.99, "$99.99")] - [InlineData("da-DK", 99.99, "99,99 kr.")] - public void NumberFormatter_Currency(string locale, decimal number, string expected) + [Theory] + [InlineData("en-US", 20, "$20.00")] + [InlineData("en-US", 99.99, "$99.99")] + [InlineData("da-DK", 99.99, "99,99 kr.")] + public void NumberFormatter_Currency(string locale, decimal number, string expected) + { + var mf = new MessageFormatter(locale: locale); + + // NOTE: The inconsistent whitespace in the pattern is to cover whitespace tolerance in parsing. + var actual = mf.FormatMessage("{value, number, currency }", new { - var mf = new MessageFormatter(locale: locale); + value = number + }); - // NOTE: The inconsistent whitespace in the pattern is to cover whitespace tolerance in parsing. - var actual = mf.FormatMessage("{value, number, currency }", new - { - value = number - }); + Assert.Equal(expected, actual); + } - Assert.Equal(expected, actual); - } + [Fact] + public void NumberFormatter_ThrowsIfStyleIsNotSupported() + { + const decimal Number = 12.34m; + var mf = new MessageFormatter(locale: "en-US"); + var ex = Assert.Throws(() => + mf.FormatMessage($"{{value, number, wow}}", + new + { + value = Number + })); + Assert.Equal("value", ex.Variable); + Assert.Equal("number", ex.Format); + Assert.Equal("wow", ex.Style); + } - [Fact] - public void NumberFormatter_ThrowsIfStyleIsNotSupported() - { - const decimal Number = 12.34m; - var mf = new MessageFormatter(locale: "en-US"); - var ex = Assert.Throws(() => - mf.FormatMessage($"{{value, number, wow}}", - new - { - value = Number - })); - Assert.Equal("value", ex.Variable); - Assert.Equal("number", ex.Format); - Assert.Equal("wow", ex.Style); - } + [Fact] + public void NumberFormatter_BadInput_FallsBackToRegularFormat() + { + var mf = new MessageFormatter(locale: "en-US"); - [Fact] - public void NumberFormatter_BadInput_FallsBackToRegularFormat() { - var mf = new MessageFormatter(locale: "en-US"); - + var actual = mf.FormatMessage($"{{value, number, currency}}", new { - var actual = mf.FormatMessage($"{{value, number, currency}}", new - { - value = "a lot of money" - }); + value = "a lot of money" + }); - Assert.Equal("a lot of money", actual); - } + Assert.Equal("a lot of money", actual); + } + { + var actual = mf.FormatMessage($"{{value, number, integer}}", new { - var actual = mf.FormatMessage($"{{value, number, integer}}", new - { - value = "a lot of money" - }); + value = "a lot of money" + }); - Assert.Equal("a lot of money", actual); - } + Assert.Equal("a lot of money", actual); + } + { + var actual = mf.FormatMessage($"{{value, number, integer}}", new { - var actual = mf.FormatMessage($"{{value, number, integer}}", new - { - value = true - }); + value = true + }); - Assert.Equal("True", actual); - } + Assert.Equal("True", actual); } } } \ No newline at end of file diff --git a/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/PluralFormatterTests.cs b/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/PluralFormatterTests.cs index 43d56bc..e870bde 100644 --- a/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/PluralFormatterTests.cs +++ b/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/PluralFormatterTests.cs @@ -12,110 +12,109 @@ using Xunit; -namespace Jeffijoe.MessageFormat.Tests.Formatting.Formatters +namespace Jeffijoe.MessageFormat.Tests.Formatting.Formatters; + +/// +/// The plural formatter tests. +/// +public class PluralFormatterTests { + #region Public Methods and Operators + /// - /// The plural formatter tests. + /// The pluralize. /// - public class PluralFormatterTests + /// + /// The n. + /// + /// + /// The expected. + /// + [Theory] + [InlineData(0, "nothing")] + [InlineData(1, "just one")] + [InlineData(1337, "wow")] + public void Pluralize(double n, string expected) { - #region Public Methods and Operators - - /// - /// The pluralize. - /// - /// - /// The n. - /// - /// - /// The expected. - /// - [Theory] - [InlineData(0, "nothing")] - [InlineData(1, "just one")] - [InlineData(1337, "wow")] - public void Pluralize(double n, string expected) - { - var subject = new PluralFormatter(); - var args = new Dictionary { { "test", n } }; - var arguments = - new ParsedArguments( - new[] - { - new KeyedBlock("zero", "nothing"), - new KeyedBlock("one", "just one"), - new KeyedBlock("other", "wow") - }, - Array.Empty()); - var request = new FormatterRequest(new Literal(1, 1, 1, 1, ""), "test", "plural", null); - var actual = subject.Pluralize("en", arguments, new PluralContext(Convert.ToDecimal(Convert.ToDouble(args[request.Variable]))), 0); - Assert.Equal(expected, actual); - } + var subject = new PluralFormatter(); + var args = new Dictionary { { "test", n } }; + var arguments = + new ParsedArguments( + new[] + { + new KeyedBlock("zero", "nothing"), + new KeyedBlock("one", "just one"), + new KeyedBlock("other", "wow") + }, + Array.Empty()); + var request = new FormatterRequest(new Literal(1, 1, 1, 1, ""), "test", "plural", null); + var actual = subject.Pluralize("en", arguments, new PluralContext(Convert.ToDecimal(Convert.ToDouble(args[request.Variable]))), 0); + Assert.Equal(expected, actual); + } - /// - /// The pluralize_defaults_to_en_locale_when_specified_locale_is_not_found - /// - [Fact] - public void Pluralize_defaults_to_en_locale_when_specified_locale_is_not_found() - { - var subject = new PluralFormatter(); - var args = new Dictionary { { "test", 1 } }; - var arguments = - new ParsedArguments( - new[] - { - new KeyedBlock("zero", "nothing"), - new KeyedBlock("one", "just one"), - new KeyedBlock("other", "wow") - }, - Array.Empty()); - var request = new FormatterRequest(new Literal(1, 1, 1, 1, ""), "test", "plural", null); - var actual = subject.Pluralize("unknown", arguments, new PluralContext(Convert.ToDecimal(Convert.ToDouble(args[request.Variable]))), 0); - Assert.Equal("just one", actual); - } + /// + /// The pluralize_defaults_to_en_locale_when_specified_locale_is_not_found + /// + [Fact] + public void Pluralize_defaults_to_en_locale_when_specified_locale_is_not_found() + { + var subject = new PluralFormatter(); + var args = new Dictionary { { "test", 1 } }; + var arguments = + new ParsedArguments( + new[] + { + new KeyedBlock("zero", "nothing"), + new KeyedBlock("one", "just one"), + new KeyedBlock("other", "wow") + }, + Array.Empty()); + var request = new FormatterRequest(new Literal(1, 1, 1, 1, ""), "test", "plural", null); + var actual = subject.Pluralize("unknown", arguments, new PluralContext(Convert.ToDecimal(Convert.ToDouble(args[request.Variable]))), 0); + Assert.Equal("just one", actual); + } - /// - /// The pluralize_defaults_to_en_locale_when_specified_locale_is_not_found - /// - [Fact] - public void Pluralize_throws_when_missing_other_block() - { - var subject = new PluralFormatter(); - var args = new Dictionary { { "test", 5 } }; - var arguments = - new ParsedArguments( - new[] - { - new KeyedBlock("zero", "nothing"), - new KeyedBlock("one", "just one") - }, - Array.Empty()); - var request = new FormatterRequest(new Literal(1, 1, 1, 1, ""), "test", "plural", null); - Assert.Throws(() => subject.Pluralize("unknown", arguments, new PluralContext(Convert.ToDecimal(Convert.ToDouble(args[request.Variable]))), 0)); - } - - /// - /// The replace number literals. - /// - /// - /// The input. - /// - /// - /// The expected. - /// - [Theory] - [InlineData(@"Number '#1' has # results", "Number '#1' has 1337 results")] - [InlineData(@"Number '#'1 has # results", "Number '#'1 has 1337 results")] - [InlineData(@"Number '#'# has # results", "Number '#'1337 has 1337 results")] - [InlineData(@"Number '''#'''# has # results", "Number '''#'''1337 has 1337 results")] - [InlineData(@"# results", "1337 results")] - public void ReplaceNumberLiterals(string input, string expected) - { - var subject = new PluralFormatter(); - var actual = subject.ReplaceNumberLiterals(input, 1337); - Assert.Equal(expected, actual); - } + /// + /// The pluralize_defaults_to_en_locale_when_specified_locale_is_not_found + /// + [Fact] + public void Pluralize_throws_when_missing_other_block() + { + var subject = new PluralFormatter(); + var args = new Dictionary { { "test", 5 } }; + var arguments = + new ParsedArguments( + new[] + { + new KeyedBlock("zero", "nothing"), + new KeyedBlock("one", "just one") + }, + Array.Empty()); + var request = new FormatterRequest(new Literal(1, 1, 1, 1, ""), "test", "plural", null); + Assert.Throws(() => subject.Pluralize("unknown", arguments, new PluralContext(Convert.ToDecimal(Convert.ToDouble(args[request.Variable]))), 0)); + } - #endregion + /// + /// The replace number literals. + /// + /// + /// The input. + /// + /// + /// The expected. + /// + [Theory] + [InlineData(@"Number '#1' has # results", "Number '#1' has 1337 results")] + [InlineData(@"Number '#'1 has # results", "Number '#'1 has 1337 results")] + [InlineData(@"Number '#'# has # results", "Number '#'1337 has 1337 results")] + [InlineData(@"Number '''#'''# has # results", "Number '''#'''1337 has 1337 results")] + [InlineData(@"# results", "1337 results")] + public void ReplaceNumberLiterals(string input, string expected) + { + var subject = new PluralFormatter(); + var actual = subject.ReplaceNumberLiterals(input, 1337); + Assert.Equal(expected, actual); } + + #endregion } \ No newline at end of file diff --git a/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/SelectFormatterTests.cs b/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/SelectFormatterTests.cs index a35bedb..4e2ce37 100644 --- a/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/SelectFormatterTests.cs +++ b/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/SelectFormatterTests.cs @@ -11,82 +11,81 @@ using Jeffijoe.MessageFormat.Tests.TestHelpers; using Xunit; -namespace Jeffijoe.MessageFormat.Tests.Formatting.Formatters +namespace Jeffijoe.MessageFormat.Tests.Formatting.Formatters; + +/// +/// The select formatter tests. +/// +public class SelectFormatterTests { + #region Public Properties + /// - /// The select formatter tests. + /// Gets the format_tests. /// - public class SelectFormatterTests + public static IEnumerable FormatTests { - #region Public Properties - - /// - /// Gets the format_tests. - /// - public static IEnumerable FormatTests + get { - get - { - yield return new object[] { "male {he said} female {she said} other {they said}", "male", "he said" }; - yield return new object[] - { "male {he said} female {she said} other {they said}", "female", "she said" }; - yield return new object[] { "male {he said} female {she said} other {they said}", "dawg", "they said" }; - } + yield return new object[] { "male {he said} female {she said} other {they said}", "male", "he said" }; + yield return new object[] + { "male {he said} female {she said} other {they said}", "female", "she said" }; + yield return new object[] { "male {he said} female {she said} other {they said}", "dawg", "they said" }; } + } - #endregion + #endregion - #region Public Methods and Operators + #region Public Methods and Operators - /// - /// The format. - /// - /// - /// The formatter args. - /// - /// - /// The gender. - /// - /// - /// The expected block. - /// - [Theory] - [MemberData(nameof(FormatTests))] - public void Format(string formatterArgs, string gender, string expectedBlock) - { - var subject = new SelectFormatter(); - var messageFormatter = new FakeMessageFormatter(); - var req = new FormatterRequest( - new Literal(1, 1, 1, 1, ""), - "gender", - "select", - formatterArgs); - var args = new Dictionary { { "gender", gender } }; - var result = subject.Format("en", req, args, gender, messageFormatter); - Assert.Equal(expectedBlock, result); - } - - /// - /// Verifies that format throws when no other option is given. - /// - [Fact] - public void VerifyFormatThrowsWhenNoOtherOptionIsGiven() - { - var subject = new SelectFormatter(); - var messageFormatter = new FakeMessageFormatter(); - var req = new FormatterRequest( - new Literal(1, 1, 1, 1, ""), - "gender", - "select", - "male {he} female{she}"); - var args = new Dictionary { { "gender", "non-binary" } }; + /// + /// The format. + /// + /// + /// The formatter args. + /// + /// + /// The gender. + /// + /// + /// The expected block. + /// + [Theory] + [MemberData(nameof(FormatTests))] + public void Format(string formatterArgs, string gender, string expectedBlock) + { + var subject = new SelectFormatter(); + var messageFormatter = new FakeMessageFormatter(); + var req = new FormatterRequest( + new Literal(1, 1, 1, 1, ""), + "gender", + "select", + formatterArgs); + var args = new Dictionary { { "gender", gender } }; + var result = subject.Format("en", req, args, gender, messageFormatter); + Assert.Equal(expectedBlock, result); + } - Assert.Throws(() => - { - subject.Format("en", req, args, "non-binary", messageFormatter); - }); - } + /// + /// Verifies that format throws when no other option is given. + /// + [Fact] + public void VerifyFormatThrowsWhenNoOtherOptionIsGiven() + { + var subject = new SelectFormatter(); + var messageFormatter = new FakeMessageFormatter(); + var req = new FormatterRequest( + new Literal(1, 1, 1, 1, ""), + "gender", + "select", + "male {he} female{she}"); + var args = new Dictionary { { "gender", "non-binary" } }; - #endregion + Assert.Throws(() => + { + subject.Format("en", req, args, "non-binary", messageFormatter); + }); } + + #endregion } \ No newline at end of file diff --git a/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/TimeFormatterTests.cs b/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/TimeFormatterTests.cs index f582b33..ca22c7f 100644 --- a/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/TimeFormatterTests.cs +++ b/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/TimeFormatterTests.cs @@ -4,82 +4,81 @@ using Jeffijoe.MessageFormat.Formatting; using Xunit; -namespace Jeffijoe.MessageFormat.Tests.Formatting.Formatters +namespace Jeffijoe.MessageFormat.Tests.Formatting.Formatters; + +public partial class TimeFormatterTests { - public partial class TimeFormatterTests - { - [Theory] - [InlineData("en-US", "1994-09-06T15:01:23Z", "3:01 PM")] - [InlineData("da-DK", "1994-09-06T15:01:23Z", "15.01")] - public void TimeFormatter_Short(string locale, string dateStr, string expected) + [Theory] + [InlineData("en-US", "1994-09-06T15:01:23Z", "3:01 PM")] + [InlineData("da-DK", "1994-09-06T15:01:23Z", "15.01")] + public void TimeFormatter_Short(string locale, string dateStr, string expected) + { + var mf = new MessageFormatter(locale: locale); + var actual = mf.FormatMessage("{value, time, short}", new { - var mf = new MessageFormatter(locale: locale); - var actual = mf.FormatMessage("{value, time, short}", new - { - value = DateTimeOffset.Parse(dateStr) - }); + value = DateTimeOffset.Parse(dateStr) + }); - // Replacing all whitespace due to a difference in formatting on macOS vs Linux. - expected = Normalize(expected); - actual = Normalize(actual); - Assert.Equal(expected, actual); - } + // Replacing all whitespace due to a difference in formatting on macOS vs Linux. + expected = Normalize(expected); + actual = Normalize(actual); + Assert.Equal(expected, actual); + } - [Theory] - [InlineData("en-US", "1994-09-06T15:01:23Z", "3:01:23 PM")] - [InlineData("da-DK", "1994-09-06T15:01:23Z", "15.01.23")] - public void TimeFormatter_Default(string locale, string dateStr, string expected) + [Theory] + [InlineData("en-US", "1994-09-06T15:01:23Z", "3:01:23 PM")] + [InlineData("da-DK", "1994-09-06T15:01:23Z", "15.01.23")] + public void TimeFormatter_Default(string locale, string dateStr, string expected) + { + var mf = new MessageFormatter(locale: locale); + var actual = mf.FormatMessage("{value, time}", new { - var mf = new MessageFormatter(locale: locale); - var actual = mf.FormatMessage("{value, time}", new - { - value = DateTimeOffset.Parse(dateStr) - }); + value = DateTimeOffset.Parse(dateStr) + }); - // Replacing all whitespace due to a difference in formatting on macOS vs Linux. - expected = Normalize(expected); - actual = Normalize(actual); - Assert.Equal(expected, actual); - } + // Replacing all whitespace due to a difference in formatting on macOS vs Linux. + expected = Normalize(expected); + actual = Normalize(actual); + Assert.Equal(expected, actual); + } - [Fact] - public void TimeFormatter_UnsupportedStyle() - { - var mf = new MessageFormatter(); - Assert.Throws( - () => mf.FormatMessage("{value, time, lol}", new - { - value = DateTimeOffset.UtcNow - })); - } + [Fact] + public void TimeFormatter_UnsupportedStyle() + { + var mf = new MessageFormatter(); + Assert.Throws( + () => mf.FormatMessage("{value, time, lol}", new + { + value = DateTimeOffset.UtcNow + })); + } - [Fact] - public void TimeFormatter_Custom() + [Fact] + public void TimeFormatter_Custom() + { + var formatter = new CustomValueFormatters { - var formatter = new CustomValueFormatters - { - Time = (CultureInfo _, object? value, string? _, out string? formatted) => - { - formatted = $"{value:hmm} nice"; - return true; - } - }; - var mf = new MessageFormatter(locale: "en-US", customValueFormatter: formatter); - var actual = mf.FormatMessage("{value, time, long}", new + Time = (CultureInfo _, object? value, string? _, out string? formatted) => { - value = DateTimeOffset.Parse("1994-09-06T16:20:09Z") - }); + formatted = $"{value:hmm} nice"; + return true; + } + }; + var mf = new MessageFormatter(locale: "en-US", customValueFormatter: formatter); + var actual = mf.FormatMessage("{value, time, long}", new + { + value = DateTimeOffset.Parse("1994-09-06T16:20:09Z") + }); - Assert.Equal("420 nice", actual); - } + Assert.Equal("420 nice", actual); + } - [GeneratedRegex("\\s")] - private static partial Regex WhitespaceRegex(); + [GeneratedRegex("\\s")] + private static partial Regex WhitespaceRegex(); - private static string Normalize(string input) - { - return WhitespaceRegex().Replace(input, string.Empty); - } + private static string Normalize(string input) + { + return WhitespaceRegex().Replace(input, string.Empty); } } \ No newline at end of file diff --git a/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/VariableFormatterTests.cs b/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/VariableFormatterTests.cs index 6c0232b..9cd26c7 100644 --- a/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/VariableFormatterTests.cs +++ b/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/VariableFormatterTests.cs @@ -12,82 +12,81 @@ using Xunit; -namespace Jeffijoe.MessageFormat.Tests.Formatting.Formatters +namespace Jeffijoe.MessageFormat.Tests.Formatting.Formatters; + +/// +/// The variable formatter tests. +/// +public class VariableFormatterTests { + #region Fields + + /// + /// The subject. + /// + private readonly VariableFormatter subject; + + /// + /// The fake message formatter. + /// + private readonly IMessageFormatter formatter; + + #endregion + + #region Constructors and Destructors + + /// + /// Initializes a new instance of the class. + /// + public VariableFormatterTests() + { + this.formatter = new FakeMessageFormatter(); + this.subject = new VariableFormatter(); + } + + #endregion + + #region Public Methods and Operators + + /// + /// Verifies that an empty string is returned when the argument is null. + /// + [Fact] + public void VerifyAnEmptyStringIsReturnedWhenTheArgumentIsNull() + { + var req = CreateRequest(); + var args = new Dictionary(); + + Assert.Equal(string.Empty, this.subject.Format("en", req, args, null, this.formatter)); + } + + /// + /// Verifies that the value from the given arguments is returned as a string. + /// + [Fact] + public void VerifyTheValueFromTheGivenArgumentsIsReturnedAsAString() + { + var req = CreateRequest(); + var args = new Dictionary(); + + Assert.Equal("is good", this.subject.Format("en", req, args, "is good", this.formatter)); + } + + #endregion + + #region Methods + /// - /// The variable formatter tests. + /// Creates the request. /// - public class VariableFormatterTests + /// + /// The . + /// + private static FormatterRequest CreateRequest() { - #region Fields - - /// - /// The subject. - /// - private readonly VariableFormatter subject; - - /// - /// The fake message formatter. - /// - private readonly IMessageFormatter formatter; - - #endregion - - #region Constructors and Destructors - - /// - /// Initializes a new instance of the class. - /// - public VariableFormatterTests() - { - this.formatter = new FakeMessageFormatter(); - this.subject = new VariableFormatter(); - } - - #endregion - - #region Public Methods and Operators - - /// - /// Verifies that an empty string is returned when the argument is null. - /// - [Fact] - public void VerifyAnEmptyStringIsReturnedWhenTheArgumentIsNull() - { - var req = CreateRequest(); - var args = new Dictionary(); - - Assert.Equal(string.Empty, this.subject.Format("en", req, args, null, this.formatter)); - } - - /// - /// Verifies that the value from the given arguments is returned as a string. - /// - [Fact] - public void VerifyTheValueFromTheGivenArgumentsIsReturnedAsAString() - { - var req = CreateRequest(); - var args = new Dictionary(); - - Assert.Equal("is good", this.subject.Format("en", req, args, "is good", this.formatter)); - } - - #endregion - - #region Methods - - /// - /// Creates the request. - /// - /// - /// The . - /// - private static FormatterRequest CreateRequest() - { - var req = new FormatterRequest(new Literal(1, 10, 1, 1, ""), "test", null, null); - return req; - } - - #endregion + var req = new FormatterRequest(new Literal(1, 10, 1, 1, ""), "test", null, null); + return req; } + + #endregion } \ No newline at end of file diff --git a/src/Jeffijoe.MessageFormat.Tests/Helpers/CharHelperTests.cs b/src/Jeffijoe.MessageFormat.Tests/Helpers/CharHelperTests.cs index cef0319..361d681 100644 --- a/src/Jeffijoe.MessageFormat.Tests/Helpers/CharHelperTests.cs +++ b/src/Jeffijoe.MessageFormat.Tests/Helpers/CharHelperTests.cs @@ -8,31 +8,30 @@ using Xunit; -namespace Jeffijoe.MessageFormat.Tests.Helpers +namespace Jeffijoe.MessageFormat.Tests.Helpers; + +/// +/// The char helper tests. +/// +public class CharHelperTests { + #region Public Methods and Operators + /// - /// The char helper tests. + /// The is alpha numeric. /// - public class CharHelperTests + [Fact] + public void IsAlphaNumeric() { - #region Public Methods and Operators - - /// - /// The is alpha numeric. - /// - [Fact] - public void IsAlphaNumeric() - { - Assert.True('a'.IsAlphaNumeric()); - Assert.True('A'.IsAlphaNumeric()); - Assert.True('0'.IsAlphaNumeric()); - Assert.True('1'.IsAlphaNumeric()); - Assert.False('ä'.IsAlphaNumeric()); - Assert.False('ø'.IsAlphaNumeric()); - Assert.False('æ'.IsAlphaNumeric()); - Assert.False('å'.IsAlphaNumeric()); - } - - #endregion + Assert.True('a'.IsAlphaNumeric()); + Assert.True('A'.IsAlphaNumeric()); + Assert.True('0'.IsAlphaNumeric()); + Assert.True('1'.IsAlphaNumeric()); + Assert.False('ä'.IsAlphaNumeric()); + Assert.False('ø'.IsAlphaNumeric()); + Assert.False('æ'.IsAlphaNumeric()); + Assert.False('å'.IsAlphaNumeric()); } + + #endregion } \ No newline at end of file diff --git a/src/Jeffijoe.MessageFormat.Tests/Helpers/ObjectHelperTests.cs b/src/Jeffijoe.MessageFormat.Tests/Helpers/ObjectHelperTests.cs index a6d6652..002e1b4 100644 --- a/src/Jeffijoe.MessageFormat.Tests/Helpers/ObjectHelperTests.cs +++ b/src/Jeffijoe.MessageFormat.Tests/Helpers/ObjectHelperTests.cs @@ -14,118 +14,117 @@ // ReSharper disable UnusedMember.Local -namespace Jeffijoe.MessageFormat.Tests.Helpers +namespace Jeffijoe.MessageFormat.Tests.Helpers; + +/// +/// The object helper tests. +/// +public class ObjectHelperTests { + #region Fields + /// - /// The object helper tests. + /// The output helper. /// - public class ObjectHelperTests - { - #region Fields + private readonly ITestOutputHelper outputHelper; - /// - /// The output helper. - /// - private readonly ITestOutputHelper outputHelper; - - #endregion + #endregion - #region Constructors and Destructors + #region Constructors and Destructors - /// - /// Initializes a new instance of the class. - /// - /// - /// The output helper. - /// - public ObjectHelperTests(ITestOutputHelper outputHelper) - { - this.outputHelper = outputHelper; - } + /// + /// Initializes a new instance of the class. + /// + /// + /// The output helper. + /// + public ObjectHelperTests(ITestOutputHelper outputHelper) + { + this.outputHelper = outputHelper; + } - #endregion + #endregion - #region Public Methods and Operators + #region Public Methods and Operators - /// - /// The get properties_anonymous_and_dynamic. - /// - [Fact] - public void GetProperties_anonymous_and_dynamic() - { - var obj = new { Test = "wee", Toast = "woo" }; - var actual = ObjectHelper.GetProperties(obj); - Assert.Equal(2, actual.Count()); + /// + /// The get properties_anonymous_and_dynamic. + /// + [Fact] + public void GetProperties_anonymous_and_dynamic() + { + var obj = new { Test = "wee", Toast = "woo" }; + var actual = ObjectHelper.GetProperties(obj); + Assert.Equal(2, actual.Count()); - dynamic d = new { Cool = "sweet" }; - actual = ObjectHelper.GetProperties(d); - Assert.Single(actual); - } + dynamic d = new { Cool = "sweet" }; + actual = ObjectHelper.GetProperties(d); + Assert.Single(actual); + } - /// - /// The get properties_base_and_derived. - /// - [Fact] - public void GetProperties_base_and_derived() - { - var actual = ObjectHelper.GetProperties(new Base()); - Assert.Single(actual); + /// + /// The get properties_base_and_derived. + /// + [Fact] + public void GetProperties_base_and_derived() + { + var actual = ObjectHelper.GetProperties(new Base()); + Assert.Single(actual); - actual = ObjectHelper.GetProperties(new Derived()); - Assert.Equal(2, actual.Count()); - } + actual = ObjectHelper.GetProperties(new Derived()); + Assert.Equal(2, actual.Count()); + } - /// - /// The to dictionary. - /// - [Fact] - public void ToDictionary() + /// + /// The to dictionary. + /// + [Fact] + public void ToDictionary() + { + var obj = new { name = "test", num = 1337 }; + var actual = obj.ToDictionary(); + Assert.Equal(2, actual.Count); + Assert.Equal("test", actual["name"]); + Assert.Equal(1337, actual["num"]); + + Benchmark.Start("Converting object to dictionary..", this.outputHelper); + for (int i = 0; i < 10000; i++) { - var obj = new { name = "test", num = 1337 }; - var actual = obj.ToDictionary(); - Assert.Equal(2, actual.Count); - Assert.Equal("test", actual["name"]); - Assert.Equal(1337, actual["num"]); - - Benchmark.Start("Converting object to dictionary..", this.outputHelper); - for (int i = 0; i < 10000; i++) - { - obj.ToDictionary(); - } - - Benchmark.End(this.outputHelper); + obj.ToDictionary(); } - #endregion + Benchmark.End(this.outputHelper); + } + + #endregion + + /// + /// The base. + /// + private class Base + { + #region Public Properties /// - /// The base. + /// Gets or sets the prop 1. /// - private class Base - { - #region Public Properties + public string? Prop1 { get; set; } - /// - /// Gets or sets the prop 1. - /// - public string? Prop1 { get; set; } + #endregion + } - #endregion - } + /// + /// The derived. + /// + private class Derived : Base + { + #region Public Properties /// - /// The derived. + /// Gets or sets the prop 2. /// - private class Derived : Base - { - #region Public Properties + public int Prop2 { get; set; } - /// - /// Gets or sets the prop 2. - /// - public int Prop2 { get; set; } - - #endregion - } + #endregion } } \ No newline at end of file diff --git a/src/Jeffijoe.MessageFormat.Tests/Helpers/StringBuilderHelperTests.cs b/src/Jeffijoe.MessageFormat.Tests/Helpers/StringBuilderHelperTests.cs index a47fe8d..fedfdd1 100644 --- a/src/Jeffijoe.MessageFormat.Tests/Helpers/StringBuilderHelperTests.cs +++ b/src/Jeffijoe.MessageFormat.Tests/Helpers/StringBuilderHelperTests.cs @@ -10,78 +10,77 @@ using Xunit; -namespace Jeffijoe.MessageFormat.Tests.Helpers +namespace Jeffijoe.MessageFormat.Tests.Helpers; + +/// +/// The string builder helper tests. +/// +public class StringBuilderHelperTests { + #region Public Methods and Operators + /// - /// The string builder helper tests. + /// The contains. /// - public class StringBuilderHelperTests + /// + /// The src. + /// + /// + /// The c. + /// + /// + /// The expected. + /// + [Theory] + [InlineData("hello ", ' ', true)] + [InlineData("hello ", 'l', true)] + [InlineData("hello ", 'p', false)] + public void Contains(string src, char c, bool expected) { - #region Public Methods and Operators - - /// - /// The contains. - /// - /// - /// The src. - /// - /// - /// The c. - /// - /// - /// The expected. - /// - [Theory] - [InlineData("hello ", ' ', true)] - [InlineData("hello ", 'l', true)] - [InlineData("hello ", 'p', false)] - public void Contains(string src, char c, bool expected) - { - Assert.Equal(expected, new StringBuilder(src).Contains(c)); - } - - /// - /// The contains whitespace. - /// - /// - /// The src. - /// - /// - /// The expected. - /// - [Theory] - [InlineData(" hello", true)] - [InlineData(" hello ", true)] - [InlineData("hello ", true)] - [InlineData("Hi", false)] - public void ContainsWhitespace(string src, bool expected) - { - Assert.Equal(expected, new StringBuilder(src).ContainsWhitespace()); - } + Assert.Equal(expected, new StringBuilder(src).Contains(c)); + } - /// - /// The trim whitespace. - /// - /// - /// The input. - /// - /// - /// The expected. - /// - [Theory] - [InlineData(" dawg ", "dawg")] - [InlineData(" dawg dawg ", "dawg dawg")] - [InlineData(" dawg dawg", "dawg dawg")] - [InlineData("dawg dawg ", "dawg dawg")] - [InlineData(" dawg dawg ", "dawg dawg")] - [InlineData(" dawg dawg", "dawg dawg")] - [InlineData("dawg dawg", "dawg dawg")] - public void TrimWhitespace(string input, string expected) - { - string actual = new StringBuilder(input).TrimWhitespace().ToString(); - Assert.Equal(expected, actual); - } + /// + /// The contains whitespace. + /// + /// + /// The src. + /// + /// + /// The expected. + /// + [Theory] + [InlineData(" hello", true)] + [InlineData(" hello ", true)] + [InlineData("hello ", true)] + [InlineData("Hi", false)] + public void ContainsWhitespace(string src, bool expected) + { + Assert.Equal(expected, new StringBuilder(src).ContainsWhitespace()); + } - #endregion + /// + /// The trim whitespace. + /// + /// + /// The input. + /// + /// + /// The expected. + /// + [Theory] + [InlineData(" dawg ", "dawg")] + [InlineData(" dawg dawg ", "dawg dawg")] + [InlineData(" dawg dawg", "dawg dawg")] + [InlineData("dawg dawg ", "dawg dawg")] + [InlineData(" dawg dawg ", "dawg dawg")] + [InlineData(" dawg dawg", "dawg dawg")] + [InlineData("dawg dawg", "dawg dawg")] + public void TrimWhitespace(string input, string expected) + { + string actual = new StringBuilder(input).TrimWhitespace().ToString(); + Assert.Equal(expected, actual); } + + #endregion } \ No newline at end of file diff --git a/src/Jeffijoe.MessageFormat.Tests/MessageFormatterCachingTests.cs b/src/Jeffijoe.MessageFormat.Tests/MessageFormatterCachingTests.cs index 69123b6..be7f893 100644 --- a/src/Jeffijoe.MessageFormat.Tests/MessageFormatterCachingTests.cs +++ b/src/Jeffijoe.MessageFormat.Tests/MessageFormatterCachingTests.cs @@ -8,43 +8,42 @@ using Jeffijoe.MessageFormat.Tests.TestHelpers; using Xunit; -namespace Jeffijoe.MessageFormat.Tests +namespace Jeffijoe.MessageFormat.Tests; + +/// +/// The message formatter_caching_tests. +/// +public class MessageFormatterCachingTests { + #region Public Methods and Operators + /// - /// The message formatter_caching_tests. + /// The format message_caches_reused_pattern. /// - public class MessageFormatterCachingTests() + [Fact] + public void FormatMessage_caches_reused_pattern() { - #region Public Methods and Operators - - /// - /// The format message_caches_reused_pattern. - /// - [Fact] - public void FormatMessage_caches_reused_pattern() - { - var parser = new TrackingPatternParser(); - var library = new FormatterLibrary(); - - var subject = new MessageFormatter(patternParser: parser, library: library, useCache: true); - - var pattern = "Hi {gender, select, male {Sir} female {Ma'am}}!"; - var actual = subject.FormatMessage(pattern, new { gender = "male" }); - Assert.Equal("Hi Sir!", actual); - - // '2' because it did not format "Ma'am" yet. - Assert.Equal(2, parser.ParseCount); - - actual = subject.FormatMessage(pattern, new { gender = "female" }); - Assert.Equal("Hi Ma'am!", actual); - Assert.Equal(3, parser.ParseCount); - - // '3' because it has cached all options - actual = subject.FormatMessage(pattern, new { gender = "female" }); - Assert.Equal("Hi Ma'am!", actual); - Assert.Equal(3, parser.ParseCount); - } - - #endregion + var parser = new TrackingPatternParser(); + var library = new FormatterLibrary(); + + var subject = new MessageFormatter(patternParser: parser, library: library, useCache: true); + + var pattern = "Hi {gender, select, male {Sir} female {Ma'am}}!"; + var actual = subject.FormatMessage(pattern, new { gender = "male" }); + Assert.Equal("Hi Sir!", actual); + + // '2' because it did not format "Ma'am" yet. + Assert.Equal(2, parser.ParseCount); + + actual = subject.FormatMessage(pattern, new { gender = "female" }); + Assert.Equal("Hi Ma'am!", actual); + Assert.Equal(3, parser.ParseCount); + + // '3' because it has cached all options + actual = subject.FormatMessage(pattern, new { gender = "female" }); + Assert.Equal("Hi Ma'am!", actual); + Assert.Equal(3, parser.ParseCount); } + + #endregion } \ No newline at end of file diff --git a/src/Jeffijoe.MessageFormat.Tests/MessageFormatterFullIntegrationTests.cs b/src/Jeffijoe.MessageFormat.Tests/MessageFormatterFullIntegrationTests.cs index 8592c39..6651ff3 100644 --- a/src/Jeffijoe.MessageFormat.Tests/MessageFormatterFullIntegrationTests.cs +++ b/src/Jeffijoe.MessageFormat.Tests/MessageFormatterFullIntegrationTests.cs @@ -10,143 +10,143 @@ using Xunit; using Xunit.Abstractions; -namespace Jeffijoe.MessageFormat.Tests +namespace Jeffijoe.MessageFormat.Tests; + +/// +/// The message formatter_full_integration_tests. +/// +public class MessageFormatterFullIntegrationTests { + #region Fields + /// - /// The message formatter_full_integration_tests. + /// The output helper. /// - public class MessageFormatterFullIntegrationTests - { - #region Fields + private readonly ITestOutputHelper outputHelper; - /// - /// The output helper. - /// - private readonly ITestOutputHelper outputHelper; + #endregion - #endregion + #region Constructors and Destructors - #region Constructors and Destructors - - /// - /// Initializes a new instance of the class. - /// - /// - /// The output helper. - /// - public MessageFormatterFullIntegrationTests(ITestOutputHelper outputHelper) - { - this.outputHelper = outputHelper; - } + /// + /// Initializes a new instance of the class. + /// + /// + /// The output helper. + /// + public MessageFormatterFullIntegrationTests(ITestOutputHelper outputHelper) + { + this.outputHelper = outputHelper; + } - #endregion + #endregion - #region Public Properties + #region Public Properties - public static IEnumerable EscapingTests + public static IEnumerable EscapingTests + { + get { - get - { - yield return - new object[] - { - "This '{isn''t}' obvious", - new Dictionary(), - "This {isn't} obvious" - }; - yield return - new object[] - { - "Anna's house has '{0} and # in the roof' and {NUM_COWS} cows.", - new Dictionary { { "NUM_COWS", 5 } }, - "Anna's house has {0} and # in the roof and 5 cows." - }; - yield return - new object[] - { - "Anna's house has '{'0'} and # in the roof' and {NUM_COWS} cows.", - new Dictionary { { "NUM_COWS", 5 } }, - "Anna's house has {0} and # in the roof and 5 cows." - }; - yield return - new object[] - { - "Anna's house has '{0}' and '# in the roof' and {NUM_COWS} cows.", - new Dictionary { { "NUM_COWS", 5 } }, - "Anna's house has {0} and # in the roof and 5 cows." - }; - yield return - new object[] - { - "Anna's house 'has {NUM_COWS} cows'.", - new Dictionary { { "NUM_COWS", 5 } }, - "Anna's house 'has 5 cows'." - }; - yield return - new object[] - { - "Anna''s house a'{''''b'", - new Dictionary(), - "Anna's house a{''b" - }; - yield return - new object[] - { - "a''{NUM_COWS}'b", - new Dictionary { { "NUM_COWS", 5 } }, - "a'5'b" - }; - yield return - new object[] - { - "a'{NUM_COWS}'b'", - new Dictionary { { "NUM_COWS", 5 } }, - "a{NUM_COWS}b'" - }; - yield return - new object[] - { - "These '{'braces'}' and thoses '{braces}' ain''t not escaped, which makes a total of {braces, plural, one {a single pair} other {'#'# (=#) pairs}} of escaped braces.", - new Dictionary { { "braces", 2 } }, - "These {braces} and thoses {braces} ain't not escaped, which makes a total of #2 (=2) pairs of escaped braces." - }; - yield return - new object[] - { - "{num, plural, =1 {1} other {'#'{num, plural, =1 {1} other {'{'#'#'#'}'}}}}", - new Dictionary { { "num", 2 } }, - "#{2#2}" - }; - yield return - new object[] - { - "'''{'''", - new Dictionary(), - "'{'" - }; - // yield return - // new object[] - // { - // "{num, plural, =1 {1} other {'''{'''#'''}'''}}", - // new Dictionary { { "num", 2 } }, - // "'{'2'}'" - // }; - } + yield return + new object[] + { + "This '{isn''t}' obvious", + new Dictionary(), + "This {isn't} obvious" + }; + yield return + new object[] + { + "Anna's house has '{0} and # in the roof' and {NUM_COWS} cows.", + new Dictionary { { "NUM_COWS", 5 } }, + "Anna's house has {0} and # in the roof and 5 cows." + }; + yield return + new object[] + { + "Anna's house has '{'0'} and # in the roof' and {NUM_COWS} cows.", + new Dictionary { { "NUM_COWS", 5 } }, + "Anna's house has {0} and # in the roof and 5 cows." + }; + yield return + new object[] + { + "Anna's house has '{0}' and '# in the roof' and {NUM_COWS} cows.", + new Dictionary { { "NUM_COWS", 5 } }, + "Anna's house has {0} and # in the roof and 5 cows." + }; + yield return + new object[] + { + "Anna's house 'has {NUM_COWS} cows'.", + new Dictionary { { "NUM_COWS", 5 } }, + "Anna's house 'has 5 cows'." + }; + yield return + new object[] + { + "Anna''s house a'{''''b'", + new Dictionary(), + "Anna's house a{''b" + }; + yield return + new object[] + { + "a''{NUM_COWS}'b", + new Dictionary { { "NUM_COWS", 5 } }, + "a'5'b" + }; + yield return + new object[] + { + "a'{NUM_COWS}'b'", + new Dictionary { { "NUM_COWS", 5 } }, + "a{NUM_COWS}b'" + }; + yield return + new object[] + { + "These '{'braces'}' and thoses '{braces}' ain''t not escaped, which makes a total of {braces, plural, one {a single pair} other {'#'# (=#) pairs}} of escaped braces.", + new Dictionary { { "braces", 2 } }, + "These {braces} and thoses {braces} ain't not escaped, which makes a total of #2 (=2) pairs of escaped braces." + }; + yield return + new object[] + { + "{num, plural, =1 {1} other {'#'{num, plural, =1 {1} other {'{'#'#'#'}'}}}}", + new Dictionary { { "num", 2 } }, + "#{2#2}" + }; + yield return + new object[] + { + "'''{'''", + new Dictionary(), + "'{'" + }; + // yield return + // new object[] + // { + // "{num, plural, =1 {1} other {'''{'''#'''}'''}}", + // new Dictionary { { "num", 2 } }, + // "'{'2'}'" + // }; } + } - /// - /// Gets the tests. - /// - public static IEnumerable Tests + /// + /// Gets the tests. + /// + public static IEnumerable Tests + { + get { - get - { - const string Case1 = @"{gender, select, + const string Case1 = @"{gender, select, male {He - '{'{name}'}' -} female {She - '{'{name}'}' -} other {They} } said: You're pretty cool!"; - const string Case2 = @"{gender, select, + const string Case2 = @"{gender, select, male {He - '{'{name}'}' -} female {She - '{'{name}'}' -} other {They} @@ -156,13 +156,13 @@ public static IEnumerable Tests =42 {a universal amount of notifications} other {# notifications} }. Have a nice day!"; - const string Case3 = @"You have {count, plural, + const string Case3 = @"You have {count, plural, zero {no notifications} one {just one notification} =42 {a universal amount of notifications} other {# notifications} }. Have a nice day!"; - const string Case4 = @"{gender, select, + const string Case4 = @"{gender, select, male {He} female {She} other {They} @@ -173,8 +173,8 @@ public static IEnumerable Tests other {# notifications} }. Have a nice day!"; - // Please take the following sample in the spirit it was intended. :) - const string Case5 = @"{gender, select, + // Please take the following sample in the spirit it was intended. :) + const string Case5 = @"{gender, select, male {He (who has {genitals, plural, zero {no testicles} one {just one testicle} @@ -195,220 +195,220 @@ public static IEnumerable Tests other {# notifications} }. Have a nice day!"; - const string Case6 = @"You {count, plural, offset:1, + const string Case6 = @"You {count, plural, offset:1, =0{didn't add this to your profile} =1{added this to your profile} one {and one other person added this to their profile} other {and # others added this to their profiles} }."; - yield return - new object[] - { - Case1, - new Dictionary { { "gender", "male" }, { "name", "Jeff" } }, - "He - {Jeff} - said: You're pretty cool!" - }; - yield return - new object[] - { - Case2, - new Dictionary { { "gender", "male" }, { "name", "Jeff" }, { "count", 0 } }, - "He - {Jeff} - said: You have no notifications. Have a nice day!" - }; - yield return - new object[] - { - Case2, - new Dictionary - { { "gender", "female" }, { "name", "Amanda" }, { "count", 1 } }, - "She - {Amanda} - said: You have just one notification. Have a nice day!" - }; - yield return - new object[] - { - Case2, - new Dictionary { { "gender", "uni" }, { "count", 42 } }, - "They said: You have a universal amount of notifications. Have a nice day!" - }; - yield return - new object[] - { - Case3, - new Dictionary { { "count", 5 } }, - "You have 5 notifications. Have a nice day!" - }; - yield return - new object[] - { - Case4, - new Dictionary { { "count", 5 }, { "gender", "male" } }, - "He said: You have 5 notifications. Have a nice day!" - }; - yield return - new object[] - { - Case5, - new Dictionary { { "count", 5 }, { "gender", "male" }, { "genitals", 0 } }, - "He (who has no testicles) said: You have 5 notifications. Have a nice day!" - }; - yield return - new object[] - { - Case5, - new Dictionary { { "count", 5 }, { "gender", "female" }, { "genitals", 0 } }, - "She (who has no boobies) said: You have 5 notifications. Have a nice day!" - }; - yield return - new object[] - { - Case5, - new Dictionary { { "count", 0 }, { "gender", "female" }, { "genitals", 1 } }, - "She (who has just one boob) said: You have no notifications. Have a nice day!" - }; - yield return - new object[] - { - Case5, - new Dictionary { { "count", 0 }, { "gender", "female" }, { "genitals", 2 } }, - "She (who has a pair of lovelies) said: You have no notifications. Have a nice day!" - }; - yield return - new object[] - { - Case5, - new Dictionary { { "count", 0 }, { "gender", "female" }, { "genitals", 102 } }, - "She (who has the freakish amount of 102 boobies) said: You have no notifications. Have a nice day!" - }; - yield return - new object[] - { - Case5, - new Dictionary - { { "count", 42 }, { "gender", "female" }, { "genitals", 102 } }, - "She (who has the freakish amount of 102 boobies) said: You have a universal amount of notifications. Have a nice day!" - }; - yield return - new object[] - { - Case5, - new Dictionary { { "count", 1 }, { "gender", "male" }, { "genitals", 102 } }, - "He (who has the insane amount of 102 testicles) said: You have just one notification. Have a nice day!" - }; - - // Case from https://github.com/jeffijoe/messageformat.net/issues/2 - yield return - new object[] - { - "{nbrAttachments, plural, zero {} one {{nbrAttachmentsFmt} attachment} other {{nbrAttachmentsFmt} attachments}}", - new Dictionary { { "nbrAttachments", 0 }, { "nbrAttachmentsFmt", "wut" } }, - string.Empty - }; - - // Following 2 cases from https://github.com/jeffijoe/messageformat.net/issues/4 - yield return - new object[] - { - "{maybeCount}", - new Dictionary { { "maybeCount", null } }, - string.Empty - }; - yield return - new object[] - { - "{maybeCount}", - new Dictionary { { "maybeCount", (int?)2 } }, - "2" - }; - yield return - new object[] - { - Case6, - new Dictionary { { "count", 0 } }, - "You didn't add this to your profile." - }; - yield return - new object[] - { - Case6, - new Dictionary { { "count", 1 } }, - "You added this to your profile." - }; - yield return - new object[] - { - Case6, - new Dictionary { { "count", 2 } }, - "You and one other person added this to their profile." - }; - yield return - new object[] - { - Case6, - new Dictionary { { "count", 3 } }, - "You and 2 others added this to their profiles." - }; - yield return - new object[] - { - "{ count, plural, one {1 thing} other {# things} }", - new Dictionary { { "count", 2 } }, - "2 things" - }; - } - } + yield return + new object[] + { + Case1, + new Dictionary { { "gender", "male" }, { "name", "Jeff" } }, + "He - {Jeff} - said: You're pretty cool!" + }; + yield return + new object[] + { + Case2, + new Dictionary { { "gender", "male" }, { "name", "Jeff" }, { "count", 0 } }, + "He - {Jeff} - said: You have no notifications. Have a nice day!" + }; + yield return + new object[] + { + Case2, + new Dictionary + { { "gender", "female" }, { "name", "Amanda" }, { "count", 1 } }, + "She - {Amanda} - said: You have just one notification. Have a nice day!" + }; + yield return + new object[] + { + Case2, + new Dictionary { { "gender", "uni" }, { "count", 42 } }, + "They said: You have a universal amount of notifications. Have a nice day!" + }; + yield return + new object[] + { + Case3, + new Dictionary { { "count", 5 } }, + "You have 5 notifications. Have a nice day!" + }; + yield return + new object[] + { + Case4, + new Dictionary { { "count", 5 }, { "gender", "male" } }, + "He said: You have 5 notifications. Have a nice day!" + }; + yield return + new object[] + { + Case5, + new Dictionary { { "count", 5 }, { "gender", "male" }, { "genitals", 0 } }, + "He (who has no testicles) said: You have 5 notifications. Have a nice day!" + }; + yield return + new object[] + { + Case5, + new Dictionary { { "count", 5 }, { "gender", "female" }, { "genitals", 0 } }, + "She (who has no boobies) said: You have 5 notifications. Have a nice day!" + }; + yield return + new object[] + { + Case5, + new Dictionary { { "count", 0 }, { "gender", "female" }, { "genitals", 1 } }, + "She (who has just one boob) said: You have no notifications. Have a nice day!" + }; + yield return + new object[] + { + Case5, + new Dictionary { { "count", 0 }, { "gender", "female" }, { "genitals", 2 } }, + "She (who has a pair of lovelies) said: You have no notifications. Have a nice day!" + }; + yield return + new object[] + { + Case5, + new Dictionary { { "count", 0 }, { "gender", "female" }, { "genitals", 102 } }, + "She (who has the freakish amount of 102 boobies) said: You have no notifications. Have a nice day!" + }; + yield return + new object[] + { + Case5, + new Dictionary + { { "count", 42 }, { "gender", "female" }, { "genitals", 102 } }, + "She (who has the freakish amount of 102 boobies) said: You have a universal amount of notifications. Have a nice day!" + }; + yield return + new object[] + { + Case5, + new Dictionary { { "count", 1 }, { "gender", "male" }, { "genitals", 102 } }, + "He (who has the insane amount of 102 testicles) said: You have just one notification. Have a nice day!" + }; - #endregion - - #region Public Methods and Operators - - /// - /// The format message. - /// - /// - /// The source. - /// - /// - /// The args. - /// - /// - /// The expected. - /// - [Theory] - [MemberData(nameof(Tests))] - public void FormatMessage(string source, Dictionary args, string expected) - { - var subject = new MessageFormatter(false); - - // Warmup - subject.FormatMessage(source, args); - Benchmark.Start("Formatting", this.outputHelper); - string result = subject.FormatMessage(source, args); - Benchmark.End(this.outputHelper); - Assert.Equal(expected, result); - this.outputHelper.WriteLine(result); + // Case from https://github.com/jeffijoe/messageformat.net/issues/2 + yield return + new object[] + { + "{nbrAttachments, plural, zero {} one {{nbrAttachmentsFmt} attachment} other {{nbrAttachmentsFmt} attachments}}", + new Dictionary { { "nbrAttachments", 0 }, { "nbrAttachmentsFmt", "wut" } }, + string.Empty + }; + + // Following 2 cases from https://github.com/jeffijoe/messageformat.net/issues/4 + yield return + new object[] + { + "{maybeCount}", + new Dictionary { { "maybeCount", null } }, + string.Empty + }; + yield return + new object[] + { + "{maybeCount}", + new Dictionary { { "maybeCount", (int?)2 } }, + "2" + }; + yield return + new object[] + { + Case6, + new Dictionary { { "count", 0 } }, + "You didn't add this to your profile." + }; + yield return + new object[] + { + Case6, + new Dictionary { { "count", 1 } }, + "You added this to your profile." + }; + yield return + new object[] + { + Case6, + new Dictionary { { "count", 2 } }, + "You and one other person added this to their profile." + }; + yield return + new object[] + { + Case6, + new Dictionary { { "count", 3 } }, + "You and 2 others added this to their profiles." + }; + yield return + new object[] + { + "{ count, plural, one {1 thing} other {# things} }", + new Dictionary { { "count", 2 } }, + "2 things" + }; } + } - /// - /// The format message_debug. - /// - [Theory] - [MemberData(nameof(EscapingTests))] - public void FormatMessage_escaping(string source, Dictionary args, string expected) - { - var subject = new MessageFormatter(false); + #endregion - string result = subject.FormatMessage(source, args); - Assert.Equal(expected, result); - } + #region Public Methods and Operators - /// - /// The format message_debug. - /// - [Fact] - public void FormatMessage_debug() - { - const string Source = @"{gender, select, + /// + /// The format message. + /// + /// + /// The source. + /// + /// + /// The args. + /// + /// + /// The expected. + /// + [Theory] + [MemberData(nameof(Tests))] + public void FormatMessage(string source, Dictionary args, string expected) + { + var subject = new MessageFormatter(false); + + // Warmup + subject.FormatMessage(source, args); + Benchmark.Start("Formatting", this.outputHelper); + string result = subject.FormatMessage(source, args); + Benchmark.End(this.outputHelper); + Assert.Equal(expected, result); + this.outputHelper.WriteLine(result); + } + + /// + /// The format message_debug. + /// + [Theory] + [MemberData(nameof(EscapingTests))] + public void FormatMessage_escaping(string source, Dictionary args, string expected) + { + var subject = new MessageFormatter(false); + + string result = subject.FormatMessage(source, args); + Assert.Equal(expected, result); + } + + /// + /// The format message_debug. + /// + [Fact] + public void FormatMessage_debug() + { + const string Source = @"{gender, select, male {He} female {She} other {They} @@ -418,113 +418,113 @@ public void FormatMessage_debug() =42 {a universal amount of notifications} other {# notifications} }. Have a nice day!"; - const string Expected = "He said: You have 5 notifications. Have a nice day!"; - var args = new Dictionary { { "gender", "male" }, { "count", 5 } }; - var subject = new MessageFormatter(false); + const string Expected = "He said: You have 5 notifications. Have a nice day!"; + var args = new Dictionary { { "gender", "male" }, { "count", 5 } }; + var subject = new MessageFormatter(false); - string result = subject.FormatMessage(Source, args); - Assert.Equal(Expected, result); - } + string result = subject.FormatMessage(Source, args); + Assert.Equal(Expected, result); + } - /// - /// The format message_lets_non_ascii_characters_right_through. - /// - [Fact] - public void FormatMessage_lets_non_ascii_characters_right_through() - { - const string Input = "中test中国话不用彁字。"; - var subject = new MessageFormatter(false); - var actual = subject.FormatMessage(Input, new Dictionary()); - Assert.Equal(Input, actual); - } + /// + /// The format message_lets_non_ascii_characters_right_through. + /// + [Fact] + public void FormatMessage_lets_non_ascii_characters_right_through() + { + const string Input = "中test中国话不用彁字。"; + var subject = new MessageFormatter(false); + var actual = subject.FormatMessage(Input, new Dictionary()); + Assert.Equal(Input, actual); + } - /// - /// The format message_nesting_with_brace_escaping. - /// - [Fact] - public void FormatMessage_nesting_with_brace_escaping() - { - var subject = new MessageFormatter(false); - const string Pattern = @"{s1, select, + /// + /// The format message_nesting_with_brace_escaping. + /// + [Fact] + public void FormatMessage_nesting_with_brace_escaping() + { + var subject = new MessageFormatter(false); + const string Pattern = @"{s1, select, 1 {{s2, select, 2 {'{'} }} }"; - var actual = subject.FormatMessage(Pattern, new { s1 = 1, s2 = 2 }); - this.outputHelper.WriteLine(actual); - Assert.Equal("{", actual); - } + var actual = subject.FormatMessage(Pattern, new { s1 = 1, s2 = 2 }); + this.outputHelper.WriteLine(actual); + Assert.Equal("{", actual); + } - /// - /// The format message_with_reflection_overload. - /// - [Fact] - public void FormatMessage_with_reflection_overload() - { - var subject = new MessageFormatter(false); - const string Pattern = "You have {UnreadCount, plural, " - + "zero {no unread messages}" - + "one {just one unread message}" + "other {# unread messages}" + "} today."; - var actual = subject.FormatMessage(Pattern, new { UnreadCount = 0 }); - Assert.Equal("You have no unread messages today.", actual); - - // The absence of UnreadCount should make it throw. - var ex = Assert.Throws(() => subject.FormatMessage(Pattern, new { })); - Assert.Equal("UnreadCount", ex.MissingVariable); - - actual = subject.FormatMessage(Pattern, new { UnreadCount = 1 }); - Assert.Equal("You have just one unread message today.", actual); - actual = subject.FormatMessage(Pattern, new { UnreadCount = 2 }); - Assert.Equal("You have 2 unread messages today.", actual); - - actual = subject.FormatMessage(Pattern, new { UnreadCount = 3 }); - Assert.Equal("You have 3 unread messages today.", actual); - } + /// + /// The format message_with_reflection_overload. + /// + [Fact] + public void FormatMessage_with_reflection_overload() + { + var subject = new MessageFormatter(false); + const string Pattern = "You have {UnreadCount, plural, " + + "zero {no unread messages}" + + "one {just one unread message}" + "other {# unread messages}" + "} today."; + var actual = subject.FormatMessage(Pattern, new { UnreadCount = 0 }); + Assert.Equal("You have no unread messages today.", actual); + + // The absence of UnreadCount should make it throw. + var ex = Assert.Throws(() => subject.FormatMessage(Pattern, new { })); + Assert.Equal("UnreadCount", ex.MissingVariable); + + actual = subject.FormatMessage(Pattern, new { UnreadCount = 1 }); + Assert.Equal("You have just one unread message today.", actual); + actual = subject.FormatMessage(Pattern, new { UnreadCount = 2 }); + Assert.Equal("You have 2 unread messages today.", actual); + + actual = subject.FormatMessage(Pattern, new { UnreadCount = 3 }); + Assert.Equal("You have 3 unread messages today.", actual); + } - /// - /// The read me_test_to_make_sure_ i_dont_look_like_a_fool. - /// - [Fact] - public void ReadMe_test_to_make_sure_I_dont_look_like_a_fool() + /// + /// The read me_test_to_make_sure_ i_dont_look_like_a_fool. + /// + [Fact] + public void ReadMe_test_to_make_sure_I_dont_look_like_a_fool() + { { - { - var mf = new MessageFormatter(false); - const string Str = @"You have {notifications, plural, + var mf = new MessageFormatter(false); + const string Str = @"You have {notifications, plural, zero {no notifications} one {one notification} =42 {a universal amount of notifications} other {# notifications} }. Have a nice day, {name}!"; - var formatted = mf.FormatMessage( - Str, - new Dictionary { { "notifications", 4 }, { "name", "Jeff" } }); - Assert.Equal("You have 4 notifications. Have a nice day, Jeff!", formatted); - } + var formatted = mf.FormatMessage( + Str, + new Dictionary { { "notifications", 4 }, { "name", "Jeff" } }); + Assert.Equal("You have 4 notifications. Have a nice day, Jeff!", formatted); + } - { - var mf = new MessageFormatter(false); - const string Str = @"You {NUM_ADDS, plural, offset:1 + { + var mf = new MessageFormatter(false); + const string Str = @"You {NUM_ADDS, plural, offset:1 =0{didnt add this to your profile} zero{added this to your profile} one{and one other person added this to their profile} other{and # others added this to their profiles} }."; - var formatted = mf.FormatMessage(Str, new Dictionary { { "NUM_ADDS", 0 } }); - Assert.Equal("You didnt add this to your profile.", formatted); + var formatted = mf.FormatMessage(Str, new Dictionary { { "NUM_ADDS", 0 } }); + Assert.Equal("You didnt add this to your profile.", formatted); - formatted = mf.FormatMessage(Str, new Dictionary { { "NUM_ADDS", 1 } }); - Assert.Equal("You added this to your profile.", formatted); + formatted = mf.FormatMessage(Str, new Dictionary { { "NUM_ADDS", 1 } }); + Assert.Equal("You added this to your profile.", formatted); - formatted = mf.FormatMessage(Str, new Dictionary { { "NUM_ADDS", 2 } }); - Assert.Equal("You and one other person added this to their profile.", formatted); + formatted = mf.FormatMessage(Str, new Dictionary { { "NUM_ADDS", 2 } }); + Assert.Equal("You and one other person added this to their profile.", formatted); - formatted = mf.FormatMessage(Str, new Dictionary { { "NUM_ADDS", 3 } }); - Assert.Equal("You and 2 others added this to their profiles.", formatted); - } + formatted = mf.FormatMessage(Str, new Dictionary { { "NUM_ADDS", 3 } }); + Assert.Equal("You and 2 others added this to their profiles.", formatted); + } - { - var mf = new MessageFormatter(false); - const string Str = @"{GENDER, select, + { + var mf = new MessageFormatter(false); + const string Str = @"{GENDER, select, male {He} female {She} other {They} @@ -535,105 +535,104 @@ public void ReadMe_test_to_make_sure_I_dont_look_like_a_fool() one {1 category} other {# categories} }."; - var formatted = mf.FormatMessage( - Str, - new Dictionary - { - { "GENDER", "male" }, - { "NUM_RESULTS", 1 }, - { "NUM_CATEGORIES", 2 } - }); - Assert.Equal("He found 1 result in 2 categories.", formatted); - - formatted = mf.FormatMessage( - Str, - new Dictionary - { - { "GENDER", "male" }, - { "NUM_RESULTS", 1 }, - { "NUM_CATEGORIES", 1 } - }); - Assert.Equal("He found 1 result in 1 category.", formatted); - - formatted = mf.FormatMessage( - Str, - new Dictionary - { - { "GENDER", "female" }, - { "NUM_RESULTS", 2 }, - { "NUM_CATEGORIES", 1 } - }); - Assert.Equal("She found 2 results in 1 category.", formatted); - } + var formatted = mf.FormatMessage( + Str, + new Dictionary + { + { "GENDER", "male" }, + { "NUM_RESULTS", 1 }, + { "NUM_CATEGORIES", 2 } + }); + Assert.Equal("He found 1 result in 2 categories.", formatted); + + formatted = mf.FormatMessage( + Str, + new Dictionary + { + { "GENDER", "male" }, + { "NUM_RESULTS", 1 }, + { "NUM_CATEGORIES", 1 } + }); + Assert.Equal("He found 1 result in 1 category.", formatted); + + formatted = mf.FormatMessage( + Str, + new Dictionary + { + { "GENDER", "female" }, + { "NUM_RESULTS", 2 }, + { "NUM_CATEGORIES", 1 } + }); + Assert.Equal("She found 2 results in 1 category.", formatted); + } - { - var mf = new MessageFormatter(false); - const string Str = @"Your {NUM, plural, one{message} other{messages}} go here."; - var formatted = mf.FormatMessage(Str, new Dictionary { { "NUM", 1 } }); - Assert.Equal("Your message go here.", formatted); + { + var mf = new MessageFormatter(false); + const string Str = @"Your {NUM, plural, one{message} other{messages}} go here."; + var formatted = mf.FormatMessage(Str, new Dictionary { { "NUM", 1 } }); + Assert.Equal("Your message go here.", formatted); - formatted = mf.FormatMessage(Str, new Dictionary { { "NUM", 3 } }); - Assert.Equal("Your messages go here.", formatted); - } + formatted = mf.FormatMessage(Str, new Dictionary { { "NUM", 3 } }); + Assert.Equal("Your messages go here.", formatted); + } - { - var mf = new MessageFormatter(false); - const string Str = @"His name is {LAST_NAME}... {FIRST_NAME} {LAST_NAME}"; - var formatted = mf.FormatMessage( - Str, - new Dictionary { { "FIRST_NAME", "James" }, { "LAST_NAME", "Bond" } }); - Assert.Equal("His name is Bond... James Bond", formatted); - } + { + var mf = new MessageFormatter(false); + const string Str = @"His name is {LAST_NAME}... {FIRST_NAME} {LAST_NAME}"; + var formatted = mf.FormatMessage( + Str, + new Dictionary { { "FIRST_NAME", "James" }, { "LAST_NAME", "Bond" } }); + Assert.Equal("His name is Bond... James Bond", formatted); + } - { - var mf = new MessageFormatter(false); - const string Str = @"{GENDER, select, male{He} female{She} other{They}} liked this."; - var formatted = mf.FormatMessage(Str, new Dictionary { { "GENDER", "male" } }); - Assert.Equal("He liked this.", formatted); + { + var mf = new MessageFormatter(false); + const string Str = @"{GENDER, select, male{He} female{She} other{They}} liked this."; + var formatted = mf.FormatMessage(Str, new Dictionary { { "GENDER", "male" } }); + Assert.Equal("He liked this.", formatted); - formatted = mf.FormatMessage(Str, new Dictionary { { "GENDER", "female" } }); - Assert.Equal("She liked this.", formatted); + formatted = mf.FormatMessage(Str, new Dictionary { { "GENDER", "female" } }); + Assert.Equal("She liked this.", formatted); - formatted = mf.FormatMessage(Str, new Dictionary { { "GENDER", "somethingelse" } }); - Assert.Equal("They liked this.", formatted); + formatted = mf.FormatMessage(Str, new Dictionary { { "GENDER", "somethingelse" } }); + Assert.Equal("They liked this.", formatted); - formatted = mf.FormatMessage(Str, new Dictionary { { "GENDER", null } }); - Assert.Equal("They liked this.", formatted); - } + formatted = mf.FormatMessage(Str, new Dictionary { { "GENDER", null } }); + Assert.Equal("They liked this.", formatted); + } + { + var mf = new MessageFormatter(useCache: true, locale: "en"); + mf.Pluralizers!["en"] = n => { - var mf = new MessageFormatter(useCache: true, locale: "en"); - mf.Pluralizers!["en"] = n => - { - // ´n´ is the number being pluralized. - // ReSharper disable once CompareOfFloatsByEqualityOperator - if (n == 0) - { - return "zero"; - } - - // ReSharper disable once CompareOfFloatsByEqualityOperator - if (n == 1) - { - return "one"; - } - - if (n > 1000) - { - return "thatsalot"; - } - - return "other"; - }; - - var actual = - mf.FormatMessage( - "You have {number, plural, thatsalot {a shitload of notifications} other {# notifications}}", - new Dictionary { { "number", 1001 } }); - Assert.Equal("You have a shitload of notifications", actual); - } - } + // ´n´ is the number being pluralized. + // ReSharper disable once CompareOfFloatsByEqualityOperator + if (n == 0) + { + return "zero"; + } + + // ReSharper disable once CompareOfFloatsByEqualityOperator + if (n == 1) + { + return "one"; + } - #endregion + if (n > 1000) + { + return "thatsalot"; + } + + return "other"; + }; + + var actual = + mf.FormatMessage( + "You have {number, plural, thatsalot {a shitload of notifications} other {# notifications}}", + new Dictionary { { "number", 1001 } }); + Assert.Equal("You have a shitload of notifications", actual); + } } + + #endregion } \ No newline at end of file diff --git a/src/Jeffijoe.MessageFormat.Tests/MessageFormatterIssues.cs b/src/Jeffijoe.MessageFormat.Tests/MessageFormatterIssues.cs index 2214674..87aeca9 100644 --- a/src/Jeffijoe.MessageFormat.Tests/MessageFormatterIssues.cs +++ b/src/Jeffijoe.MessageFormat.Tests/MessageFormatterIssues.cs @@ -7,70 +7,85 @@ using System.Collections.Generic; using Xunit; -namespace Jeffijoe.MessageFormat.Tests +namespace Jeffijoe.MessageFormat.Tests; + +/// +/// Issue cases. +/// +public class MessageFormatterIssues { - /// - /// Issue cases. - /// - public class MessageFormatterIssues + [Fact] + public void Issue13_Bad_escaping_on_pound_symbol() + { + string plural = @"{num_guests, plural, offset:1, other {# {host} invites # people to their party.}}"; + string broken = @"{num_guests, plural, offset:1, other {{host} invites # people to their party.}}"; + + var mf = new MessageFormatter(); + var vars = new { num_guests = "5", host = "Mary" }; + Assert.Equal("Mary invites 4 people to their party.", mf.FormatMessage(broken, vars)); + Assert.Equal("4 Mary invites 4 people to their party.", mf.FormatMessage(plural, vars)); + } + + [Fact] + public void Issue27_WhiteSpace_in_identifiers_is_ignored() + { + var subject = new MessageFormatter(false); + var result = subject.FormatMessage("{ count, plural , one {1 thing} other {# things} }", new + { + count = 2 + }); + + Assert.Equal("2 things", result); + } + + [Fact] + public void Issue31_IDictionary_interface_support() { - [Fact] - public void Issue13_Bad_escaping_on_pound_symbol() + var subject = new MessageFormatter(locale: "en-US"); + + IDictionary idict = new Dictionary { - string plural = @"{num_guests, plural, offset:1, other {# {host} invites # people to their party.}}"; - string broken = @"{num_guests, plural, offset:1, other {{host} invites # people to their party.}}"; - - var mf = new MessageFormatter(); - var vars = new { num_guests = "5", host = "Mary" }; - Assert.Equal("Mary invites 4 people to their party.", mf.FormatMessage(broken, vars)); - Assert.Equal("4 Mary invites 4 people to their party.", mf.FormatMessage(plural, vars)); - } - - [Fact] - public void Issue27_WhiteSpace_in_identifiers_is_ignored() + ["string"] = "value" + }; + + IDictionary idictNullable = new Dictionary { - var subject = new MessageFormatter(false); - var result = subject.FormatMessage("{ count, plural , one {1 thing} other {# things} }", new + ["string"] = "value" + }; + + Assert.Equal("value", subject.FormatMessage("{string}", idict)); + Assert.Equal("value", subject.FormatMessage("{string}", idictNullable!)); + } + + [Fact] + public void Issue34_Newlines_are_stripped() + { + var subject = new MessageFormatter(locale: "en-US"); + + const string Expected = "Single text which will not change.\nSummary:\nAccepted\nData:\n-X\n-Y\n-Z"; + + var result = subject.FormatMessage( + "Single text which will not change.\nSummary:{acceptedData, select, NONE {} other {\nAccepted\nData:{acceptedData}}}", + new { - count = 2 + acceptedData = "\n-X\n-Y\n-Z" }); + Assert.Equal(Expected, result); + } - Assert.Equal("2 things", result); - } + [Fact] + public void Issue45_Url_should_not_be_parsed_as_extension() + { + var subject = new MessageFormatter(locale: "en-US"); - [Fact] - public void Issue31_IDictionary_interface_support() + IDictionary dict = new Dictionary { - var subject = new MessageFormatter(locale: "en-US"); + ["cond"] = "foo" + }; - IDictionary idict = new Dictionary - { - ["string"] = "value" - }; - - IDictionary idictNullable = new Dictionary - { - ["string"] = "value" - }; - - Assert.Equal("value", subject.FormatMessage("{string}", idict)); - Assert.Equal("value", subject.FormatMessage("{string}", idictNullable!)); - } - - [Fact] - public void Issue34_Newlines_are_stripped() - { - var subject = new MessageFormatter(locale: "en-US"); - - const string Expected = "Single text which will not change.\nSummary:\nAccepted\nData:\n-X\n-Y\n-Z"; - - var result = subject.FormatMessage( - "Single text which will not change.\nSummary:{acceptedData, select, NONE {} other {\nAccepted\nData:{acceptedData}}}", - new - { - acceptedData = "\n-X\n-Y\n-Z" - }); - Assert.Equal(Expected, result); - } + var result = subject.FormatMessage( + "{cond, select, foo{https://www.google.com/} other{https://www.bing.com/}}", + dict); + Assert.Equal("https://www.google.com/", result); } } \ No newline at end of file diff --git a/src/Jeffijoe.MessageFormat.Tests/MessageFormatterStringExtensionTests.cs b/src/Jeffijoe.MessageFormat.Tests/MessageFormatterStringExtensionTests.cs index 4884687..1f55141 100644 --- a/src/Jeffijoe.MessageFormat.Tests/MessageFormatterStringExtensionTests.cs +++ b/src/Jeffijoe.MessageFormat.Tests/MessageFormatterStringExtensionTests.cs @@ -8,37 +8,36 @@ using Xunit; -namespace Jeffijoe.MessageFormat.Tests +namespace Jeffijoe.MessageFormat.Tests; + +/// +/// The message formatter string extension tests. +/// +public class MessageFormatterStringExtensionTests { + #region Public Methods and Operators + /// - /// The message formatter string extension tests. + /// The format message_with_multiple_tasks. /// - public class MessageFormatterStringExtensionTests + /// + /// The . + /// + [Fact] + public async Task FormatMessage_with_multiple_tasks() { - #region Public Methods and Operators - - /// - /// The format message_with_multiple_tasks. - /// - /// - /// The . - /// - [Fact] - public async Task FormatMessage_with_multiple_tasks() - { - var pattern = "Copying {fileCount, plural, one {one file} other{# files}}."; - - // 2 with the same message to test there are no issues with caching with multiple threads. - var t1 = Task.Run(() => MessageFormatter.Format(pattern, new { fileCount = 1 })); - var t2 = Task.Run(() => MessageFormatter.Format(pattern, new { fileCount = 1 })); - var t3 = Task.Run(() => MessageFormatter.Format(pattern, new { fileCount = 5 })); - await Task.WhenAll(t1, t2, t3); - - Assert.Equal("Copying one file.", t1.Result); - Assert.Equal("Copying one file.", t2.Result); - Assert.Equal("Copying 5 files.", t3.Result); - } - - #endregion + var pattern = "Copying {fileCount, plural, one {one file} other{# files}}."; + + // 2 with the same message to test there are no issues with caching with multiple threads. + var t1 = Task.Run(() => MessageFormatter.Format(pattern, new { fileCount = 1 })); + var t2 = Task.Run(() => MessageFormatter.Format(pattern, new { fileCount = 1 })); + var t3 = Task.Run(() => MessageFormatter.Format(pattern, new { fileCount = 5 })); + await Task.WhenAll(t1, t2, t3); + + Assert.Equal("Copying one file.", t1.Result); + Assert.Equal("Copying one file.", t2.Result); + Assert.Equal("Copying 5 files.", t3.Result); } + + #endregion } \ No newline at end of file diff --git a/src/Jeffijoe.MessageFormat.Tests/MessageFormatterTests.cs b/src/Jeffijoe.MessageFormat.Tests/MessageFormatterTests.cs index 0ff94e2..62677c0 100644 --- a/src/Jeffijoe.MessageFormat.Tests/MessageFormatterTests.cs +++ b/src/Jeffijoe.MessageFormat.Tests/MessageFormatterTests.cs @@ -12,111 +12,110 @@ using Xunit; -namespace Jeffijoe.MessageFormat.Tests +namespace Jeffijoe.MessageFormat.Tests; + +/// +/// The message formatter tests. +/// +public class MessageFormatterTests { + #region Public Methods and Operators + /// - /// The message formatter tests. + /// The format message. /// - public class MessageFormatterTests + [Fact] + public void FormatMessage() { - #region Public Methods and Operators - - /// - /// The format message. - /// - [Fact] - public void FormatMessage() - { - const string Pattern = "{name} has {messages, plural, other {# messages}}."; - const string Expected = "Jeff has 123 messages."; - IReadOnlyDictionary args = new Dictionary { { "name", "Jeff" }, { "messages", 123} }; + const string Pattern = "{name} has {messages, plural, other {# messages}}."; + const string Expected = "Jeff has 123 messages."; + IReadOnlyDictionary args = new Dictionary { { "name", "Jeff" }, { "messages", 123} }; - var actual = MessageFormatter.Format(Pattern, args); + var actual = MessageFormatter.Format(Pattern, args); - Assert.Equal(Expected, actual); - } + Assert.Equal(Expected, actual); + } - /// - /// The unescape literals. - /// - /// - /// The source. - /// - /// - /// The expected. - /// - [Theory] - [InlineData(@"Hello '{buddy}', how are you '{doing}'?", "Hello {buddy}, how are you {doing}?")] - [InlineData(@"Hello ''{buddy}'', how are you '{doing}'?", @"Hello '{buddy}', how are you {doing}?")] - [InlineData(@"{''}", @"{'}")] - public void UnescapeLiterals(string source, string expected) - { - var actual = MessageFormatter.UnescapeLiterals(new StringBuilder(source)); - Assert.Equal(expected, actual); - } + /// + /// The unescape literals. + /// + /// + /// The source. + /// + /// + /// The expected. + /// + [Theory] + [InlineData(@"Hello '{buddy}', how are you '{doing}'?", "Hello {buddy}, how are you {doing}?")] + [InlineData(@"Hello ''{buddy}'', how are you '{doing}'?", @"Hello '{buddy}', how are you {doing}?")] + [InlineData(@"{''}", @"{'}")] + public void UnescapeLiterals(string source, string expected) + { + var actual = MessageFormatter.UnescapeLiterals(new StringBuilder(source)); + Assert.Equal(expected, actual); + } - /// - /// Verifies that format message throws when variables are missing and the formatter requires it to exist. - /// - [Fact] - public void VerifyFormatMessageThrowsWhenVariablesAreMissingAndTheFormatterRequiresItToExist() - { - const string Pattern = "{name}"; + /// + /// Verifies that format message throws when variables are missing and the formatter requires it to exist. + /// + [Fact] + public void VerifyFormatMessageThrowsWhenVariablesAreMissingAndTheFormatterRequiresItToExist() + { + const string Pattern = "{name}"; - // Note the missing "name" variable. - var args = new Dictionary { { "messages", 1 } }; + // Note the missing "name" variable. + var args = new Dictionary { { "messages", 1 } }; - var subject = new MessageFormatter(); + var subject = new MessageFormatter(); - var ex = Assert.Throws(() => subject.FormatMessage(Pattern, args)); - Assert.Equal("name", ex.MissingVariable); - } + var ex = Assert.Throws(() => subject.FormatMessage(Pattern, args)); + Assert.Equal("name", ex.MissingVariable); + } - /// - /// Verifies that format message allows non-existent variables when formatter allows it. - /// - [Fact] - public void VerifyFormatMessageAllowsNonExistentVariablesWhenFormatterAllowsIt() - { - const string Pattern = "{name, fake}"; + /// + /// Verifies that format message allows non-existent variables when formatter allows it. + /// + [Fact] + public void VerifyFormatMessageAllowsNonExistentVariablesWhenFormatterAllowsIt() + { + const string Pattern = "{name, fake}"; - // Note the missing "name" variable. - var args = new Dictionary (); + // Note the missing "name" variable. + var args = new Dictionary (); - var library = new FormatterLibrary(); - library.Add(new TestFormatter(variableMustExist: false, formatterName: "fake")); - var subject = new MessageFormatter(new PatternParser(), library, useCache: false); + var library = new FormatterLibrary(); + library.Add(new TestFormatter(variableMustExist: false, formatterName: "fake")); + var subject = new MessageFormatter(new PatternParser(), library, useCache: false); - var actual = subject.FormatMessage(Pattern, args); + var actual = subject.FormatMessage(Pattern, args); - Assert.Equal("formatted", actual); - } + Assert.Equal("formatted", actual); + } - #endregion + #endregion - #region Fakes + #region Fakes - private class TestFormatter : IFormatter - { - private readonly string formatterName; + private class TestFormatter : IFormatter + { + private readonly string formatterName; - public TestFormatter(bool variableMustExist, string formatterName) - { - this.VariableMustExist = variableMustExist; - this.formatterName = formatterName; - } + public TestFormatter(bool variableMustExist, string formatterName) + { + this.VariableMustExist = variableMustExist; + this.formatterName = formatterName; + } - public bool VariableMustExist { get; } + public bool VariableMustExist { get; } - public bool CanFormat(FormatterRequest request) => request.FormatterName == this.formatterName; + public bool CanFormat(FormatterRequest request) => request.FormatterName == this.formatterName; - public string Format(string locale, FormatterRequest request, IReadOnlyDictionary args, object? value, - IMessageFormatter messageFormatter) - { - return "formatted"; - } + public string Format(string locale, FormatterRequest request, IReadOnlyDictionary args, object? value, + IMessageFormatter messageFormatter) + { + return "formatted"; } - - #endregion } + + #endregion } \ No newline at end of file diff --git a/src/Jeffijoe.MessageFormat.Tests/MessageFormatterUsingRealParserTests.cs b/src/Jeffijoe.MessageFormat.Tests/MessageFormatterUsingRealParserTests.cs index c6c3dd1..5d810fe 100644 --- a/src/Jeffijoe.MessageFormat.Tests/MessageFormatterUsingRealParserTests.cs +++ b/src/Jeffijoe.MessageFormat.Tests/MessageFormatterUsingRealParserTests.cs @@ -13,50 +13,50 @@ using Xunit; using Xunit.Abstractions; -namespace Jeffijoe.MessageFormat.Tests +namespace Jeffijoe.MessageFormat.Tests; + +/// +/// The message formatter_using_real_parser_ tests. +/// +public class MessageFormatterUsingRealParserTests { + #region Fields + /// - /// The message formatter_using_real_parser_ tests. + /// The output helper. /// - public class MessageFormatterUsingRealParserTests - { - #region Fields + private ITestOutputHelper outputHelper; - /// - /// The output helper. - /// - private ITestOutputHelper outputHelper; + #endregion - #endregion + #region Constructors and Destructors - #region Constructors and Destructors - - /// - /// Initializes a new instance of the class. - /// - /// - /// The output helper. - /// - public MessageFormatterUsingRealParserTests(ITestOutputHelper outputHelper) - { - this.outputHelper = outputHelper; - } + /// + /// Initializes a new instance of the class. + /// + /// + /// The output helper. + /// + public MessageFormatterUsingRealParserTests(ITestOutputHelper outputHelper) + { + this.outputHelper = outputHelper; + } - #endregion + #endregion - #region Public Methods and Operators + #region Public Methods and Operators - /// - /// The format message_using_real_parser_and_library_mock. - /// - /// - /// The source. - /// - /// - /// The expected. - /// - [Theory] - [InlineData(@"Hi, I'm {name}, and it's still {name, fake, whatever + /// + /// The format message_using_real_parser_and_library_mock. + /// + /// + /// The source. + /// + /// + /// The expected. + /// + [Theory] + [InlineData(@"Hi, I'm {name}, and it's still {name, fake, whatever i do what i want @@ -69,35 +69,34 @@ whatchu gonna do? whatchu gonna do when dey come for youu? }, ok?", "Hi, I'm Jeff, and it's still Jeff, ok?")] - public void FormatMessage_using_real_parser_and_library_mock(string source, string expected) + public void FormatMessage_using_real_parser_and_library_mock(string source, string expected) + { + var library = new FormatterLibrary(); + var dummyFormatter = new FakeFormatter(canFormat:true, formatResult: "Jeff"); + library.Add(dummyFormatter); + var subject = new MessageFormatter( + new PatternParser(new LiteralParser()), + library, + false); + + var args = new Dictionary(); + args.Add("name", "Jeff"); + + // Warm up + Benchmark.Start("Warm-up", this.outputHelper); + subject.FormatMessage(source, args); + Benchmark.End(this.outputHelper); + + Benchmark.Start("Aaaand a few after warm-up", this.outputHelper); + for (int i = 0; i < 1000; i++) { - var library = new FormatterLibrary(); - var dummyFormatter = new FakeFormatter(canFormat:true, formatResult: "Jeff"); - library.Add(dummyFormatter); - var subject = new MessageFormatter( - new PatternParser(new LiteralParser()), - library, - false); - - var args = new Dictionary(); - args.Add("name", "Jeff"); - - // Warm up - Benchmark.Start("Warm-up", this.outputHelper); subject.FormatMessage(source, args); - Benchmark.End(this.outputHelper); - - Benchmark.Start("Aaaand a few after warm-up", this.outputHelper); - for (int i = 0; i < 1000; i++) - { - subject.FormatMessage(source, args); - } - - Benchmark.End(this.outputHelper); - - Assert.Equal(expected, subject.FormatMessage(source, args)); } - #endregion + Benchmark.End(this.outputHelper); + + Assert.Equal(expected, subject.FormatMessage(source, args)); } + + #endregion } \ No newline at end of file diff --git a/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/GeneratedPluralRulesTests.cs b/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/GeneratedPluralRulesTests.cs index dac2477..0db8c6b 100644 --- a/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/GeneratedPluralRulesTests.cs +++ b/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/GeneratedPluralRulesTests.cs @@ -5,81 +5,80 @@ using Jeffijoe.MessageFormat.Parsing; using Xunit; -namespace Jeffijoe.MessageFormat.Tests.MetadataGenerator +namespace Jeffijoe.MessageFormat.Tests.MetadataGenerator; + +public class GeneratedPluralRulesTests { - public class GeneratedPluralRulesTests + [Theory] + [InlineData(0, "днів")] + [InlineData(1, "день")] + [InlineData(101, "день")] + [InlineData(102, "дні")] + [InlineData(105, "днів")] + public void Uk_PluralizerTests(double n, string expected) { - [Theory] - [InlineData(0, "днів")] - [InlineData(1, "день")] - [InlineData(101, "день")] - [InlineData(102, "дні")] - [InlineData(105, "днів")] - public void Uk_PluralizerTests(double n, string expected) - { - var subject = new PluralFormatter(); - var args = new Dictionary { { "test", n } }; - var arguments = - new ParsedArguments( - new[] - { - new KeyedBlock("one", "день"), - new KeyedBlock("few", "дні"), - new KeyedBlock("many", "днів"), - new KeyedBlock("other", "дня") - }, - new FormatterExtension[0]); - var request = new FormatterRequest(new Literal(1, 1, 1, 1, ""), "test", "plural", null); - var actual = subject.Pluralize("uk", arguments, new PluralContext(Convert.ToDecimal(Convert.ToDouble(args[request.Variable]))), 0); - Assert.Equal(expected, actual); - } + var subject = new PluralFormatter(); + var args = new Dictionary { { "test", n } }; + var arguments = + new ParsedArguments( + new[] + { + new KeyedBlock("one", "день"), + new KeyedBlock("few", "дні"), + new KeyedBlock("many", "днів"), + new KeyedBlock("other", "дня") + }, + new FormatterExtension[0]); + var request = new FormatterRequest(new Literal(1, 1, 1, 1, ""), "test", "plural", null); + var actual = subject.Pluralize("uk", arguments, new PluralContext(Convert.ToDecimal(Convert.ToDouble(args[request.Variable]))), 0); + Assert.Equal(expected, actual); + } - [Theory] - [InlineData(0, "дней")] - [InlineData(1, "день")] - [InlineData(101, "день")] - [InlineData(102, "дня")] - [InlineData(105, "дней")] - public void Ru_PluralizerTests(double n, string expected) - { - var subject = new PluralFormatter(); - var args = new Dictionary { { "test", n } }; - var arguments = - new ParsedArguments( - new[] - { - new KeyedBlock("one", "день"), - new KeyedBlock("few", "дня"), - new KeyedBlock("many", "дней"), - new KeyedBlock("other", "дня") - }, - new FormatterExtension[0]); - var request = new FormatterRequest(new Literal(1, 1, 1, 1, ""), "test", "plural", null); - var actual = subject.Pluralize("ru", arguments, new PluralContext(Convert.ToDecimal(args[request.Variable])), 0); - Assert.Equal(expected, actual); - } + [Theory] + [InlineData(0, "дней")] + [InlineData(1, "день")] + [InlineData(101, "день")] + [InlineData(102, "дня")] + [InlineData(105, "дней")] + public void Ru_PluralizerTests(double n, string expected) + { + var subject = new PluralFormatter(); + var args = new Dictionary { { "test", n } }; + var arguments = + new ParsedArguments( + new[] + { + new KeyedBlock("one", "день"), + new KeyedBlock("few", "дня"), + new KeyedBlock("many", "дней"), + new KeyedBlock("other", "дня") + }, + new FormatterExtension[0]); + var request = new FormatterRequest(new Literal(1, 1, 1, 1, ""), "test", "plural", null); + var actual = subject.Pluralize("ru", arguments, new PluralContext(Convert.ToDecimal(args[request.Variable])), 0); + Assert.Equal(expected, actual); + } - [Theory] - [InlineData(0, "days")] - [InlineData(1, "day")] - [InlineData(101, "days")] - [InlineData(102, "days")] - [InlineData(105, "days")] - public void En_PluralizerTests(double n, string expected) - { - var subject = new PluralFormatter(); - var args = new Dictionary { { "test", n } }; - var arguments = - new ParsedArguments( - new[] - { - new KeyedBlock("one", "day"), - new KeyedBlock("other", "days") - }, - new FormatterExtension[0]); - var request = new FormatterRequest(new Literal(1, 1, 1, 1, ""), "test", "plural", null); - var actual = subject.Pluralize("en", arguments, new PluralContext(Convert.ToDecimal(args[request.Variable])), 0); - Assert.Equal(expected, actual); - } + [Theory] + [InlineData(0, "days")] + [InlineData(1, "day")] + [InlineData(101, "days")] + [InlineData(102, "days")] + [InlineData(105, "days")] + public void En_PluralizerTests(double n, string expected) + { + var subject = new PluralFormatter(); + var args = new Dictionary { { "test", n } }; + var arguments = + new ParsedArguments( + new[] + { + new KeyedBlock("one", "day"), + new KeyedBlock("other", "days") + }, + new FormatterExtension[0]); + var request = new FormatterRequest(new Literal(1, 1, 1, 1, ""), "test", "plural", null); + var actual = subject.Pluralize("en", arguments, new PluralContext(Convert.ToDecimal(args[request.Variable])), 0); + Assert.Equal(expected, actual); } } \ No newline at end of file diff --git a/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/ParserTests.cs b/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/ParserTests.cs index a3480d0..4cecb75 100644 --- a/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/ParserTests.cs +++ b/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/ParserTests.cs @@ -7,14 +7,14 @@ using Xunit; -namespace Jeffijoe.MessageFormat.Tests.MetadataGenerator +namespace Jeffijoe.MessageFormat.Tests.MetadataGenerator; + +public class ParserTests { - public class ParserTests + [Fact] + public void CanParseLocales() { - [Fact] - public void CanParseLocales() - { - var rules = ParseRules(@" + var rules = ParseRules(@" @@ -23,19 +23,19 @@ public void CanParseLocales() "); - var rule = Assert.Single(rules); - var expected = new[] - { - "am", "as", "bn", "doi", "fa", "gu", "hi", "kn", "pcm", "zu" - }; - var actual = rule.Locales; - Assert.Equal(actual, expected); - } - - [Fact] - public void OtherCountIsIgnored() + var rule = Assert.Single(rules); + var expected = new[] { - var rules = ParseRules(@" + "am", "as", "bn", "doi", "fa", "gu", "hi", "kn", "pcm", "zu" + }; + var actual = rule.Locales; + Assert.Equal(actual, expected); + } + + [Fact] + public void OtherCountIsIgnored() + { + var rules = ParseRules(@" @@ -44,209 +44,209 @@ public void OtherCountIsIgnored() "); - var rule = Assert.Single(rules); - Assert.Empty(rule.Conditions); - } + var rule = Assert.Single(rules); + Assert.Empty(rule.Conditions); + } - [Fact] - public void CanParseSingleCount_RuleDescription_WithoutRelations() - { - var rules = ParseRules(GenerateXmlWithRuleContent("@integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, …")); + [Fact] + public void CanParseSingleCount_RuleDescription_WithoutRelations() + { + var rules = ParseRules(GenerateXmlWithRuleContent("@integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, …")); - var rule = Assert.Single(rules); - var condition = Assert.Single(rule.Conditions); - var expected = "@integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, …"; - Assert.Equal(expected, condition.RuleDescription); - } + var rule = Assert.Single(rules); + var condition = Assert.Single(rule.Conditions); + var expected = "@integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, …"; + Assert.Equal(expected, condition.RuleDescription); + } - [Fact] - public void CanParseSingleCount_VisibleDigitsNumber() - { - var rules = ParseRules( - GenerateXmlWithRuleContent(@"v = 0 @integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, …")); - var rule = Assert.Single(rules); - var condition = Assert.Single(rule.Conditions); - var orCondition = Assert.Single(condition.OrConditions); - var actual = Assert.Single(orCondition.AndConditions); - var expected = new Operation(new VariableOperand(OperandSymbol.VisibleFractionDigitNumber), Relation.Equals, new[] { new NumberOperand(0) }); - - AssertOperationEqual(expected, actual); - } - - [Fact] - public void CanParseSingleCount_IntegerDigits() - { - var rules = ParseRules( - GenerateXmlWithRuleContent(@"i = 0 @integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, …")); - var rule = Assert.Single(rules); - var condition = Assert.Single(rule.Conditions); - var orCondition = Assert.Single(condition.OrConditions); - var actual = Assert.Single(orCondition.AndConditions); - var expected = new Operation(new VariableOperand(OperandSymbol.IntegerDigits), Relation.Equals, new[] { new NumberOperand(0) }); - - AssertOperationEqual(expected, actual); - } - - [Fact] - public void CanParseSingleCount_AbsoluteNumber() - { - var rules = ParseRules( - GenerateXmlWithRuleContent("n = 1 @integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, …")); - var rule = Assert.Single(rules); - var condition = Assert.Single(rule.Conditions); - var orCondition = Assert.Single(condition.OrConditions); - var actual = Assert.Single(orCondition.AndConditions); - var expected = new Operation(new VariableOperand(OperandSymbol.AbsoluteValue), Relation.Equals, new[] {new NumberOperand(1) }); - - AssertOperationEqual(expected, actual); - } - - [Theory] - [InlineData("n = 2 @integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, …", Relation.Equals)] - [InlineData("n != 2 @integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, …", Relation.NotEquals)] - public void CanParseVariousRelations(string ruleText, Relation expectedRelation) - { - var rules = ParseRules(GenerateXmlWithRuleContent(ruleText)); - var rule = Assert.Single(rules); - var condition = Assert.Single(rule.Conditions); - var orCondition = Assert.Single(condition.OrConditions); - var actual = Assert.Single(orCondition.AndConditions); - var expected = new Operation(new VariableOperand(OperandSymbol.AbsoluteValue), expectedRelation, new[] { new NumberOperand(2) }); - - AssertOperationEqual(expected, actual); - } - - [Fact] - public void CanParseOrRules() - { - var rules = ParseRules(GenerateXmlWithRuleContent("n = 2 or n = 1 or n = 0 @integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, …")); - var rule = Assert.Single(rules); - var condition = Assert.Single(rule.Conditions); + [Fact] + public void CanParseSingleCount_VisibleDigitsNumber() + { + var rules = ParseRules( + GenerateXmlWithRuleContent(@"v = 0 @integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, …")); + var rule = Assert.Single(rules); + var condition = Assert.Single(rule.Conditions); + var orCondition = Assert.Single(condition.OrConditions); + var actual = Assert.Single(orCondition.AndConditions); + var expected = new Operation(new VariableOperand(OperandSymbol.VisibleFractionDigitNumber), Relation.Equals, new[] { new NumberOperand(0) }); + + AssertOperationEqual(expected, actual); + } + + [Fact] + public void CanParseSingleCount_IntegerDigits() + { + var rules = ParseRules( + GenerateXmlWithRuleContent(@"i = 0 @integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, …")); + var rule = Assert.Single(rules); + var condition = Assert.Single(rule.Conditions); + var orCondition = Assert.Single(condition.OrConditions); + var actual = Assert.Single(orCondition.AndConditions); + var expected = new Operation(new VariableOperand(OperandSymbol.IntegerDigits), Relation.Equals, new[] { new NumberOperand(0) }); + + AssertOperationEqual(expected, actual); + } + + [Fact] + public void CanParseSingleCount_AbsoluteNumber() + { + var rules = ParseRules( + GenerateXmlWithRuleContent("n = 1 @integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, …")); + var rule = Assert.Single(rules); + var condition = Assert.Single(rule.Conditions); + var orCondition = Assert.Single(condition.OrConditions); + var actual = Assert.Single(orCondition.AndConditions); + var expected = new Operation(new VariableOperand(OperandSymbol.AbsoluteValue), Relation.Equals, new[] {new NumberOperand(1) }); + + AssertOperationEqual(expected, actual); + } + + [Theory] + [InlineData("n = 2 @integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, …", Relation.Equals)] + [InlineData("n != 2 @integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, …", Relation.NotEquals)] + public void CanParseVariousRelations(string ruleText, Relation expectedRelation) + { + var rules = ParseRules(GenerateXmlWithRuleContent(ruleText)); + var rule = Assert.Single(rules); + var condition = Assert.Single(rule.Conditions); + var orCondition = Assert.Single(condition.OrConditions); + var actual = Assert.Single(orCondition.AndConditions); + var expected = new Operation(new VariableOperand(OperandSymbol.AbsoluteValue), expectedRelation, new[] { new NumberOperand(2) }); + + AssertOperationEqual(expected, actual); + } + + [Fact] + public void CanParseOrRules() + { + var rules = ParseRules(GenerateXmlWithRuleContent("n = 2 or n = 1 or n = 0 @integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, …")); + var rule = Assert.Single(rules); + var condition = Assert.Single(rule.Conditions); - Assert.Equal(3, condition.OrConditions.Count); + Assert.Equal(3, condition.OrConditions.Count); - var actualFirst = Assert.Single(condition.OrConditions[0].AndConditions); - var expectedFirst = new Operation(new VariableOperand(OperandSymbol.AbsoluteValue), Relation.Equals, new[] { new NumberOperand(2) }); - AssertOperationEqual(expectedFirst, actualFirst); + var actualFirst = Assert.Single(condition.OrConditions[0].AndConditions); + var expectedFirst = new Operation(new VariableOperand(OperandSymbol.AbsoluteValue), Relation.Equals, new[] { new NumberOperand(2) }); + AssertOperationEqual(expectedFirst, actualFirst); - var actualSecond = Assert.Single(condition.OrConditions[1].AndConditions); - var expectedSecond = new Operation(new VariableOperand(OperandSymbol.AbsoluteValue), Relation.Equals, new[] { new NumberOperand(1) }); - AssertOperationEqual(expectedSecond, actualSecond); + var actualSecond = Assert.Single(condition.OrConditions[1].AndConditions); + var expectedSecond = new Operation(new VariableOperand(OperandSymbol.AbsoluteValue), Relation.Equals, new[] { new NumberOperand(1) }); + AssertOperationEqual(expectedSecond, actualSecond); - var actualThird = Assert.Single(condition.OrConditions[2].AndConditions); - var expectedThird = new Operation(new VariableOperand(OperandSymbol.AbsoluteValue), Relation.Equals, new[] { new NumberOperand(0) }); - AssertOperationEqual(expectedThird, actualThird); - } + var actualThird = Assert.Single(condition.OrConditions[2].AndConditions); + var expectedThird = new Operation(new VariableOperand(OperandSymbol.AbsoluteValue), Relation.Equals, new[] { new NumberOperand(0) }); + AssertOperationEqual(expectedThird, actualThird); + } - [Fact] - public void CanParseAndRules() - { - var rules = ParseRules(GenerateXmlWithRuleContent("n = 2 and n = 1 and n = 0 @integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, …")); - var rule = Assert.Single(rules); - var condition = Assert.Single(rule.Conditions); + [Fact] + public void CanParseAndRules() + { + var rules = ParseRules(GenerateXmlWithRuleContent("n = 2 and n = 1 and n = 0 @integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, …")); + var rule = Assert.Single(rules); + var condition = Assert.Single(rule.Conditions); - var orCondition = Assert.Single(condition.OrConditions); - Assert.Equal(3, orCondition.AndConditions.Count); + var orCondition = Assert.Single(condition.OrConditions); + Assert.Equal(3, orCondition.AndConditions.Count); - var actualFirst = orCondition.AndConditions[0]; - var expectedFirst = new Operation(new VariableOperand(OperandSymbol.AbsoluteValue), Relation.Equals, new[] { new NumberOperand(2) }); - AssertOperationEqual(expectedFirst, actualFirst); + var actualFirst = orCondition.AndConditions[0]; + var expectedFirst = new Operation(new VariableOperand(OperandSymbol.AbsoluteValue), Relation.Equals, new[] { new NumberOperand(2) }); + AssertOperationEqual(expectedFirst, actualFirst); - var actualSecond = orCondition.AndConditions[1]; - var expectedSecond = new Operation(new VariableOperand(OperandSymbol.AbsoluteValue), Relation.Equals, new[] { new NumberOperand(1) }); - AssertOperationEqual(expectedSecond, actualSecond); + var actualSecond = orCondition.AndConditions[1]; + var expectedSecond = new Operation(new VariableOperand(OperandSymbol.AbsoluteValue), Relation.Equals, new[] { new NumberOperand(1) }); + AssertOperationEqual(expectedSecond, actualSecond); - var actualThird = orCondition.AndConditions[2]; - var expectedThird = new Operation(new VariableOperand(OperandSymbol.AbsoluteValue), Relation.Equals, new[] { new NumberOperand(0) }); - AssertOperationEqual(expectedThird, actualThird); - } + var actualThird = orCondition.AndConditions[2]; + var expectedThird = new Operation(new VariableOperand(OperandSymbol.AbsoluteValue), Relation.Equals, new[] { new NumberOperand(0) }); + AssertOperationEqual(expectedThird, actualThird); + } - [Fact] - public void CanParseModuloInLeftOperator() - { - var rules = ParseRules( - GenerateXmlWithRuleContent("n % 5 = 3 @integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, …")); - var rule = Assert.Single(rules); - var condition = Assert.Single(rule.Conditions); - var orCondition = Assert.Single(condition.OrConditions); - var actual = Assert.Single(orCondition.AndConditions); - var modulo = new ModuloOperand(OperandSymbol.AbsoluteValue, 5); - var expected = new Operation(modulo, Relation.Equals, new[] { new NumberOperand(3) }); - - AssertOperationEqual(expected, actual); - } - - [Fact] - public void CanParseRangeInRightOperator() - { - var rules = ParseRules( - GenerateXmlWithRuleContent("n = 3..5 @integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, …")); - var rule = Assert.Single(rules); - var condition = Assert.Single(rule.Conditions); - var orCondition = Assert.Single(condition.OrConditions); - var actual = Assert.Single(orCondition.AndConditions); - var range = new[] { new RangeOperand(3, 5) }; - var expected = new Operation(new VariableOperand(OperandSymbol.AbsoluteValue), Relation.Equals, range); - - AssertOperationEqual(expected, actual); - } - - [Fact] - public void CanParseCommaSeparatedInRightOperator() - { - var rules = ParseRules( - GenerateXmlWithRuleContent("n = 3,5,8, 10 @integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, …")); - var rule = Assert.Single(rules); - var condition = Assert.Single(rule.Conditions); - var orCondition = Assert.Single(condition.OrConditions); - var actual = Assert.Single(orCondition.AndConditions); - var range = new[] { new NumberOperand(3), new NumberOperand(5), new NumberOperand(8), new NumberOperand(10) }; - var expected = new Operation(new VariableOperand(OperandSymbol.AbsoluteValue), Relation.Equals, range); - - AssertOperationEqual(expected, actual); - } - - [Fact] - public void CanParseMixedCommaSeparatedAndRangeInRightOperator() - { - var rules = ParseRules( - GenerateXmlWithRuleContent("n = 3,5..7,12,15 @integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, …")); - var rule = Assert.Single(rules); - var condition = Assert.Single(rule.Conditions); - var orCondition = Assert.Single(condition.OrConditions); - var actual = Assert.Single(orCondition.AndConditions); - var range = new IRightOperand[] { new NumberOperand(3), new RangeOperand(5, 7), new NumberOperand(12), new NumberOperand(15) }; - var expected = new Operation(new VariableOperand(OperandSymbol.AbsoluteValue), Relation.Equals, range); - - AssertOperationEqual(expected, actual); - } - - [Theory] - [InlineData('n', OperandSymbol.AbsoluteValue)] - [InlineData('i', OperandSymbol.IntegerDigits)] - [InlineData('v', OperandSymbol.VisibleFractionDigitNumber)] - [InlineData('w', OperandSymbol.VisibleFractionDigitNumberWithoutTrailingZeroes)] - [InlineData('f', OperandSymbol.VisibleFractionDigits)] - [InlineData('t', OperandSymbol.VisibleFractionDigitsWithoutTrailingZeroes)] - [InlineData('c', OperandSymbol.ExponentC)] - [InlineData('e', OperandSymbol.ExponentE)] - public void MapsVariable_ToCorrectOperator(char variable, OperandSymbol symbol) - { - var rules = ParseRules( - GenerateXmlWithRuleContent($"{variable} = 3")); - var rule = Assert.Single(rules); - var condition = Assert.Single(rule.Conditions); - var orCondition = Assert.Single(condition.OrConditions); - var actual = Assert.Single(orCondition.AndConditions); - var right = new IRightOperand[] { new NumberOperand(3) }; - var expected = new Operation(new VariableOperand(symbol), Relation.Equals, right); - - AssertOperationEqual(expected, actual); - } - - private static string GenerateXmlWithRuleContent(string ruleText) - { - return $@" + [Fact] + public void CanParseModuloInLeftOperator() + { + var rules = ParseRules( + GenerateXmlWithRuleContent("n % 5 = 3 @integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, …")); + var rule = Assert.Single(rules); + var condition = Assert.Single(rule.Conditions); + var orCondition = Assert.Single(condition.OrConditions); + var actual = Assert.Single(orCondition.AndConditions); + var modulo = new ModuloOperand(OperandSymbol.AbsoluteValue, 5); + var expected = new Operation(modulo, Relation.Equals, new[] { new NumberOperand(3) }); + + AssertOperationEqual(expected, actual); + } + + [Fact] + public void CanParseRangeInRightOperator() + { + var rules = ParseRules( + GenerateXmlWithRuleContent("n = 3..5 @integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, …")); + var rule = Assert.Single(rules); + var condition = Assert.Single(rule.Conditions); + var orCondition = Assert.Single(condition.OrConditions); + var actual = Assert.Single(orCondition.AndConditions); + var range = new[] { new RangeOperand(3, 5) }; + var expected = new Operation(new VariableOperand(OperandSymbol.AbsoluteValue), Relation.Equals, range); + + AssertOperationEqual(expected, actual); + } + + [Fact] + public void CanParseCommaSeparatedInRightOperator() + { + var rules = ParseRules( + GenerateXmlWithRuleContent("n = 3,5,8, 10 @integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, …")); + var rule = Assert.Single(rules); + var condition = Assert.Single(rule.Conditions); + var orCondition = Assert.Single(condition.OrConditions); + var actual = Assert.Single(orCondition.AndConditions); + var range = new[] { new NumberOperand(3), new NumberOperand(5), new NumberOperand(8), new NumberOperand(10) }; + var expected = new Operation(new VariableOperand(OperandSymbol.AbsoluteValue), Relation.Equals, range); + + AssertOperationEqual(expected, actual); + } + + [Fact] + public void CanParseMixedCommaSeparatedAndRangeInRightOperator() + { + var rules = ParseRules( + GenerateXmlWithRuleContent("n = 3,5..7,12,15 @integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, …")); + var rule = Assert.Single(rules); + var condition = Assert.Single(rule.Conditions); + var orCondition = Assert.Single(condition.OrConditions); + var actual = Assert.Single(orCondition.AndConditions); + var range = new IRightOperand[] { new NumberOperand(3), new RangeOperand(5, 7), new NumberOperand(12), new NumberOperand(15) }; + var expected = new Operation(new VariableOperand(OperandSymbol.AbsoluteValue), Relation.Equals, range); + + AssertOperationEqual(expected, actual); + } + + [Theory] + [InlineData('n', OperandSymbol.AbsoluteValue)] + [InlineData('i', OperandSymbol.IntegerDigits)] + [InlineData('v', OperandSymbol.VisibleFractionDigitNumber)] + [InlineData('w', OperandSymbol.VisibleFractionDigitNumberWithoutTrailingZeroes)] + [InlineData('f', OperandSymbol.VisibleFractionDigits)] + [InlineData('t', OperandSymbol.VisibleFractionDigitsWithoutTrailingZeroes)] + [InlineData('c', OperandSymbol.ExponentC)] + [InlineData('e', OperandSymbol.ExponentE)] + public void MapsVariable_ToCorrectOperator(char variable, OperandSymbol symbol) + { + var rules = ParseRules( + GenerateXmlWithRuleContent($"{variable} = 3")); + var rule = Assert.Single(rules); + var condition = Assert.Single(rule.Conditions); + var orCondition = Assert.Single(condition.OrConditions); + var actual = Assert.Single(orCondition.AndConditions); + var right = new IRightOperand[] { new NumberOperand(3) }; + var expected = new Operation(new VariableOperand(symbol), Relation.Equals, right); + + AssertOperationEqual(expected, actual); + } + + private static string GenerateXmlWithRuleContent(string ruleText) + { + return $@" @@ -255,23 +255,22 @@ private static string GenerateXmlWithRuleContent(string ruleText) "; - } + } - private static void AssertOperationEqual(Operation expected, Operation actual) - { - Assert.Equal(expected.OperandLeft, actual.OperandLeft); - Assert.Equal(expected.Relation, actual.Relation); - Assert.Equal(expected.OperandRight, actual.OperandRight); - } + private static void AssertOperationEqual(Operation expected, Operation actual) + { + Assert.Equal(expected.OperandLeft, actual.OperandLeft); + Assert.Equal(expected.Relation, actual.Relation); + Assert.Equal(expected.OperandRight, actual.OperandRight); + } - private static IEnumerable ParseRules(string xmlText) - { - var xml = new XmlDocument(); - xml.LoadXml(xmlText); + private static IEnumerable ParseRules(string xmlText) + { + var xml = new XmlDocument(); + xml.LoadXml(xmlText); - var parser = new PluralParser(xml, Array.Empty()); + var parser = new PluralParser(xml, Array.Empty()); - return parser.Parse(); - } + return parser.Parse(); } -} +} \ No newline at end of file diff --git a/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/PluralContextTests.cs b/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/PluralContextTests.cs index da02049..c0986c5 100644 --- a/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/PluralContextTests.cs +++ b/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/PluralContextTests.cs @@ -1,109 +1,108 @@ using Jeffijoe.MessageFormat.Formatting.Formatters; using Xunit; -namespace Jeffijoe.MessageFormat.Tests.MetadataGenerator +namespace Jeffijoe.MessageFormat.Tests.MetadataGenerator; + +public class PluralContextTests { - public class PluralContextTests + [Theory] + [InlineData("-12312.213213", 12312.213213)] + [InlineData("12312.213213", 12312.213213)] + [InlineData("-12312", 12312)] + [InlineData("12312", 12312)] + [InlineData("0", 0)] + public void Parses_N(string s, double expectedN) + { + var ctx = new PluralContext(s); + + Assert.Equal(expectedN, ctx.N); + } + + [Theory] + [InlineData("-12312.213213", -12312)] + [InlineData("12312.213213", 12312)] + [InlineData("-12312", -12312)] + [InlineData("12312", 12312)] + [InlineData("0", 0)] + public void Parses_I(string s, double expectedI) + { + var ctx = new PluralContext(s); + + Assert.Equal(expectedI, ctx.I); + } + + [Theory] + [InlineData("-12312.213213", 6)] + [InlineData("12312.213213", 6)] + [InlineData("12312.2132130", 7)] + [InlineData("-12312", 0)] + [InlineData("12312", 0)] + [InlineData("0", 0)] + public void Parses_V(string s, double expectedV) { - [Theory] - [InlineData("-12312.213213", 12312.213213)] - [InlineData("12312.213213", 12312.213213)] - [InlineData("-12312", 12312)] - [InlineData("12312", 12312)] - [InlineData("0", 0)] - public void Parses_N(string s, double expectedN) - { - var ctx = new PluralContext(s); - - Assert.Equal(expectedN, ctx.N); - } - - [Theory] - [InlineData("-12312.213213", -12312)] - [InlineData("12312.213213", 12312)] - [InlineData("-12312", -12312)] - [InlineData("12312", 12312)] - [InlineData("0", 0)] - public void Parses_I(string s, double expectedI) - { - var ctx = new PluralContext(s); - - Assert.Equal(expectedI, ctx.I); - } - - [Theory] - [InlineData("-12312.213213", 6)] - [InlineData("12312.213213", 6)] - [InlineData("12312.2132130", 7)] - [InlineData("-12312", 0)] - [InlineData("12312", 0)] - [InlineData("0", 0)] - public void Parses_V(string s, double expectedV) - { - var ctx = new PluralContext(s); - - Assert.Equal(expectedV, ctx.V); - } - - [Theory] - [InlineData("-12312.213213", 6)] - [InlineData("12312.213213", 6)] - [InlineData("12312.2132130", 6)] - [InlineData("-12312", 0)] - [InlineData("12312", 0)] - [InlineData("0", 0)] - public void Parses_W(string s, double expectedW) - { - var ctx = new PluralContext(s); - - Assert.Equal(expectedW, ctx.W); - } - - [Theory] - [InlineData("-12312.213213", 213213)] - [InlineData("12312.213213", 213213)] - [InlineData("12312.2132130", 2132130)] - [InlineData("-12312", 0)] - [InlineData("12312", 0)] - [InlineData("0", 0)] - public void Parses_F(string s, double expectedF) - { - var ctx = new PluralContext(s); - - Assert.Equal(expectedF, ctx.F); - } - - [Theory] - [InlineData("-12312.213213", 213213)] - [InlineData("12312.213213", 213213)] - [InlineData("12312.2132130", 213213)] - [InlineData("-12312", 0)] - [InlineData("12312", 0)] - [InlineData("0", 0)] - public void Parses_T(string s, double expectedT) - { - var ctx = new PluralContext(s); - - Assert.Equal(expectedT, ctx.T); - } - - - /// - /// Exponents not supported yet - /// - [Theory] - [InlineData("-12312.213213", 0)] - [InlineData("12312.213213", 0)] - [InlineData("12312.2132130", 0)] - [InlineData("-12312", 0)] - [InlineData("12312", 0)] - [InlineData("0", 0)] - public void Parses_C_And_E(string s, double expectedC) - { - var ctx = new PluralContext(s); - - Assert.Equal(expectedC, ctx.C); - Assert.Equal(expectedC, ctx.E); - } + var ctx = new PluralContext(s); + + Assert.Equal(expectedV, ctx.V); + } + + [Theory] + [InlineData("-12312.213213", 6)] + [InlineData("12312.213213", 6)] + [InlineData("12312.2132130", 6)] + [InlineData("-12312", 0)] + [InlineData("12312", 0)] + [InlineData("0", 0)] + public void Parses_W(string s, double expectedW) + { + var ctx = new PluralContext(s); + + Assert.Equal(expectedW, ctx.W); + } + + [Theory] + [InlineData("-12312.213213", 213213)] + [InlineData("12312.213213", 213213)] + [InlineData("12312.2132130", 2132130)] + [InlineData("-12312", 0)] + [InlineData("12312", 0)] + [InlineData("0", 0)] + public void Parses_F(string s, double expectedF) + { + var ctx = new PluralContext(s); + + Assert.Equal(expectedF, ctx.F); + } + + [Theory] + [InlineData("-12312.213213", 213213)] + [InlineData("12312.213213", 213213)] + [InlineData("12312.2132130", 213213)] + [InlineData("-12312", 0)] + [InlineData("12312", 0)] + [InlineData("0", 0)] + public void Parses_T(string s, double expectedT) + { + var ctx = new PluralContext(s); + + Assert.Equal(expectedT, ctx.T); + } + + + /// + /// Exponents not supported yet + /// + [Theory] + [InlineData("-12312.213213", 0)] + [InlineData("12312.213213", 0)] + [InlineData("12312.2132130", 0)] + [InlineData("-12312", 0)] + [InlineData("12312", 0)] + [InlineData("0", 0)] + public void Parses_C_And_E(string s, double expectedC) + { + var ctx = new PluralContext(s); + + Assert.Equal(expectedC, ctx.C); + Assert.Equal(expectedC, ctx.E); } -} +} \ No newline at end of file diff --git a/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/PluralMetadataClassGeneratorTests.cs b/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/PluralMetadataClassGeneratorTests.cs index bbcd101..b10ed33 100644 --- a/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/PluralMetadataClassGeneratorTests.cs +++ b/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/PluralMetadataClassGeneratorTests.cs @@ -3,16 +3,16 @@ using Xunit; -namespace Jeffijoe.MessageFormat.Tests.MetadataGenerator +namespace Jeffijoe.MessageFormat.Tests.MetadataGenerator; + +public class PluralMetadataClassGeneratorTests { - public class PluralMetadataClassGeneratorTests + [Fact] + public void CanGenerateClassFromRules() { - [Fact] - public void CanGenerateClassFromRules() + var rules = new[] { - var rules = new[] - { - new PluralRule(new[] {"en", "uk"}, + new PluralRule(new[] {"en", "uk"}, new[] { new Condition("one", string.Empty, new [] @@ -23,12 +23,12 @@ public void CanGenerateClassFromRules() }) }) }) - }; - var generator = new PluralRulesMetadataGenerator(rules); + }; + var generator = new PluralRulesMetadataGenerator(rules); - var actual = generator.GenerateClass(); + var actual = generator.GenerateClass(); - var expected = @" + var expected = @" using System; using System.Collections.Generic; namespace Jeffijoe.MessageFormat.Formatting.Formatters @@ -62,7 +62,6 @@ public static partial bool TryGetRuleByLocale(string locale, out ContextPluraliz } ".TrimStart(); - Assert.Equal(expected, actual); - } + Assert.Equal(expected, actual); } -} +} \ No newline at end of file diff --git a/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/RuleSourceGeneratorTests.cs b/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/RuleSourceGeneratorTests.cs index a04725d..fce52c6 100644 --- a/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/RuleSourceGeneratorTests.cs +++ b/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/RuleSourceGeneratorTests.cs @@ -6,331 +6,330 @@ using Xunit; -namespace Jeffijoe.MessageFormat.Tests.MetadataGenerator +namespace Jeffijoe.MessageFormat.Tests.MetadataGenerator; + +public class RuleSourceGeneratorTests { - public class RuleSourceGeneratorTests + [Fact] + public void CanGenerateEmptyRule() { - [Fact] - public void CanGenerateEmptyRule() - { - var generator = new RuleGenerator(new PluralRule(new[] { "en" }, Array.Empty())); + var generator = new RuleGenerator(new PluralRule(new[] { "en" }, Array.Empty())); - var actual = GenerateText(generator); - var expected = $"return \"other\";{Environment.NewLine}"; - Assert.Equal(expected, actual); - } + var actual = GenerateText(generator); + var expected = $"return \"other\";{Environment.NewLine}"; + Assert.Equal(expected, actual); + } - [Fact] - public void CanGenerateRuleForFractionNumberEquals() + [Fact] + public void CanGenerateRuleForFractionNumberEquals() + { + var generator = new RuleGenerator(new PluralRule(new[] { "en" }, new[] { - var generator = new RuleGenerator(new PluralRule(new[] { "en" }, new[] + new Condition("one", string.Empty, new[] { - new Condition("one", string.Empty, new[] + new OrCondition(new [] { - new OrCondition(new [] - { - new Operation(new VariableOperand(OperandSymbol.VisibleFractionDigitNumber), Relation.Equals, new[] { new NumberOperand(0) }) - }) + new Operation(new VariableOperand(OperandSymbol.VisibleFractionDigitNumber), Relation.Equals, new[] { new NumberOperand(0) }) }) - })); + }) + })); - var actual = GenerateText(generator); - var expected = @$" + var actual = GenerateText(generator); + var expected = @$" if ((context.V == 0)) return ""one""; return ""other""; ".TrimStart(); - Assert.Equal(expected, actual); - } + Assert.Equal(expected, actual); + } - [Fact] - public void CanGenerateRuleForIntegerDigitsEquals() + [Fact] + public void CanGenerateRuleForIntegerDigitsEquals() + { + var generator = new RuleGenerator(new PluralRule(new[] { "en" }, new[] { - var generator = new RuleGenerator(new PluralRule(new[] { "en" }, new[] + new Condition("one", string.Empty, new[] { - new Condition("one", string.Empty, new[] + new OrCondition(new [] { - new OrCondition(new [] - { - new Operation(new VariableOperand(OperandSymbol.IntegerDigits), Relation.Equals, new[] { new NumberOperand(1) }) - }) + new Operation(new VariableOperand(OperandSymbol.IntegerDigits), Relation.Equals, new[] { new NumberOperand(1) }) }) - })); + }) + })); - var actual = GenerateText(generator); - var expected = @$" + var actual = GenerateText(generator); + var expected = @$" if ((context.I == 1)) return ""one""; return ""other""; ".TrimStart(); - Assert.Equal(expected, actual); - } + Assert.Equal(expected, actual); + } - [Fact] - public void CanGenerateRuleForModuloEquals() + [Fact] + public void CanGenerateRuleForModuloEquals() + { + var generator = new RuleGenerator(new PluralRule(new[] { "en" }, new[] { - var generator = new RuleGenerator(new PluralRule(new[] { "en" }, new[] + new Condition("one", string.Empty, new[] { - new Condition("one", string.Empty, new[] + new OrCondition(new [] { - new OrCondition(new [] - { - new Operation(new ModuloOperand(OperandSymbol.IntegerDigits, 5), Relation.Equals, new[] { new NumberOperand(1) }) - }) + new Operation(new ModuloOperand(OperandSymbol.IntegerDigits, 5), Relation.Equals, new[] { new NumberOperand(1) }) }) - })); + }) + })); - var actual = GenerateText(generator); - var expected = @$" + var actual = GenerateText(generator); + var expected = @$" if ((context.I % 5 == 1)) return ""one""; return ""other""; ".TrimStart(); - Assert.Equal(expected, actual); - } + Assert.Equal(expected, actual); + } - [Fact] - public void CanGenerateRuleForNumberEquals() + [Fact] + public void CanGenerateRuleForNumberEquals() + { + var generator = new RuleGenerator(new PluralRule(new[] { "en" }, new[] { - var generator = new RuleGenerator(new PluralRule(new[] { "en" }, new[] + new Condition("one", string.Empty, new[] { - new Condition("one", string.Empty, new[] + new OrCondition(new [] { - new OrCondition(new [] - { - new Operation(new VariableOperand(OperandSymbol.AbsoluteValue), Relation.Equals, new[] { new NumberOperand(5) }) - }) + new Operation(new VariableOperand(OperandSymbol.AbsoluteValue), Relation.Equals, new[] { new NumberOperand(5) }) }) - })); + }) + })); - var actual = GenerateText(generator); - var expected = @$" + var actual = GenerateText(generator); + var expected = @$" if ((context.N == 5)) return ""one""; return ""other""; ".TrimStart(); - Assert.Equal(expected, actual); - } + Assert.Equal(expected, actual); + } - [Fact] - public void CanGenerateRuleForNumberNotEquals() + [Fact] + public void CanGenerateRuleForNumberNotEquals() + { + var generator = new RuleGenerator(new PluralRule(new[] { "en" }, new[] { - var generator = new RuleGenerator(new PluralRule(new[] { "en" }, new[] + new Condition("one", string.Empty, new[] { - new Condition("one", string.Empty, new[] + new OrCondition(new [] { - new OrCondition(new [] - { - new Operation(new VariableOperand(OperandSymbol.AbsoluteValue), Relation.NotEquals, new[] { new NumberOperand(5) }) - }) + new Operation(new VariableOperand(OperandSymbol.AbsoluteValue), Relation.NotEquals, new[] { new NumberOperand(5) }) }) - })); + }) + })); - var actual = GenerateText(generator); - var expected = @$" + var actual = GenerateText(generator); + var expected = @$" if ((context.N != 5)) return ""one""; return ""other""; ".TrimStart(); - Assert.Equal(expected, actual); - } + Assert.Equal(expected, actual); + } - [Fact] - public void CanGenerateRuleForNumberRange() + [Fact] + public void CanGenerateRuleForNumberRange() + { + var generator = new RuleGenerator(new PluralRule(new[] { "en" }, new[] { - var generator = new RuleGenerator(new PluralRule(new[] { "en" }, new[] + new Condition("one", string.Empty, new[] { - new Condition("one", string.Empty, new[] + new OrCondition(new [] { - new OrCondition(new [] - { - new Operation(new VariableOperand(OperandSymbol.AbsoluteValue), Relation.Equals, new IRightOperand[] { new RangeOperand(5, 6), new NumberOperand(10) }) - }) + new Operation(new VariableOperand(OperandSymbol.AbsoluteValue), Relation.Equals, new IRightOperand[] { new RangeOperand(5, 6), new NumberOperand(10) }) }) - })); + }) + })); - var actual = GenerateText(generator); - var expected = @$" + var actual = GenerateText(generator); + var expected = @$" if ((context.N >= 5 && context.N <= 6 || context.N == 10)) return ""one""; return ""other""; ".TrimStart(); - Assert.Equal(expected, actual); - } + Assert.Equal(expected, actual); + } - [Fact] - public void CanGenerateRuleForNegativeNumberRange() + [Fact] + public void CanGenerateRuleForNegativeNumberRange() + { + var generator = new RuleGenerator(new PluralRule(new[] { "en" }, new[] { - var generator = new RuleGenerator(new PluralRule(new[] { "en" }, new[] + new Condition("one", string.Empty, new[] { - new Condition("one", string.Empty, new[] + new OrCondition(new [] { - new OrCondition(new [] - { - new Operation(new VariableOperand(OperandSymbol.AbsoluteValue), Relation.NotEquals, new IRightOperand[] { new RangeOperand(5, 6), new NumberOperand(10) }) - }) + new Operation(new VariableOperand(OperandSymbol.AbsoluteValue), Relation.NotEquals, new IRightOperand[] { new RangeOperand(5, 6), new NumberOperand(10) }) }) - })); + }) + })); - var actual = GenerateText(generator); - var expected = @$" + var actual = GenerateText(generator); + var expected = @$" if (((context.N < 5 || context.N > 6) && context.N != 10)) return ""one""; return ""other""; ".TrimStart(); - Assert.Equal(expected, actual); - } + Assert.Equal(expected, actual); + } - [Fact] - public void CanGenerateRuleForAndRules() + [Fact] + public void CanGenerateRuleForAndRules() + { + var generator = new RuleGenerator(new PluralRule(new[] { "en" }, new[] { - var generator = new RuleGenerator(new PluralRule(new[] { "en" }, new[] + new Condition("one", string.Empty, new[] { - new Condition("one", string.Empty, new[] + new OrCondition(new [] { - new OrCondition(new [] - { - new Operation(new VariableOperand(OperandSymbol.AbsoluteValue), Relation.Equals, new[] { new NumberOperand(4) }), - new Operation(new VariableOperand(OperandSymbol.VisibleFractionDigitNumber), Relation.Equals, new[] { new NumberOperand(0) }) - }) + new Operation(new VariableOperand(OperandSymbol.AbsoluteValue), Relation.Equals, new[] { new NumberOperand(4) }), + new Operation(new VariableOperand(OperandSymbol.VisibleFractionDigitNumber), Relation.Equals, new[] { new NumberOperand(0) }) }) - })); + }) + })); - var actual = GenerateText(generator); - var expected = @$" + var actual = GenerateText(generator); + var expected = @$" if ((context.N == 4) && (context.V == 0)) return ""one""; return ""other""; ".TrimStart(); - Assert.Equal(expected, actual); - } + Assert.Equal(expected, actual); + } - [Fact] - public void CanGenerateRuleForMixedRangeAndNumberRangeRules() + [Fact] + public void CanGenerateRuleForMixedRangeAndNumberRangeRules() + { + var generator = new RuleGenerator(new PluralRule(new[] { "en" }, new[] { - var generator = new RuleGenerator(new PluralRule(new[] { "en" }, new[] + new Condition("one", string.Empty, new[] { - new Condition("one", string.Empty, new[] + new OrCondition(new [] { - new OrCondition(new [] - { - new Operation(new VariableOperand(OperandSymbol.AbsoluteValue), Relation.Equals, new[] { new RangeOperand(4, 5) }), - new Operation(new VariableOperand(OperandSymbol.VisibleFractionDigitNumber), Relation.Equals, new[] { new NumberOperand(0) }) - }) + new Operation(new VariableOperand(OperandSymbol.AbsoluteValue), Relation.Equals, new[] { new RangeOperand(4, 5) }), + new Operation(new VariableOperand(OperandSymbol.VisibleFractionDigitNumber), Relation.Equals, new[] { new NumberOperand(0) }) }) - })); + }) + })); - var actual = GenerateText(generator); - var expected = @$" + var actual = GenerateText(generator); + var expected = @$" if ((context.N >= 4 && context.N <= 5) && (context.V == 0)) return ""one""; return ""other""; ".TrimStart(); - Assert.Equal(expected, actual); - } + Assert.Equal(expected, actual); + } - [Fact] - public void CanGenerateRuleForMultipleOrRules() + [Fact] + public void CanGenerateRuleForMultipleOrRules() + { + var generator = new RuleGenerator(new PluralRule(new[] { "en" }, new[] { - var generator = new RuleGenerator(new PluralRule(new[] { "en" }, new[] + new Condition("one", string.Empty, new[] { - new Condition("one", string.Empty, new[] + new OrCondition(new [] { - new OrCondition(new [] - { - new Operation(new VariableOperand(OperandSymbol.AbsoluteValue), Relation.Equals, new[] { new NumberOperand(4) }) - }), - new OrCondition(new [] - { - new Operation(new VariableOperand(OperandSymbol.VisibleFractionDigitNumber), Relation.Equals, new[] { new NumberOperand(0) }) - }) + new Operation(new VariableOperand(OperandSymbol.AbsoluteValue), Relation.Equals, new[] { new NumberOperand(4) }) + }), + new OrCondition(new [] + { + new Operation(new VariableOperand(OperandSymbol.VisibleFractionDigitNumber), Relation.Equals, new[] { new NumberOperand(0) }) }) - })); + }) + })); - var actual = GenerateText(generator); - var expected = @$" + var actual = GenerateText(generator); + var expected = @$" if ((context.N == 4) || (context.V == 0)) return ""one""; return ""other""; ".TrimStart(); - Assert.Equal(expected, actual); - } + Assert.Equal(expected, actual); + } - [Fact] - public void CanGenerateRuleForMixedAndOrRules() + [Fact] + public void CanGenerateRuleForMixedAndOrRules() + { + var generator = new RuleGenerator(new PluralRule(new[] { "en" }, new[] { - var generator = new RuleGenerator(new PluralRule(new[] { "en" }, new[] + new Condition("one", string.Empty, new[] { - new Condition("one", string.Empty, new[] + new OrCondition(new [] + { + new Operation(new VariableOperand(OperandSymbol.AbsoluteValue), Relation.Equals, new[] { new NumberOperand(4) }), + new Operation(new VariableOperand(OperandSymbol.VisibleFractionDigitNumber), Relation.NotEquals, new[] { new NumberOperand(0) }) + }), + new OrCondition(new [] { - new OrCondition(new [] - { - new Operation(new VariableOperand(OperandSymbol.AbsoluteValue), Relation.Equals, new[] { new NumberOperand(4) }), - new Operation(new VariableOperand(OperandSymbol.VisibleFractionDigitNumber), Relation.NotEquals, new[] { new NumberOperand(0) }) - }), - new OrCondition(new [] - { - new Operation(new VariableOperand(OperandSymbol.VisibleFractionDigitNumber), Relation.Equals, new[] { new NumberOperand(0) }) - }) + new Operation(new VariableOperand(OperandSymbol.VisibleFractionDigitNumber), Relation.Equals, new[] { new NumberOperand(0) }) }) - })); + }) + })); - var actual = GenerateText(generator); - var expected = @$" + var actual = GenerateText(generator); + var expected = @$" if ((context.N == 4) && (context.V != 0) || (context.V == 0)) return ""one""; return ""other""; ".TrimStart(); - Assert.Equal(expected, actual); - } + Assert.Equal(expected, actual); + } - [Fact] - public void CanGenerateRuleForMixedAndOrRangeRules() + [Fact] + public void CanGenerateRuleForMixedAndOrRangeRules() + { + var generator = new RuleGenerator(new PluralRule(new[] { "en" }, new[] { - var generator = new RuleGenerator(new PluralRule(new[] { "en" }, new[] + new Condition("one", string.Empty, new[] { - new Condition("one", string.Empty, new[] + new OrCondition(new [] { - new OrCondition(new [] - { - new Operation(new VariableOperand(OperandSymbol.AbsoluteValue), Relation.Equals, new[] { new RangeOperand(4, 5) }), - new Operation(new VariableOperand(OperandSymbol.VisibleFractionDigitNumber), Relation.NotEquals, new[] { new NumberOperand(0) }) - }), - new OrCondition(new [] - { - new Operation(new VariableOperand(OperandSymbol.VisibleFractionDigitNumber), Relation.Equals, new[] { new NumberOperand(0) }) - }) + new Operation(new VariableOperand(OperandSymbol.AbsoluteValue), Relation.Equals, new[] { new RangeOperand(4, 5) }), + new Operation(new VariableOperand(OperandSymbol.VisibleFractionDigitNumber), Relation.NotEquals, new[] { new NumberOperand(0) }) + }), + new OrCondition(new [] + { + new Operation(new VariableOperand(OperandSymbol.VisibleFractionDigitNumber), Relation.Equals, new[] { new NumberOperand(0) }) }) - })); + }) + })); - var actual = GenerateText(generator); - var expected = @$" + var actual = GenerateText(generator); + var expected = @$" if ((context.N >= 4 && context.N <= 5) && (context.V != 0) || (context.V == 0)) return ""one""; return ""other""; ".TrimStart(); - Assert.Equal(expected, actual); - } + Assert.Equal(expected, actual); + } - private string GenerateText(RuleGenerator generator) - { - var sb = new StringBuilder(); + private string GenerateText(RuleGenerator generator) + { + var sb = new StringBuilder(); - generator.WriteTo(sb, 0); + generator.WriteTo(sb, 0); - return sb.ToString(); - } + return sb.ToString(); } -} +} \ No newline at end of file diff --git a/src/Jeffijoe.MessageFormat.Tests/Parsing/FormatterRequestCollectionTests.cs b/src/Jeffijoe.MessageFormat.Tests/Parsing/FormatterRequestCollectionTests.cs index 4ca2b19..1ec119b 100644 --- a/src/Jeffijoe.MessageFormat.Tests/Parsing/FormatterRequestCollectionTests.cs +++ b/src/Jeffijoe.MessageFormat.Tests/Parsing/FormatterRequestCollectionTests.cs @@ -10,84 +10,83 @@ using Xunit; -namespace Jeffijoe.MessageFormat.Tests.Parsing +namespace Jeffijoe.MessageFormat.Tests.Parsing; + +/// +/// The formatter request collection tests. +/// +public class FormatterRequestCollectionTests { + #region Public Methods and Operators + /// - /// The formatter request collection tests. + /// The clone. /// - public class FormatterRequestCollectionTests + [Fact] + public void Clone() { - #region Public Methods and Operators - - /// - /// The clone. - /// - [Fact] - public void Clone() - { - var subject = new FormatterRequestCollection(); - subject.Add( - new FormatterRequest( - new Literal(0, 9, 1, 1, new string('a', 10)), - "test", - "test", - "test")); - subject.Add( - new FormatterRequest( - new Literal(10, 19, 1, 1, new string('a', 10)), - "test", - "test", - "test")); - subject.Add( - new FormatterRequest( - new Literal(20, 29, 1, 1, new string('a', 10)), - "test", - "test", - "test")); + var subject = new FormatterRequestCollection(); + subject.Add( + new FormatterRequest( + new Literal(0, 9, 1, 1, new string('a', 10)), + "test", + "test", + "test")); + subject.Add( + new FormatterRequest( + new Literal(10, 19, 1, 1, new string('a', 10)), + "test", + "test", + "test")); + subject.Add( + new FormatterRequest( + new Literal(20, 29, 1, 1, new string('a', 10)), + "test", + "test", + "test")); - var cloned = subject.Clone(); - Assert.Equal(subject.Count, cloned.Count()); + var cloned = subject.Clone(); + Assert.Equal(subject.Count, cloned.Count()); - foreach (var clonedReq in cloned) - { - Assert.DoesNotContain(subject, x => ReferenceEquals(x, clonedReq)); - Assert.DoesNotContain(subject, x => x.SourceLiteral == clonedReq.SourceLiteral); - Assert.Contains(subject, x => x.SourceLiteral.StartIndex == clonedReq.SourceLiteral.StartIndex); - } - } - - /// - /// The shift indices. - /// - [Fact] - public void ShiftIndices() + foreach (var clonedReq in cloned) { - var subject = new FormatterRequestCollection(); - subject.Add( - new FormatterRequest( - new Literal(0, 9, 1, 1, new string('a', 10)), - "test", - "test", - "test")); - subject.Add( - new FormatterRequest( - new Literal(10, 19, 1, 1, new string('a', 10)), - "test", - "test", - "test")); - subject.Add( - new FormatterRequest( - new Literal(20, 29, 1, 1, new string('a', 10)), - "test", - "test", - "test")); - subject.ShiftIndices(1, 4); - Assert.Equal(0, subject[0].SourceLiteral.StartIndex); - Assert.Equal(10, subject[1].SourceLiteral.StartIndex); - - Assert.Equal(14, subject[2].SourceLiteral.StartIndex); + Assert.DoesNotContain(subject, x => ReferenceEquals(x, clonedReq)); + Assert.DoesNotContain(subject, x => x.SourceLiteral == clonedReq.SourceLiteral); + Assert.Contains(subject, x => x.SourceLiteral.StartIndex == clonedReq.SourceLiteral.StartIndex); } + } + + /// + /// The shift indices. + /// + [Fact] + public void ShiftIndices() + { + var subject = new FormatterRequestCollection(); + subject.Add( + new FormatterRequest( + new Literal(0, 9, 1, 1, new string('a', 10)), + "test", + "test", + "test")); + subject.Add( + new FormatterRequest( + new Literal(10, 19, 1, 1, new string('a', 10)), + "test", + "test", + "test")); + subject.Add( + new FormatterRequest( + new Literal(20, 29, 1, 1, new string('a', 10)), + "test", + "test", + "test")); + subject.ShiftIndices(1, 4); + Assert.Equal(0, subject[0].SourceLiteral.StartIndex); + Assert.Equal(10, subject[1].SourceLiteral.StartIndex); - #endregion + Assert.Equal(14, subject[2].SourceLiteral.StartIndex); } + + #endregion } \ No newline at end of file diff --git a/src/Jeffijoe.MessageFormat.Tests/Parsing/LiteralParserTests.cs b/src/Jeffijoe.MessageFormat.Tests/Parsing/LiteralParserTests.cs index 8be5277..de7237a 100644 --- a/src/Jeffijoe.MessageFormat.Tests/Parsing/LiteralParserTests.cs +++ b/src/Jeffijoe.MessageFormat.Tests/Parsing/LiteralParserTests.cs @@ -12,187 +12,186 @@ using Xunit; -namespace Jeffijoe.MessageFormat.Tests.Parsing +namespace Jeffijoe.MessageFormat.Tests.Parsing; + +/// +/// The literal parser tests. +/// +public class LiteralParserTests { + #region Public Methods and Operators + + /// + /// The parse literals_bracket_mismatch. + /// + /// + /// The source. + /// + /// + /// The expected open brace count. + /// + /// + /// The expected close brace count. + /// + [Theory] + [InlineData("{", 1, 0)] + [InlineData("}", 0, 1)] + [InlineData("A beginning {", 1, 0)] + [InlineData("An ending }", 0, 1)] + [InlineData("One { and multiple }}", 1, 2)] + [InlineData("A few {{{{ and one }", 4, 1)] + [InlineData("A few {{{{ and one '}'}", 4, 1)] + [InlineData("A few '{'{{{{ and one '}'}", 4, 1)] + public void ParseLiterals_bracket_mismatch( + string source, + int expectedOpenBraceCount, + int expectedCloseBraceCount) + { + var sb = new StringBuilder(source); + var subject = new LiteralParser(); + var ex = Assert.Throws(() => subject.ParseLiterals(sb)); + Assert.Equal(expectedOpenBraceCount, ex.OpenBraceCount); + Assert.Equal(expectedCloseBraceCount, ex.CloseBraceCount); + } + /// - /// The literal parser tests. + /// The parse literals_count. /// - public class LiteralParserTests + /// + /// The source. + /// + /// + /// The expected match count. + /// + [Theory] + [InlineData("Hello, {something smells {really} weird.}", 1)] + [InlineData("Hello, {something smells {really} weird.}, {Hi}", 2)] + [InlineData("Hello, {something smells {really} weird.}, '{Hi}'", 1)] + public void ParseLiterals_count(string source, int expectedMatchCount) { - #region Public Methods and Operators - - /// - /// The parse literals_bracket_mismatch. - /// - /// - /// The source. - /// - /// - /// The expected open brace count. - /// - /// - /// The expected close brace count. - /// - [Theory] - [InlineData("{", 1, 0)] - [InlineData("}", 0, 1)] - [InlineData("A beginning {", 1, 0)] - [InlineData("An ending }", 0, 1)] - [InlineData("One { and multiple }}", 1, 2)] - [InlineData("A few {{{{ and one }", 4, 1)] - [InlineData("A few {{{{ and one '}'}", 4, 1)] - [InlineData("A few '{'{{{{ and one '}'}", 4, 1)] - public void ParseLiterals_bracket_mismatch( - string source, - int expectedOpenBraceCount, - int expectedCloseBraceCount) - { - var sb = new StringBuilder(source); - var subject = new LiteralParser(); - var ex = Assert.Throws(() => subject.ParseLiterals(sb)); - Assert.Equal(expectedOpenBraceCount, ex.OpenBraceCount); - Assert.Equal(expectedCloseBraceCount, ex.CloseBraceCount); - } - - /// - /// The parse literals_count. - /// - /// - /// The source. - /// - /// - /// The expected match count. - /// - [Theory] - [InlineData("Hello, {something smells {really} weird.}", 1)] - [InlineData("Hello, {something smells {really} weird.}, {Hi}", 2)] - [InlineData("Hello, {something smells {really} weird.}, '{Hi}'", 1)] - public void ParseLiterals_count(string source, int expectedMatchCount) - { - var sb = new StringBuilder(source); - var subject = new LiteralParser(); - var actual = subject.ParseLiterals(sb); - Assert.Equal(expectedMatchCount, actual.Count()); - } - - /// - /// The parse unclosed_escape_sequence. - /// - /// - /// The source. - /// - /// - /// The expected line number. - /// - /// - /// The expected column number. - /// - [Theory] - [InlineData("'{", 1, 1)] - [InlineData("'}", 1, 1)] - [InlineData("a {b {c} d}, '{open escape sequence}", 1, 14)] - [InlineData(@"Hello, + var sb = new StringBuilder(source); + var subject = new LiteralParser(); + var actual = subject.ParseLiterals(sb); + Assert.Equal(expectedMatchCount, actual.Count()); + } + + /// + /// The parse unclosed_escape_sequence. + /// + /// + /// The source. + /// + /// + /// The expected line number. + /// + /// + /// The expected column number. + /// + [Theory] + [InlineData("'{", 1, 1)] + [InlineData("'}", 1, 1)] + [InlineData("a {b {c} d}, '{open escape sequence}", 1, 14)] + [InlineData(@"Hello, '{World}", 2, 1)] - public void ParseLiterals_unclosed_escape_sequence( - string source, - int expectedLineNumber, - int expectedColumnNumber) - { - var sb = new StringBuilder(source); - var subject = new LiteralParser(); - var ex = Assert.Throws(() => subject.ParseLiterals(sb)); - Assert.Equal(expectedLineNumber, ex.LineNumber); - Assert.Equal(expectedColumnNumber, ex.ColumnNumber); - } - - /// - /// The parse literals_position_and_inner_text. - /// - /// - /// The source. - /// - /// - /// The position. - /// - /// - /// The expected inner text. - /// - [Theory] - [InlineData("Hello, {something smells {really} weird.}", new[] { 7, 40 }, "something smells {really} weird.")] - [InlineData("Pretty {sweet}, right?", new[] { 7, 13 }, "sweet")] - [InlineData(@"{ + public void ParseLiterals_unclosed_escape_sequence( + string source, + int expectedLineNumber, + int expectedColumnNumber) + { + var sb = new StringBuilder(source); + var subject = new LiteralParser(); + var ex = Assert.Throws(() => subject.ParseLiterals(sb)); + Assert.Equal(expectedLineNumber, ex.LineNumber); + Assert.Equal(expectedColumnNumber, ex.ColumnNumber); + } + + /// + /// The parse literals_position_and_inner_text. + /// + /// + /// The source. + /// + /// + /// The position. + /// + /// + /// The expected inner text. + /// + [Theory] + [InlineData("Hello, {something smells {really} weird.}", new[] { 7, 40 }, "something smells {really} weird.")] + [InlineData("Pretty {sweet}, right?", new[] { 7, 13 }, "sweet")] + [InlineData(@"{ sweet }, right?", new[] { 0, 9 }, @" sweet ")] - [InlineData(@"{ + [InlineData(@"{ '{sweet}' }, right?", new[] { 0, 13 }, @" '{sweet}' ")] - public void ParseLiterals_position_and_inner_text(string source, int[] position, string expectedInnerText) - { - // It seems that depending on platform this is compiled on, the actual representation of new lines in the - // string literals can differ, which can make this test fail due to differences. - // This will normalize those changes. - expectedInnerText = expectedInnerText.Replace("\r\n", "\n"); - - var sb = new StringBuilder(source); - var subject = new LiteralParser(); - var actual = subject.ParseLiterals(sb); - var first = actual.First(); - string innerText = first.InnerText; - Assert.Equal(expectedInnerText, innerText); - Assert.Equal(position[0], first.StartIndex); - - // Makes up for line-ending differences due to Git. - var expectedEndIndex = position[1] + source.Count(c => c == '\r'); - var expectedSourceColumnNumber = first.StartIndex + 1; - Assert.Equal(expectedEndIndex, first.EndIndex); - - Assert.Equal(expectedSourceColumnNumber, first.SourceColumnNumber); - } - - /// - /// The parse literals_source_line_and_column_number. - /// - /// - /// The source. - /// - /// - /// The line number. - /// - /// - /// The column number. - /// - [Theory] - [InlineData(@"Hi, this is + public void ParseLiterals_position_and_inner_text(string source, int[] position, string expectedInnerText) + { + // It seems that depending on platform this is compiled on, the actual representation of new lines in the + // string literals can differ, which can make this test fail due to differences. + // This will normalize those changes. + expectedInnerText = expectedInnerText.Replace("\r\n", "\n"); + + var sb = new StringBuilder(source); + var subject = new LiteralParser(); + var actual = subject.ParseLiterals(sb); + var first = actual.First(); + string innerText = first.InnerText; + Assert.Equal(expectedInnerText, innerText); + Assert.Equal(position[0], first.StartIndex); + + // Makes up for line-ending differences due to Git. + var expectedEndIndex = position[1] + source.Count(c => c == '\r'); + var expectedSourceColumnNumber = first.StartIndex + 1; + Assert.Equal(expectedEndIndex, first.EndIndex); + + Assert.Equal(expectedSourceColumnNumber, first.SourceColumnNumber); + } + + /// + /// The parse literals_source_line_and_column_number. + /// + /// + /// The source. + /// + /// + /// The line number. + /// + /// + /// The column number. + /// + [Theory] + [InlineData(@"Hi, this is {a tricky one} yeeah! ", 3, 1)] - [InlineData(@"Hi, this is + [InlineData(@"Hi, this is {a tricky one} yeeah! ", 4, 3)] - public void ParseLiterals_source_line_and_column_number(string source, int lineNumber, int columnNumber) - { - var sb = new StringBuilder(source); - var subject = new LiteralParser(); - var actual = subject.ParseLiterals(sb); - var first = actual.First(); - Assert.Equal(lineNumber, first.SourceLineNumber); - Assert.Equal(columnNumber, first.SourceColumnNumber); - } - - #endregion + public void ParseLiterals_source_line_and_column_number(string source, int lineNumber, int columnNumber) + { + var sb = new StringBuilder(source); + var subject = new LiteralParser(); + var actual = subject.ParseLiterals(sb); + var first = actual.First(); + Assert.Equal(lineNumber, first.SourceLineNumber); + Assert.Equal(columnNumber, first.SourceColumnNumber); } + + #endregion } \ No newline at end of file diff --git a/src/Jeffijoe.MessageFormat.Tests/Parsing/LiteralTests.cs b/src/Jeffijoe.MessageFormat.Tests/Parsing/LiteralTests.cs index 0addf7a..a6500f0 100644 --- a/src/Jeffijoe.MessageFormat.Tests/Parsing/LiteralTests.cs +++ b/src/Jeffijoe.MessageFormat.Tests/Parsing/LiteralTests.cs @@ -8,31 +8,30 @@ using Xunit; -namespace Jeffijoe.MessageFormat.Tests.Parsing +namespace Jeffijoe.MessageFormat.Tests.Parsing; + +/// +/// The literal tests. +/// +public class LiteralTests { + #region Public Methods and Operators + /// - /// The literal tests. + /// The shift indices. /// - public class LiteralTests + [Fact] + public void ShiftIndices() { - #region Public Methods and Operators - - /// - /// The shift indices. - /// - [Fact] - public void ShiftIndices() - { - var subject = new Literal(20, 29, 1, 1, new string('a', 10)); - var other = new Literal(5, 10, 1, 1, new string('a', 6)); + var subject = new Literal(20, 29, 1, 1, new string('a', 10)); + var other = new Literal(5, 10, 1, 1, new string('a', 6)); - subject.ShiftIndices(2, other); + subject.ShiftIndices(2, other); - // I honestly have no explanation for this, but it works with the formatter. Magic? - Assert.Equal(18, subject.StartIndex); - Assert.Equal(27, subject.EndIndex); - } - - #endregion + // I honestly have no explanation for this, but it works with the formatter. Magic? + Assert.Equal(18, subject.StartIndex); + Assert.Equal(27, subject.EndIndex); } + + #endregion } \ No newline at end of file diff --git a/src/Jeffijoe.MessageFormat.Tests/Parsing/PatternParserGetKeyTests.cs b/src/Jeffijoe.MessageFormat.Tests/Parsing/PatternParserGetKeyTests.cs index 77914a4..86a49ad 100644 --- a/src/Jeffijoe.MessageFormat.Tests/Parsing/PatternParserGetKeyTests.cs +++ b/src/Jeffijoe.MessageFormat.Tests/Parsing/PatternParserGetKeyTests.cs @@ -9,139 +9,138 @@ using Xunit; using Xunit.Abstractions; -namespace Jeffijoe.MessageFormat.Tests.Parsing +namespace Jeffijoe.MessageFormat.Tests.Parsing; + +/// +/// The pattern parser_ get key_ tests. +/// +public class PatternParserGetKeyTests { + #region Fields + /// - /// The pattern parser_ get key_ tests. + /// The output helper. /// - public class PatternParserGetKeyTests - { - #region Fields + private ITestOutputHelper outputHelper; - /// - /// The output helper. - /// - private ITestOutputHelper outputHelper; + #endregion - #endregion + #region Constructors and Destructors - #region Constructors and Destructors - - /// - /// Initializes a new instance of the class. - /// - /// - /// The output helper. - /// - public PatternParserGetKeyTests(ITestOutputHelper outputHelper) - { - this.outputHelper = outputHelper; - } + /// + /// Initializes a new instance of the class. + /// + /// + /// The output helper. + /// + public PatternParserGetKeyTests(ITestOutputHelper outputHelper) + { + this.outputHelper = outputHelper; + } - #endregion + #endregion - #region Public Properties + #region Public Properties - /// - /// Gets the get key_throws_with_invalid_characters_ case. - /// - public static IEnumerable GetKeyThrowsWithInvalidCharactersCase + /// + /// Gets the get key_throws_with_invalid_characters_ case. + /// + public static IEnumerable GetKeyThrowsWithInvalidCharactersCase + { + get { - get - { - yield return new object[] { new Literal(3, 10, 1, 3, "Hellåw,"), 1, 8 }; - yield return new object[] { new Literal(0, 0, 3, 3, ","), 3, 4 }; - yield return new object[] { new Literal(0, 0, 3, 3, " hello dawg"), 0, 0 }; - yield return new object[] { new Literal(0, 0, 3, 3, "hello dawg "), 0, 0 }; - yield return new object[] { new Literal(0, 0, 3, 3, " hello dawg"), 0, 0 }; - yield return new object[] { new Literal(0, 0, 3, 3, " hello\r\ndawg"), 0, 0 }; - } + yield return new object[] { new Literal(3, 10, 1, 3, "Hellåw,"), 1, 8 }; + yield return new object[] { new Literal(0, 0, 3, 3, ","), 3, 4 }; + yield return new object[] { new Literal(0, 0, 3, 3, " hello dawg"), 0, 0 }; + yield return new object[] { new Literal(0, 0, 3, 3, "hello dawg "), 0, 0 }; + yield return new object[] { new Literal(0, 0, 3, 3, " hello dawg"), 0, 0 }; + yield return new object[] { new Literal(0, 0, 3, 3, " hello\r\ndawg"), 0, 0 }; } + } - #endregion - - #region Public Methods and Operators - - /// - /// The read literal section. - /// - /// - /// The source. - /// - /// - /// The expected. - /// - /// - /// The expected last index. - /// - [Theory] - [InlineData("SupDawg, yeah", "SupDawg", 7)] - [InlineData("hello", "hello", 4)] - [InlineData(" hello ", "hello", 6)] - [InlineData("\r\nhello ", "hello", 7)] - [InlineData("0,", "0", 1)] - [InlineData("0, ", "0", 1)] - [InlineData("0 ,", "0", 2)] - [InlineData("0", "0", 0)] - public void ReadLiteralSection(string source, string expected, int expectedLastIndex) - { - var literal = new Literal(10, 10, 1, 1, source); - int lastIndex; - Assert.Equal(expected, PatternParser.ReadLiteralSection(literal, 0, false, out lastIndex)); - Assert.Equal(expectedLastIndex, lastIndex); - } + #endregion - /// - /// The read literal section_throws_with_invalid_characters. - /// - /// - /// The literal. - /// - /// - /// The expected line. - /// - /// - /// The expected column. - /// - [Theory] - [MemberData(nameof(GetKeyThrowsWithInvalidCharactersCase))] - public void ReadLiteralSection_throws_with_invalid_characters( - Literal literal, - int expectedLine, - int expectedColumn) - { - var ex = - Assert.Throws( - () => PatternParser.ReadLiteralSection(literal, 0, false, out _)); - Assert.Equal(expectedLine, ex.LineNumber); - Assert.Equal(expectedColumn, ex.ColumnNumber); - this.outputHelper.WriteLine(ex.Message); - } + #region Public Methods and Operators - /// - /// The read literal section_with_offset. - /// - /// - /// The source. - /// - /// - /// The expected. - /// - /// - /// The offset. - /// - [Theory] - [InlineData("SupDawg, yeah", "yeah", 8)] - [InlineData("SupDawg,yeah", "yeah", 8)] - [InlineData("SupDawg,yeah ", "yeah", 8)] - [InlineData("SupDawg, ", null, 8)] - [InlineData("SupDawg,", null, 8)] - public void ReadLiteralSection_with_offset(string source, string? expected, int offset) - { - var literal = new Literal(10, 10, 1, 1, source); - Assert.Equal(expected, PatternParser.ReadLiteralSection(literal, offset, true, out _)); - } + /// + /// The read literal section. + /// + /// + /// The source. + /// + /// + /// The expected. + /// + /// + /// The expected last index. + /// + [Theory] + [InlineData("SupDawg, yeah", "SupDawg", 7)] + [InlineData("hello", "hello", 4)] + [InlineData(" hello ", "hello", 6)] + [InlineData("\r\nhello ", "hello", 7)] + [InlineData("0,", "0", 1)] + [InlineData("0, ", "0", 1)] + [InlineData("0 ,", "0", 2)] + [InlineData("0", "0", 0)] + public void ReadLiteralSection(string source, string expected, int expectedLastIndex) + { + var literal = new Literal(10, 10, 1, 1, source); + int lastIndex; + Assert.Equal(expected, PatternParser.ReadLiteralSection(literal, 0, false, out lastIndex)); + Assert.Equal(expectedLastIndex, lastIndex); + } + + /// + /// The read literal section_throws_with_invalid_characters. + /// + /// + /// The literal. + /// + /// + /// The expected line. + /// + /// + /// The expected column. + /// + [Theory] + [MemberData(nameof(GetKeyThrowsWithInvalidCharactersCase))] + public void ReadLiteralSection_throws_with_invalid_characters( + Literal literal, + int expectedLine, + int expectedColumn) + { + var ex = + Assert.Throws( + () => PatternParser.ReadLiteralSection(literal, 0, false, out _)); + Assert.Equal(expectedLine, ex.LineNumber); + Assert.Equal(expectedColumn, ex.ColumnNumber); + this.outputHelper.WriteLine(ex.Message); + } - #endregion + /// + /// The read literal section_with_offset. + /// + /// + /// The source. + /// + /// + /// The expected. + /// + /// + /// The offset. + /// + [Theory] + [InlineData("SupDawg, yeah", "yeah", 8)] + [InlineData("SupDawg,yeah", "yeah", 8)] + [InlineData("SupDawg,yeah ", "yeah", 8)] + [InlineData("SupDawg, ", null, 8)] + [InlineData("SupDawg,", null, 8)] + public void ReadLiteralSection_with_offset(string source, string? expected, int offset) + { + var literal = new Literal(10, 10, 1, 1, source); + Assert.Equal(expected, PatternParser.ReadLiteralSection(literal, offset, true, out _)); } + + #endregion } \ No newline at end of file diff --git a/src/Jeffijoe.MessageFormat.Tests/Parsing/PatternParserParseTests.cs b/src/Jeffijoe.MessageFormat.Tests/Parsing/PatternParserParseTests.cs index ebebbbe..5c57718 100644 --- a/src/Jeffijoe.MessageFormat.Tests/Parsing/PatternParserParseTests.cs +++ b/src/Jeffijoe.MessageFormat.Tests/Parsing/PatternParserParseTests.cs @@ -12,102 +12,101 @@ using Xunit; using Xunit.Abstractions; -namespace Jeffijoe.MessageFormat.Tests.Parsing +namespace Jeffijoe.MessageFormat.Tests.Parsing; + +/// +/// The pattern parser_ parse_ tests. +/// +public class PatternParserParseTests { + #region Fields + /// - /// The pattern parser_ parse_ tests. + /// The output helper. /// - public class PatternParserParseTests - { - #region Fields + private readonly ITestOutputHelper outputHelper; - /// - /// The output helper. - /// - private readonly ITestOutputHelper outputHelper; + #endregion - #endregion + #region Constructors and Destructors - #region Constructors and Destructors - - /// - /// Initializes a new instance of the class. - /// - /// - /// The output helper. - /// - public PatternParserParseTests(ITestOutputHelper outputHelper) - { - this.outputHelper = outputHelper; - } + /// + /// Initializes a new instance of the class. + /// + /// + /// The output helper. + /// + public PatternParserParseTests(ITestOutputHelper outputHelper) + { + this.outputHelper = outputHelper; + } - #endregion + #endregion - #region Public Methods and Operators + #region Public Methods and Operators - /// - /// The parse. - /// - /// - /// The source. - /// - /// - /// The expected key. - /// - /// - /// The expected format. - /// - /// - /// The expected args. - /// - [Theory] - [InlineData("test, select, args", "test", "select", "args")] - [InlineData("test, select, stuff {dawg}", "test", "select", "stuff {dawg}")] - [InlineData("test, select, stuff {dawg's}", "test", "select", "stuff {dawg's}")] - [InlineData("test, select, stuff {dawg''s}", "test", "select", "stuff {dawg''s}")] - [InlineData("test, select, stuff '{{dawg}}'", "test", "select", "stuff '{{dawg}}'")] - [InlineData("test, select, stuff {dawg, select, {name is '{'{name}'}'}}", "test", "select", - "stuff {dawg, select, {name is '{'{name}'}'}}")] - public void Parse(string source, string expectedKey, string expectedFormat, string expectedArgs) - { - var literalParser = FakeLiteralParser.Of(source); - var sb = new StringBuilder(source); - var subject = new PatternParser(literalParser); + /// + /// The parse. + /// + /// + /// The source. + /// + /// + /// The expected key. + /// + /// + /// The expected format. + /// + /// + /// The expected args. + /// + [Theory] + [InlineData("test, select, args", "test", "select", "args")] + [InlineData("test, select, stuff {dawg}", "test", "select", "stuff {dawg}")] + [InlineData("test, select, stuff {dawg's}", "test", "select", "stuff {dawg's}")] + [InlineData("test, select, stuff {dawg''s}", "test", "select", "stuff {dawg''s}")] + [InlineData("test, select, stuff '{{dawg}}'", "test", "select", "stuff '{{dawg}}'")] + [InlineData("test, select, stuff {dawg, select, {name is '{'{name}'}'}}", "test", "select", + "stuff {dawg, select, {name is '{'{name}'}'}}")] + public void Parse(string source, string expectedKey, string expectedFormat, string expectedArgs) + { + var literalParser = FakeLiteralParser.Of(source); + var sb = new StringBuilder(source); + var subject = new PatternParser(literalParser); - // Warm up (JIT) - Benchmark.Start("Parsing formatter patterns (first time before JIT)", this.outputHelper); - subject.Parse(sb); - Benchmark.End(this.outputHelper); - Benchmark.Start("Parsing formatter patterns (after warm-up)", this.outputHelper); - var actual = subject.Parse(sb); - Benchmark.End(this.outputHelper); - Assert.Single(actual); - var first = actual[0]; - Assert.Equal(expectedKey, first.Variable); - Assert.Equal(expectedFormat, first.FormatterName); - Assert.Equal(expectedArgs, first.FormatterArguments); - } + // Warm up (JIT) + Benchmark.Start("Parsing formatter patterns (first time before JIT)", this.outputHelper); + subject.Parse(sb); + Benchmark.End(this.outputHelper); + Benchmark.Start("Parsing formatter patterns (after warm-up)", this.outputHelper); + var actual = subject.Parse(sb); + Benchmark.End(this.outputHelper); + Assert.Single(actual); + var first = actual[0]; + Assert.Equal(expectedKey, first.Variable); + Assert.Equal(expectedFormat, first.FormatterName); + Assert.Equal(expectedArgs, first.FormatterArguments); + } - /// - /// The parse_exits_early_when_no_literals_have_been_found. - /// - [Fact] - public void Parse_exits_early_when_no_literals_have_been_found() - { - var subject = new PatternParser(); - Assert.Empty(subject.Parse(new StringBuilder())); - } - - /// - /// The parse_throws_when_only_whitespace_is_present_in_section - /// - [Fact] - public void Parse_throws_when_only_whitespace_is_present_in_section() - { - var subject = new PatternParser(); - Assert.Throws(() => subject.Parse(new StringBuilder("{ }"))); - } + /// + /// The parse_exits_early_when_no_literals_have_been_found. + /// + [Fact] + public void Parse_exits_early_when_no_literals_have_been_found() + { + var subject = new PatternParser(); + Assert.Empty(subject.Parse(new StringBuilder())); + } - #endregion + /// + /// The parse_throws_when_only_whitespace_is_present_in_section + /// + [Fact] + public void Parse_throws_when_only_whitespace_is_present_in_section() + { + var subject = new PatternParser(); + Assert.Throws(() => subject.Parse(new StringBuilder("{ }"))); } + + #endregion } \ No newline at end of file diff --git a/src/Jeffijoe.MessageFormat.Tests/Parsing/PatternParserWithRealLiteralParser.cs b/src/Jeffijoe.MessageFormat.Tests/Parsing/PatternParserWithRealLiteralParser.cs index 42df017..d0b4ec2 100644 --- a/src/Jeffijoe.MessageFormat.Tests/Parsing/PatternParserWithRealLiteralParser.cs +++ b/src/Jeffijoe.MessageFormat.Tests/Parsing/PatternParserWithRealLiteralParser.cs @@ -13,70 +13,69 @@ using Xunit; using Xunit.Abstractions; -namespace Jeffijoe.MessageFormat.Tests.Parsing +namespace Jeffijoe.MessageFormat.Tests.Parsing; + +/// +/// The pattern parser_with_real_ literal parser. +/// +public class PatternParserWithRealLiteralParser { + #region Fields + /// - /// The pattern parser_with_real_ literal parser. + /// The output helper. /// - public class PatternParserWithRealLiteralParser - { - #region Fields - - /// - /// The output helper. - /// - private readonly ITestOutputHelper outputHelper; + private readonly ITestOutputHelper outputHelper; - #endregion + #endregion - #region Constructors and Destructors + #region Constructors and Destructors - /// - /// Initializes a new instance of the class. - /// - /// - /// The output helper. - /// - public PatternParserWithRealLiteralParser(ITestOutputHelper outputHelper) - { - this.outputHelper = outputHelper; - } + /// + /// Initializes a new instance of the class. + /// + /// + /// The output helper. + /// + public PatternParserWithRealLiteralParser(ITestOutputHelper outputHelper) + { + this.outputHelper = outputHelper; + } - #endregion + #endregion - #region Public Methods and Operators + #region Public Methods and Operators - /// - /// The parse. - /// - [Fact] - public void Parse() - { - var subject = new PatternParser(new LiteralParser()); + /// + /// The parse. + /// + [Fact] + public void Parse() + { + var subject = new PatternParser(new LiteralParser()); - const string Source = @"Hi, {Name, select, + const string Source = @"Hi, {Name, select, male={guy} female={gal}}, you have {count, plural, zero {no friends}, other {# friends} }"; - Benchmark.Start("First run (warm-up)", this.outputHelper); - subject.Parse(new StringBuilder(Source)); - Benchmark.End(this.outputHelper); - - Benchmark.Start("Next one (warmed up)", this.outputHelper); - var actual = subject.Parse(new StringBuilder(Source)); - Benchmark.End(this.outputHelper); - Assert.Equal(2, actual.Count()); - var formatterParam = actual.First(); - Assert.Equal("Name", formatterParam.Variable); - Assert.Equal("select", formatterParam.FormatterName); - Assert.Equal("male={guy} female={gal}", formatterParam.FormatterArguments); - - formatterParam = actual.ElementAt(1); - Assert.Equal("count", formatterParam.Variable); - Assert.Equal("plural", formatterParam.FormatterName); - Assert.Equal("zero {no friends}, other {# friends}", formatterParam.FormatterArguments); - } - - #endregion + Benchmark.Start("First run (warm-up)", this.outputHelper); + subject.Parse(new StringBuilder(Source)); + Benchmark.End(this.outputHelper); + + Benchmark.Start("Next one (warmed up)", this.outputHelper); + var actual = subject.Parse(new StringBuilder(Source)); + Benchmark.End(this.outputHelper); + Assert.Equal(2, actual.Count()); + var formatterParam = actual.First(); + Assert.Equal("Name", formatterParam.Variable); + Assert.Equal("select", formatterParam.FormatterName); + Assert.Equal("male={guy} female={gal}", formatterParam.FormatterArguments); + + formatterParam = actual.ElementAt(1); + Assert.Equal("count", formatterParam.Variable); + Assert.Equal("plural", formatterParam.FormatterName); + Assert.Equal("zero {no friends}, other {# friends}", formatterParam.FormatterArguments); } + + #endregion } \ No newline at end of file diff --git a/src/Jeffijoe.MessageFormat.Tests/TestHelpers/Benchmark.cs b/src/Jeffijoe.MessageFormat.Tests/TestHelpers/Benchmark.cs index 416a588..011717c 100644 --- a/src/Jeffijoe.MessageFormat.Tests/TestHelpers/Benchmark.cs +++ b/src/Jeffijoe.MessageFormat.Tests/TestHelpers/Benchmark.cs @@ -8,51 +8,50 @@ using Xunit.Abstractions; -namespace Jeffijoe.MessageFormat.Tests.TestHelpers +namespace Jeffijoe.MessageFormat.Tests.TestHelpers; + +/// +/// Benchmark helper. +/// +public static class Benchmark { + #region Static Fields + + /// + /// The stopwatch. + /// + private static readonly Stopwatch Sw = new Stopwatch(); + + #endregion + + #region Public Methods and Operators + /// - /// Benchmark helper. + /// Ends the benchmark and prints the elapsed time to the console. /// - public static class Benchmark + /// + /// The output helper. + /// + public static void End(ITestOutputHelper outputHelper) { - #region Static Fields - - /// - /// The stopwatch. - /// - private static readonly Stopwatch Sw = new Stopwatch(); - - #endregion - - #region Public Methods and Operators - - /// - /// Ends the benchmark and prints the elapsed time to the console. - /// - /// - /// The output helper. - /// - public static void End(ITestOutputHelper outputHelper) - { - Sw.Stop(); - outputHelper.WriteLine("Result: {0}ms ({1} ticks)", Sw.ElapsedMilliseconds, Sw.ElapsedTicks); - } - - /// - /// Starts the benchmark, and writes the passed message to the output helper. - /// - /// - /// The message for console. - /// - /// - /// The output helper. - /// - public static void Start(string messageForConsole, ITestOutputHelper outputHelper) - { - outputHelper.WriteLine(messageForConsole); - Sw.Restart(); - } - - #endregion + Sw.Stop(); + outputHelper.WriteLine("Result: {0}ms ({1} ticks)", Sw.ElapsedMilliseconds, Sw.ElapsedTicks); } + + /// + /// Starts the benchmark, and writes the passed message to the output helper. + /// + /// + /// The message for console. + /// + /// + /// The output helper. + /// + public static void Start(string messageForConsole, ITestOutputHelper outputHelper) + { + outputHelper.WriteLine(messageForConsole); + Sw.Restart(); + } + + #endregion } \ No newline at end of file diff --git a/src/Jeffijoe.MessageFormat.Tests/TestHelpers/FakeFormatter.cs b/src/Jeffijoe.MessageFormat.Tests/TestHelpers/FakeFormatter.cs index 4dd5a81..0b97189 100644 --- a/src/Jeffijoe.MessageFormat.Tests/TestHelpers/FakeFormatter.cs +++ b/src/Jeffijoe.MessageFormat.Tests/TestHelpers/FakeFormatter.cs @@ -1,53 +1,52 @@ using System.Collections.Generic; using Jeffijoe.MessageFormat.Formatting; -namespace Jeffijoe.MessageFormat.Tests.TestHelpers +namespace Jeffijoe.MessageFormat.Tests.TestHelpers; + +/// +/// Fake formatter used for testing. +/// +internal class FakeFormatter : IFormatter { /// - /// Fake formatter used for testing. + /// What to return when is called. + /// + private readonly string formatResult; + + /// + /// Whether we should announce that we can format the input. + /// + private bool canFormat; + + /// + /// Initializes a new instance of the class. /// - internal class FakeFormatter : IFormatter + /// Whether to return true for . + /// The result to return. + public FakeFormatter(bool canFormat = false, string formatResult = "formatted") { - /// - /// What to return when is called. - /// - private readonly string formatResult; - - /// - /// Whether we should announce that we can format the input. - /// - private bool canFormat; - - /// - /// Initializes a new instance of the class. - /// - /// Whether to return true for . - /// The result to return. - public FakeFormatter(bool canFormat = false, string formatResult = "formatted") - { - this.canFormat = canFormat; - this.formatResult = formatResult; - } - - /// - public bool VariableMustExist => false; - - /// - public bool CanFormat(FormatterRequest request) => this.canFormat; - - /// - /// Sets the value of what returns. - /// - /// - public void SetCanFormat(bool value) => this.canFormat = value; - - /// - public string Format( - string locale, - FormatterRequest request, - IReadOnlyDictionary args, - object? value, - IMessageFormatter messageFormatter) => - formatResult; + this.canFormat = canFormat; + this.formatResult = formatResult; } + + /// + public bool VariableMustExist => false; + + /// + public bool CanFormat(FormatterRequest request) => this.canFormat; + + /// + /// Sets the value of what returns. + /// + /// + public void SetCanFormat(bool value) => this.canFormat = value; + + /// + public string Format( + string locale, + FormatterRequest request, + IReadOnlyDictionary args, + object? value, + IMessageFormatter messageFormatter) => + formatResult; } \ No newline at end of file diff --git a/src/Jeffijoe.MessageFormat.Tests/TestHelpers/FakeLiteralParser.cs b/src/Jeffijoe.MessageFormat.Tests/TestHelpers/FakeLiteralParser.cs index 255abae..841099b 100644 --- a/src/Jeffijoe.MessageFormat.Tests/TestHelpers/FakeLiteralParser.cs +++ b/src/Jeffijoe.MessageFormat.Tests/TestHelpers/FakeLiteralParser.cs @@ -2,40 +2,39 @@ using System.Text; using Jeffijoe.MessageFormat.Parsing; -namespace Jeffijoe.MessageFormat.Tests.TestHelpers +namespace Jeffijoe.MessageFormat.Tests.TestHelpers; + +/// +/// Fake literal parser. +/// +internal class FakeLiteralParser : ILiteralParser { /// - /// Fake literal parser. + /// The literal to return. /// - internal class FakeLiteralParser : ILiteralParser - { - /// - /// The literal to return. - /// - private readonly Literal literal; - - /// - /// Initializes a new instance of the class. - /// - /// - public FakeLiteralParser(Literal literal) - { - this.literal = literal; - } + private readonly Literal literal; - /// - public IEnumerable ParseLiterals(StringBuilder sb) - { - yield return literal; - } + /// + /// Initializes a new instance of the class. + /// + /// + public FakeLiteralParser(Literal literal) + { + this.literal = literal; + } - /// - /// Creates a fake literal parser that returns a single literal with - /// the specified inner text. - /// - /// - /// - public static ILiteralParser Of(string innerText) => - new FakeLiteralParser(new Literal(0, innerText.Length, 1, 1, innerText)); + /// + public IEnumerable ParseLiterals(StringBuilder sb) + { + yield return literal; } + + /// + /// Creates a fake literal parser that returns a single literal with + /// the specified inner text. + /// + /// + /// + public static ILiteralParser Of(string innerText) => + new FakeLiteralParser(new Literal(0, innerText.Length, 1, 1, innerText)); } \ No newline at end of file diff --git a/src/Jeffijoe.MessageFormat.Tests/TestHelpers/FakeMessageFormatter.cs b/src/Jeffijoe.MessageFormat.Tests/TestHelpers/FakeMessageFormatter.cs index 00a573d..59264a3 100644 --- a/src/Jeffijoe.MessageFormat.Tests/TestHelpers/FakeMessageFormatter.cs +++ b/src/Jeffijoe.MessageFormat.Tests/TestHelpers/FakeMessageFormatter.cs @@ -1,16 +1,15 @@ using System.Collections.Generic; -namespace Jeffijoe.MessageFormat.Tests.TestHelpers +namespace Jeffijoe.MessageFormat.Tests.TestHelpers; + +/// +/// Used for testing. Will just pass through the input pattern. +/// +internal class FakeMessageFormatter : IMessageFormatter { - /// - /// Used for testing. Will just pass through the input pattern. - /// - internal class FakeMessageFormatter : IMessageFormatter - { - public CustomValueFormatter? CustomValueFormatter { get; set; } + public CustomValueFormatter? CustomValueFormatter { get; set; } - public string FormatMessage(string pattern, IReadOnlyDictionary argsMap) => pattern; + public string FormatMessage(string pattern, IReadOnlyDictionary argsMap) => pattern; - public string FormatMessage(string pattern, object args) => pattern; - } + public string FormatMessage(string pattern, object args) => pattern; } \ No newline at end of file diff --git a/src/Jeffijoe.MessageFormat.Tests/TestHelpers/TrackingPatternParser.cs b/src/Jeffijoe.MessageFormat.Tests/TestHelpers/TrackingPatternParser.cs index c2d6be4..e1726d0 100644 --- a/src/Jeffijoe.MessageFormat.Tests/TestHelpers/TrackingPatternParser.cs +++ b/src/Jeffijoe.MessageFormat.Tests/TestHelpers/TrackingPatternParser.cs @@ -1,36 +1,35 @@ using System.Text; using Jeffijoe.MessageFormat.Parsing; -namespace Jeffijoe.MessageFormat.Tests.TestHelpers +namespace Jeffijoe.MessageFormat.Tests.TestHelpers; + +/// +/// Tracks the amount of times Parse is called. +/// +internal class TrackingPatternParser : IPatternParser { /// - /// Tracks the amount of times Parse is called. + /// The real parser. /// - internal class TrackingPatternParser : IPatternParser - { - /// - /// The real parser. - /// - private readonly PatternParser parser; + private readonly PatternParser parser; - /// - /// Initializes a new instance of the class. - /// - public TrackingPatternParser() - { - parser = new PatternParser(); - } + /// + /// Initializes a new instance of the class. + /// + public TrackingPatternParser() + { + parser = new PatternParser(); + } - /// - /// The amount of times Parse was called. - /// - public int ParseCount { get; private set; } + /// + /// The amount of times Parse was called. + /// + public int ParseCount { get; private set; } - /// - public IFormatterRequestCollection Parse(StringBuilder source) - { - ParseCount++; - return parser.Parse(source); - } + /// + public IFormatterRequestCollection Parse(StringBuilder source) + { + ParseCount++; + return parser.Parse(source); } } \ No newline at end of file diff --git a/src/Jeffijoe.MessageFormat/FormatterNotFoundException.cs b/src/Jeffijoe.MessageFormat/FormatterNotFoundException.cs index 6ee9904..b111776 100644 --- a/src/Jeffijoe.MessageFormat/FormatterNotFoundException.cs +++ b/src/Jeffijoe.MessageFormat/FormatterNotFoundException.cs @@ -7,50 +7,49 @@ using Jeffijoe.MessageFormat.Formatting; -namespace Jeffijoe.MessageFormat +namespace Jeffijoe.MessageFormat; + +/// +/// Thrown when a formatter could not be found for a specific request. +/// +public class FormatterNotFoundException : Exception { + #region Constructors and Destructors + + /// + /// Initializes a new instance of the class. + /// + /// + /// The request. + /// + public FormatterNotFoundException(FormatterRequest request) + : base(BuildMessage(request)) + { + } + + #endregion + + #region Methods + /// - /// Thrown when a formatter could not be found for a specific request. + /// Builds the message. /// - public class FormatterNotFoundException : Exception + /// + /// The request. + /// + /// + /// The . + /// + private static string BuildMessage(FormatterRequest request) { - #region Constructors and Destructors - - /// - /// Initializes a new instance of the class. - /// - /// - /// The request. - /// - public FormatterNotFoundException(FormatterRequest request) - : base(BuildMessage(request)) - { - } - - #endregion - - #region Methods - - /// - /// Builds the message. - /// - /// - /// The request. - /// - /// - /// The . - /// - private static string BuildMessage(FormatterRequest request) - { - return - string.Format( - "Format '{0}' could not be resolved.\r\n" + "Line {1}, position {2}\r\n" + "Source literal: '{3}'", - request.FormatterName, - request.SourceLiteral.SourceLineNumber, - request.SourceLiteral.SourceColumnNumber, - request.SourceLiteral.InnerText); - } - - #endregion + return + string.Format( + "Format '{0}' could not be resolved.\r\n" + "Line {1}, position {2}\r\n" + "Source literal: '{3}'", + request.FormatterName, + request.SourceLiteral.SourceLineNumber, + request.SourceLiteral.SourceColumnNumber, + request.SourceLiteral.InnerText); } + + #endregion } \ No newline at end of file diff --git a/src/Jeffijoe.MessageFormat/Formatting/BaseFormatter.cs b/src/Jeffijoe.MessageFormat/Formatting/BaseFormatter.cs index b90e830..1946de1 100644 --- a/src/Jeffijoe.MessageFormat/Formatting/BaseFormatter.cs +++ b/src/Jeffijoe.MessageFormat/Formatting/BaseFormatter.cs @@ -7,328 +7,327 @@ using System.Linq; using Jeffijoe.MessageFormat.Parsing; -namespace Jeffijoe.MessageFormat.Formatting +namespace Jeffijoe.MessageFormat.Formatting; + +/// +/// Base formatter with helpers for extracting data from the formatter request. +/// +public abstract class BaseFormatter { + #region Constants + + /// + /// The other. + /// + protected const string OtherKey = "other"; + + #endregion + + #region Methods + /// - /// Base formatter with helpers for extracting data from the formatter request. + /// Parses the arguments. /// - public abstract class BaseFormatter + /// + /// The request. + /// + /// + /// The . + /// + protected internal ParsedArguments ParseArguments(FormatterRequest request) { - #region Constants - - /// - /// The other. - /// - protected const string OtherKey = "other"; - - #endregion - - #region Methods - - /// - /// Parses the arguments. - /// - /// - /// The request. - /// - /// - /// The . - /// - protected internal ParsedArguments ParseArguments(FormatterRequest request) - { - int index; - var extensions = this.ParseExtensions(request, out index); - var keyedBlocks = this.ParseKeyedBlocks(request, index); - return new ParsedArguments(keyedBlocks, extensions); - } + int index; + var extensions = this.ParseExtensions(request, out index); + var keyedBlocks = this.ParseKeyedBlocks(request, index); + return new ParsedArguments(keyedBlocks, extensions); + } - /// - /// Parses the extensions. - /// - /// The request. - /// The index. - /// The formatter extensions. - protected internal IEnumerable ParseExtensions(FormatterRequest request, out int index) + /// + /// Parses the extensions. + /// + /// The request. + /// The index. + /// The formatter extensions. + protected internal IEnumerable ParseExtensions(FormatterRequest request, out int index) + { + var result = new List(); + if (request.FormatterArguments == null) { - var result = new List(); - if (request.FormatterArguments == null) - { - index = -1; - return Enumerable.Empty(); - } + index = -1; + return Enumerable.Empty(); + } - var length = request.FormatterArguments.Length; - index = 0; - const char Colon = ':'; - const char OpenBrace = '{'; - var foundExtension = false; - - var extension = StringBuilderPool.Get(); - var value = StringBuilderPool.Get(); - try + var length = request.FormatterArguments.Length; + index = 0; + const char Colon = ':'; + const char OpenBrace = '{'; + var foundExtension = false; + + var extension = StringBuilderPool.Get(); + var value = StringBuilderPool.Get(); + try + { + for (var i = 0; i < length; i++) { - for (var i = 0; i < length; i++) - { - var c = request.FormatterArguments[i]; + var c = request.FormatterArguments[i]; - // Whitespace is tolerated at the beginning. - var isWhiteSpace = char.IsWhiteSpace(c); - if (isWhiteSpace) + // Whitespace is tolerated at the beginning. + var isWhiteSpace = char.IsWhiteSpace(c); + if (isWhiteSpace) + { + // We've reached the end + if (value.Length > 0) { - // We've reached the end - if (value.Length > 0) - { - foundExtension = false; - result.Add(new FormatterExtension(extension.ToString(), value.ToString())); - extension.Clear(); - value.Clear(); - index = i; - continue; - } - - if (extension.Length > 0) - { - // It's not an extension, so we're done looking. - break; - } - + foundExtension = false; + result.Add(new FormatterExtension(extension.ToString(), value.ToString())); + extension.Clear(); + value.Clear(); + index = i; continue; } - if (c == Colon) + if (extension.Length > 0) { - foundExtension = true; - continue; + // It's not an extension, so we're done looking. + break; } - if (foundExtension) - { - value.Append(c); - continue; - } + continue; + } - if (c == OpenBrace) - { - // It's not an extension. - break; - } + if (c == Colon) + { + foundExtension = true; + continue; + } - extension.Append(c); + if (foundExtension) + { + value.Append(c); + continue; } - return result; - } - finally - { - StringBuilderPool.Return(extension); - StringBuilderPool.Return(value); + if (c == OpenBrace) + { + // It's not an extension. + break; + } + + extension.Append(c); } + + return result; } + finally + { + StringBuilderPool.Return(extension); + StringBuilderPool.Return(value); + } + } - /// - /// Parses the keyed blocks. - /// - /// - /// The request. - /// - /// - /// The start index. - /// - /// - /// The keyed blocks. - /// - protected internal IEnumerable ParseKeyedBlocks(FormatterRequest request, int startIndex) + /// + /// Parses the keyed blocks. + /// + /// + /// The request. + /// + /// + /// The start index. + /// + /// + /// The keyed blocks. + /// + protected internal IEnumerable ParseKeyedBlocks(FormatterRequest request, int startIndex) + { + const char OpenBrace = '{'; + const char CloseBrace = '}'; + const char EscapingChar = '\''; + + var result = new List(); + var braceBalance = 0; + var foundWhitespaceAfterKey = false; + var insideEscapeSequence = false; + if (request.FormatterArguments == null) { - const char OpenBrace = '{'; - const char CloseBrace = '}'; - const char EscapingChar = '\''; - - var result = new List(); - var braceBalance = 0; - var foundWhitespaceAfterKey = false; - var insideEscapeSequence = false; - if (request.FormatterArguments == null) - { - return Enumerable.Empty(); - } + return Enumerable.Empty(); + } - var key = StringBuilderPool.Get(); - var block = StringBuilderPool.Get(); + var key = StringBuilderPool.Get(); + var block = StringBuilderPool.Get(); - try + try + { + for (int i = startIndex; i < request.FormatterArguments.Length; i++) { - for (int i = startIndex; i < request.FormatterArguments.Length; i++) - { - var c = request.FormatterArguments[i]; - var isWhitespace = char.IsWhiteSpace(c); + var c = request.FormatterArguments[i]; + var isWhitespace = char.IsWhiteSpace(c); - if (c == EscapingChar) + if (c == EscapingChar) + { + if (braceBalance == 0) { - if (braceBalance == 0) - { - throw new MalformedLiteralException( - "Expected a key, but found start of a escape sequence.", - 0, - 0, - request.FormatterArguments); - } - - if (i == request.FormatterArguments.Length - 1) - { - if (!insideEscapeSequence) - block.Append(EscapingChar); - - // The last char can't open a new escape sequence, it can only close one - if (insideEscapeSequence) - { - insideEscapeSequence = false; - } - - continue; - } + throw new MalformedLiteralException( + "Expected a key, but found start of a escape sequence.", + 0, + 0, + request.FormatterArguments); + } - var nextChar = request.FormatterArguments[i + 1]; - if (nextChar == EscapingChar) - { - block.Append(EscapingChar); + if (i == request.FormatterArguments.Length - 1) + { + if (!insideEscapeSequence) block.Append(EscapingChar); - ++i; - continue; - } + // The last char can't open a new escape sequence, it can only close one if (insideEscapeSequence) { - block.Append(EscapingChar); insideEscapeSequence = false; - continue; } - if (nextChar == '{' || nextChar == '}' || nextChar == '#') - { - block.Append(EscapingChar); - block.Append(nextChar); - insideEscapeSequence = true; - ++i; - continue; - } + continue; + } + var nextChar = request.FormatterArguments[i + 1]; + if (nextChar == EscapingChar) + { block.Append(EscapingChar); + block.Append(EscapingChar); + ++i; continue; } if (insideEscapeSequence) { - block.Append(c); + block.Append(EscapingChar); + insideEscapeSequence = false; continue; } - if (c == OpenBrace) + if (nextChar == '{' || nextChar == '}' || nextChar == '#') { - if (key.Length == 0) - { - throw new MalformedLiteralException( - "Expected a key, but found start of a new block.", - 0, - 0, - request.FormatterArguments); - } - - braceBalance++; - if (braceBalance > 1) - { - block.Append(c); - } - + block.Append(EscapingChar); + block.Append(nextChar); + insideEscapeSequence = true; + ++i; continue; } - if (c == CloseBrace) - { - if (key.Length == 0) - { - throw new MalformedLiteralException( - "Expected a key, but found end of a block.", - 0, - 0, - request.FormatterArguments); - } + block.Append(EscapingChar); + continue; + } - if (braceBalance == 0) - { - throw new MalformedLiteralException( - "Found end of a block, but no block has been started, or the" - + " block has already been closed. " + - "This could indicate an unescaped brace somewhere.", - 0, - 0, - request.FormatterArguments); - } + if (insideEscapeSequence) + { + block.Append(c); + continue; + } - braceBalance--; - if (braceBalance == 0) - { - result.Add(new KeyedBlock(key.ToString(), block.ToString())); - block.Clear(); - key.Clear(); - foundWhitespaceAfterKey = false; - continue; - } + if (c == OpenBrace) + { + if (key.Length == 0) + { + throw new MalformedLiteralException( + "Expected a key, but found start of a new block.", + 0, + 0, + request.FormatterArguments); } - // If we are inside a block, append to the block buffer - if (braceBalance > 0) + braceBalance++; + if (braceBalance > 1) { block.Append(c); - continue; } - // Else, we are buffering our key - if (isWhitespace == false) + continue; + } + + if (c == CloseBrace) + { + if (key.Length == 0) { - if (foundWhitespaceAfterKey) - { - throw new MalformedLiteralException( - "Any whitespace after a key should be followed by the beginning of a block.", - 0, - 0, - request.FormatterArguments); - } + throw new MalformedLiteralException( + "Expected a key, but found end of a block.", + 0, + 0, + request.FormatterArguments); + } - key.Append(c); + if (braceBalance == 0) + { + throw new MalformedLiteralException( + "Found end of a block, but no block has been started, or the" + + " block has already been closed. " + + "This could indicate an unescaped brace somewhere.", + 0, + 0, + request.FormatterArguments); } - else if (key.Length > 0) + + braceBalance--; + if (braceBalance == 0) { - foundWhitespaceAfterKey = true; + result.Add(new KeyedBlock(key.ToString(), block.ToString())); + block.Clear(); + key.Clear(); + foundWhitespaceAfterKey = false; + continue; } } - if (insideEscapeSequence) + // If we are inside a block, append to the block buffer + if (braceBalance > 0) { - throw new MalformedLiteralException( - "There is an unclosed escape sequence.", - 0, - 0, - request.FormatterArguments); + block.Append(c); + continue; } - if (braceBalance > 0) + // Else, we are buffering our key + if (isWhitespace == false) + { + if (foundWhitespaceAfterKey) + { + throw new MalformedLiteralException( + "Any whitespace after a key should be followed by the beginning of a block.", + 0, + 0, + request.FormatterArguments); + } + + key.Append(c); + } + else if (key.Length > 0) { - throw new MalformedLiteralException( - "There are more open braces than there are close braces.", - 0, - 0, - request.FormatterArguments); + foundWhitespaceAfterKey = true; } + } - return result; + if (insideEscapeSequence) + { + throw new MalformedLiteralException( + "There is an unclosed escape sequence.", + 0, + 0, + request.FormatterArguments); } - finally + + if (braceBalance > 0) { - StringBuilderPool.Return(key); - StringBuilderPool.Return(block); + throw new MalformedLiteralException( + "There are more open braces than there are close braces.", + 0, + 0, + request.FormatterArguments); } - } - #endregion + return result; + } + finally + { + StringBuilderPool.Return(key); + StringBuilderPool.Return(block); + } } + + #endregion } \ No newline at end of file diff --git a/src/Jeffijoe.MessageFormat/Formatting/FormatterExtension.cs b/src/Jeffijoe.MessageFormat/Formatting/FormatterExtension.cs index fdc2d4e..abc3a07 100644 --- a/src/Jeffijoe.MessageFormat/Formatting/FormatterExtension.cs +++ b/src/Jeffijoe.MessageFormat/Formatting/FormatterExtension.cs @@ -2,51 +2,50 @@ // - FormatterExtension.cs // Author: Jeff Hansen // Copyright (C) Jeff Hansen 2014. All rights reserved. -namespace Jeffijoe.MessageFormat.Formatting +namespace Jeffijoe.MessageFormat.Formatting; + +/// +/// Contains extensions to be used by formatters. +/// Example, the offset extension for the Plural Format. +/// +public class FormatterExtension { + #region Constructors and Destructors + /// - /// Contains extensions to be used by formatters. - /// Example, the offset extension for the Plural Format. + /// Initializes a new instance of the class. /// - public class FormatterExtension + /// + /// The extension. + /// + /// + /// The value. + /// + public FormatterExtension(string extension, string value) { - #region Constructors and Destructors - - /// - /// Initializes a new instance of the class. - /// - /// - /// The extension. - /// - /// - /// The value. - /// - public FormatterExtension(string extension, string value) - { - this.Extension = extension; - this.Value = value; - } + this.Extension = extension; + this.Value = value; + } - #endregion + #endregion - #region Public Properties + #region Public Properties - /// - /// Gets the extension. - /// - /// - /// The extension. - /// - public string Extension { get; private set; } + /// + /// Gets the extension. + /// + /// + /// The extension. + /// + public string Extension { get; private set; } - /// - /// Gets the value. - /// - /// - /// The value. - /// - public string Value { get; private set; } + /// + /// Gets the value. + /// + /// + /// The value. + /// + public string Value { get; private set; } - #endregion - } + #endregion } \ No newline at end of file diff --git a/src/Jeffijoe.MessageFormat/Formatting/FormatterLibrary.cs b/src/Jeffijoe.MessageFormat/Formatting/FormatterLibrary.cs index b0eb9b8..b8543ba 100644 --- a/src/Jeffijoe.MessageFormat/Formatting/FormatterLibrary.cs +++ b/src/Jeffijoe.MessageFormat/Formatting/FormatterLibrary.cs @@ -6,55 +6,54 @@ using System.Collections.Generic; using Jeffijoe.MessageFormat.Formatting.Formatters; -namespace Jeffijoe.MessageFormat.Formatting +namespace Jeffijoe.MessageFormat.Formatting; + +/// +/// Manages formatters to use. +/// +public class FormatterLibrary : List, IFormatterLibrary { + #region Constructors and Destructors + /// - /// Manages formatters to use. + /// Initializes a new instance of the class, and adds the default formatters. /// - public class FormatterLibrary : List, IFormatterLibrary + public FormatterLibrary() { - #region Constructors and Destructors + this.Add(new VariableFormatter()); + this.Add(new SelectFormatter()); + this.Add(new PluralFormatter()); + this.Add(new NumberFormatter()); + this.Add(new DateFormatter()); + this.Add(new TimeFormatter()); + } - /// - /// Initializes a new instance of the class, and adds the default formatters. - /// - public FormatterLibrary() - { - this.Add(new VariableFormatter()); - this.Add(new SelectFormatter()); - this.Add(new PluralFormatter()); - this.Add(new NumberFormatter()); - this.Add(new DateFormatter()); - this.Add(new TimeFormatter()); - } + #endregion - #endregion - - #region Public Methods and Operators - - /// - /// Gets the formatter to use. If none was found, throws an exception. - /// - /// - /// The request. - /// - /// - /// The . - /// - /// - /// Thrown when the formatter was not found. - /// - public IFormatter GetFormatter(FormatterRequest request) - { - foreach (var formatter in this) - { - if (formatter.CanFormat(request)) - return formatter; - } + #region Public Methods and Operators - throw new FormatterNotFoundException(request); + /// + /// Gets the formatter to use. If none was found, throws an exception. + /// + /// + /// The request. + /// + /// + /// The . + /// + /// + /// Thrown when the formatter was not found. + /// + public IFormatter GetFormatter(FormatterRequest request) + { + foreach (var formatter in this) + { + if (formatter.CanFormat(request)) + return formatter; } - #endregion + throw new FormatterNotFoundException(request); } + + #endregion } \ No newline at end of file diff --git a/src/Jeffijoe.MessageFormat/Formatting/FormatterRequest.cs b/src/Jeffijoe.MessageFormat/Formatting/FormatterRequest.cs index 2a964df..001e3b1 100644 --- a/src/Jeffijoe.MessageFormat/Formatting/FormatterRequest.cs +++ b/src/Jeffijoe.MessageFormat/Formatting/FormatterRequest.cs @@ -5,93 +5,92 @@ using Jeffijoe.MessageFormat.Parsing; -namespace Jeffijoe.MessageFormat.Formatting +namespace Jeffijoe.MessageFormat.Formatting; + +/// +/// Formatter request. +/// +public class FormatterRequest { + #region Constructors and Destructors + /// - /// Formatter request. + /// Initializes a new instance of the class. /// - public class FormatterRequest + /// + /// The source literal. + /// + /// + /// The variable. + /// + /// + /// Name of the formatter. + /// + /// + /// The formatter arguments. + /// + public FormatterRequest(Literal sourceLiteral, string variable, string? formatterName, string? formatterArguments) { - #region Constructors and Destructors - - /// - /// Initializes a new instance of the class. - /// - /// - /// The source literal. - /// - /// - /// The variable. - /// - /// - /// Name of the formatter. - /// - /// - /// The formatter arguments. - /// - public FormatterRequest(Literal sourceLiteral, string variable, string? formatterName, string? formatterArguments) - { - this.SourceLiteral = sourceLiteral; - this.Variable = variable; - this.FormatterName = formatterName; - this.FormatterArguments = formatterArguments; - } - - #endregion + this.SourceLiteral = sourceLiteral; + this.Variable = variable; + this.FormatterName = formatterName; + this.FormatterArguments = formatterArguments; + } - #region Public Properties + #endregion - /// - /// Gets the formatter arguments that the formatter implementation will parse. Can be null. - /// - /// - /// The formatter arguments. - /// - public string? FormatterArguments { get; } + #region Public Properties - /// - /// Gets the name of the formatter to use . e.g. 'select', 'plural'. Can be null. - /// - /// - /// The name of the formatter. - /// - public string? FormatterName { get; } + /// + /// Gets the formatter arguments that the formatter implementation will parse. Can be null. + /// + /// + /// The formatter arguments. + /// + public string? FormatterArguments { get; } - /// - /// Gets the source literal. - /// - /// - /// The source literal. - /// - public Literal SourceLiteral { get; } + /// + /// Gets the name of the formatter to use . e.g. 'select', 'plural'. Can be null. + /// + /// + /// The name of the formatter. + /// + public string? FormatterName { get; } - /// - /// Gets the variable name. Never null. - /// - /// - /// The variable. - /// - public string Variable { get; } + /// + /// Gets the source literal. + /// + /// + /// The source literal. + /// + public Literal SourceLiteral { get; } - #endregion + /// + /// Gets the variable name. Never null. + /// + /// + /// The variable. + /// + public string Variable { get; } - #region Public Methods and Operators + #endregion - /// - /// Clones this instance. - /// - /// - /// The . - /// - public FormatterRequest Clone() - { - return new FormatterRequest( - this.SourceLiteral.Clone(), - this.Variable, - this.FormatterName, - this.FormatterArguments); - } + #region Public Methods and Operators - #endregion + /// + /// Clones this instance. + /// + /// + /// The . + /// + public FormatterRequest Clone() + { + return new FormatterRequest( + this.SourceLiteral.Clone(), + this.Variable, + this.FormatterName, + this.FormatterArguments); } + + #endregion } \ No newline at end of file diff --git a/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralContext.cs b/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralContext.cs index 55f8ec6..ca7a49c 100644 --- a/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralContext.cs +++ b/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralContext.cs @@ -1,15 +1,44 @@ using System; using System.Globalization; -namespace Jeffijoe.MessageFormat.Formatting.Formatters +namespace Jeffijoe.MessageFormat.Formatting.Formatters; + +internal readonly struct PluralContext { - internal readonly struct PluralContext + public PluralContext(int number) + { + Number = number; + N = Math.Abs(number); + I = number; + V = 0; + W = 0; + F = 0; + T = 0; + C = 0; + E = 0; + } + + public PluralContext(decimal number) : this(number.ToString(CultureInfo.InvariantCulture), (double) number) { - public PluralContext(int number) + } + + public PluralContext(double number) : this(number.ToString(CultureInfo.InvariantCulture), number) + { + } + + public PluralContext(string number) : this(number, double.Parse(number, CultureInfo.InvariantCulture)) + { + } + + private PluralContext(string number, double parsed) + { + Number = parsed; + N = Math.Abs(parsed); + I = (int) parsed; + + var dotIndex = number.IndexOf('.'); + if (dotIndex == -1) { - Number = number; - N = Math.Abs(number); - I = number; V = 0; W = 0; F = 0; @@ -17,70 +46,40 @@ public PluralContext(int number) C = 0; E = 0; } - - public PluralContext(decimal number) : this(number.ToString(CultureInfo.InvariantCulture), (double) number) + else { - } - - public PluralContext(double number) : this(number.ToString(CultureInfo.InvariantCulture), number) - { - } - - public PluralContext(string number) : this(number, double.Parse(number, CultureInfo.InvariantCulture)) - { - } - - private PluralContext(string number, double parsed) - { - Number = parsed; - N = Math.Abs(parsed); - I = (int) parsed; - - var dotIndex = number.IndexOf('.'); - if (dotIndex == -1) - { - V = 0; - W = 0; - F = 0; - T = 0; - C = 0; - E = 0; - } - else - { #if NET5_0_OR_GREATER var fractionSpan = number.AsSpan(dotIndex + 1, number.Length - dotIndex - 1); var fractionSpanWithoutZeroes = fractionSpan.TrimEnd('0'); #else - var fractionSpan = number.Substring(dotIndex + 1, number.Length - dotIndex - 1); - var fractionSpanWithoutZeroes = fractionSpan.TrimEnd('0'); + var fractionSpan = number.Substring(dotIndex + 1, number.Length - dotIndex - 1); + var fractionSpanWithoutZeroes = fractionSpan.TrimEnd('0'); #endif - V = fractionSpan.Length; - W = fractionSpanWithoutZeroes.Length; - F = int.Parse(fractionSpan); - T = int.Parse(fractionSpanWithoutZeroes); - C = 0; - E = 0; - } + V = fractionSpan.Length; + W = fractionSpanWithoutZeroes.Length; + F = int.Parse(fractionSpan); + T = int.Parse(fractionSpanWithoutZeroes); + C = 0; + E = 0; } + } - public double Number { get; } + public double Number { get; } - public double N { get; } + public double N { get; } - public int I { get; } + public int I { get; } - public int V { get; } + public int V { get; } - public int W { get; } + public int W { get; } - public int F { get; } + public int F { get; } - public int T { get; } + public int T { get; } - public int C { get; } + public int C { get; } - public int E { get; } - } + public int E { get; } } \ No newline at end of file diff --git a/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralFormatter.cs b/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralFormatter.cs index 1bb88fc..844f854 100644 --- a/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralFormatter.cs +++ b/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralFormatter.cs @@ -8,339 +8,338 @@ using System.Diagnostics.CodeAnalysis; using System.Linq; -namespace Jeffijoe.MessageFormat.Formatting.Formatters +namespace Jeffijoe.MessageFormat.Formatting.Formatters; + +/// +/// Plural Formatter +/// +public class PluralFormatter : BaseFormatter, IFormatter { + #region Constructors and Destructors + /// - /// Plural Formatter + /// Initializes a new instance of the class. /// - public class PluralFormatter : BaseFormatter, IFormatter + public PluralFormatter() { - #region Constructors and Destructors + this.Pluralizers = new Dictionary(); + this.AddStandardPluralizers(); + } - /// - /// Initializes a new instance of the class. - /// - public PluralFormatter() - { - this.Pluralizers = new Dictionary(); - this.AddStandardPluralizers(); - } + #endregion - #endregion + #region Public Properties - #region Public Properties + /// + /// This formatter requires the input variable to exist. + /// + public bool VariableMustExist => true; - /// - /// This formatter requires the input variable to exist. - /// - public bool VariableMustExist => true; + /// + /// Gets the pluralizers dictionary. Key is the locale. + /// + /// + /// The pluralizers. + /// + public IDictionary Pluralizers { get; private set; } - /// - /// Gets the pluralizers dictionary. Key is the locale. - /// - /// - /// The pluralizers. - /// - public IDictionary Pluralizers { get; private set; } + #endregion - #endregion + #region Public Methods and Operators - #region Public Methods and Operators + /// + /// Determines whether this instance can format a message based on the specified parameters. + /// + /// + /// The parameters. + /// + /// + /// The . + /// + public bool CanFormat(FormatterRequest request) + { + return request.FormatterName == "plural"; + } - /// - /// Determines whether this instance can format a message based on the specified parameters. - /// - /// - /// The parameters. - /// - /// - /// The . - /// - public bool CanFormat(FormatterRequest request) + /// + /// Using the specified parameters and arguments, a formatted string shall be returned. + /// The is being provided as well, to enable + /// nested formatting. This is only called if returns true. + /// The args will always contain the . + /// + /// + /// The locale being used. It is up to the formatter what they do with this information. + /// + /// + /// The parameters. + /// + /// + /// The arguments. + /// + /// The value of from the given args dictionary. Can be null. + /// + /// The message formatter. + /// + /// + /// The . + /// + public string Format(string locale, + FormatterRequest request, + IReadOnlyDictionary args, + object? value, + IMessageFormatter messageFormatter) + { + var arguments = this.ParseArguments(request); + double offset = 0; + var offsetExtension = arguments.Extensions.FirstOrDefault(x => x.Extension == "offset"); + if (offsetExtension != null) { - return request.FormatterName == "plural"; + offset = Convert.ToDouble(offsetExtension.Value); } - /// - /// Using the specified parameters and arguments, a formatted string shall be returned. - /// The is being provided as well, to enable - /// nested formatting. This is only called if returns true. - /// The args will always contain the . - /// - /// - /// The locale being used. It is up to the formatter what they do with this information. - /// - /// - /// The parameters. - /// - /// - /// The arguments. - /// - /// The value of from the given args dictionary. Can be null. - /// - /// The message formatter. - /// - /// - /// The . - /// - public string Format(string locale, - FormatterRequest request, - IReadOnlyDictionary args, - object? value, - IMessageFormatter messageFormatter) - { - var arguments = this.ParseArguments(request); - double offset = 0; - var offsetExtension = arguments.Extensions.FirstOrDefault(x => x.Extension == "offset"); - if (offsetExtension != null) - { - offset = Convert.ToDouble(offsetExtension.Value); - } + var ctx = CreatePluralContext(value, offset); + var pluralized = this.Pluralize(locale, arguments, ctx, offset); + var result = this.ReplaceNumberLiterals(pluralized, ctx.Number); + var formatted = messageFormatter.FormatMessage(result, args); + return formatted; + } - var ctx = CreatePluralContext(value, offset); - var pluralized = this.Pluralize(locale, arguments, ctx, offset); - var result = this.ReplaceNumberLiterals(pluralized, ctx.Number); - var formatted = messageFormatter.FormatMessage(result, args); - return formatted; - } + #endregion - #endregion - - #region Methods - - /// - /// Returns the correct plural block. - /// - /// - /// The locale. - /// - /// - /// The parsed arguments string. - /// - /// - /// The plural context. - /// - /// - /// The offset (already applied in context). - /// - /// - /// The . - /// - /// - /// The 'other' option was not found in pattern. - /// - [SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1126:PrefixCallsCorrectly", - Justification = "Reviewed. Suppression is OK here.")] - internal string Pluralize(string locale, ParsedArguments arguments, PluralContext context, double offset) + #region Methods + + /// + /// Returns the correct plural block. + /// + /// + /// The locale. + /// + /// + /// The parsed arguments string. + /// + /// + /// The plural context. + /// + /// + /// The offset (already applied in context). + /// + /// + /// The . + /// + /// + /// The 'other' option was not found in pattern. + /// + [SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1126:PrefixCallsCorrectly", + Justification = "Reviewed. Suppression is OK here.")] + internal string Pluralize(string locale, ParsedArguments arguments, PluralContext context, double offset) + { + string pluralForm; + if (this.Pluralizers.TryGetValue(locale, out var pluralizer)) { - string pluralForm; - if (this.Pluralizers.TryGetValue(locale, out var pluralizer)) - { - pluralForm = pluralizer(context.Number); - } - else if (PluralRulesMetadata.TryGetRuleByLocale(locale, out var contextPluralizer)) - { - pluralForm= contextPluralizer(context); - } - else - { - pluralForm = this.Pluralizers["en"](context.Number); - } + pluralForm = pluralizer(context.Number); + } + else if (PluralRulesMetadata.TryGetRuleByLocale(locale, out var contextPluralizer)) + { + pluralForm= contextPluralizer(context); + } + else + { + pluralForm = this.Pluralizers["en"](context.Number); + } - KeyedBlock? other = null; - foreach (var keyedBlock in arguments.KeyedBlocks) + KeyedBlock? other = null; + foreach (var keyedBlock in arguments.KeyedBlocks) + { + if (keyedBlock.Key == OtherKey) { - if (keyedBlock.Key == OtherKey) - { - other = keyedBlock; - } - - if (keyedBlock.Key.StartsWith("=")) - { - var numberLiteral = Convert.ToDouble(keyedBlock.Key.Substring(1)); + other = keyedBlock; + } - // ReSharper disable once CompareOfFloatsByEqualityOperator - if (numberLiteral == context.Number + offset) - { - return keyedBlock.BlockText; - } - } + if (keyedBlock.Key.StartsWith("=")) + { + var numberLiteral = Convert.ToDouble(keyedBlock.Key.Substring(1)); - if (keyedBlock.Key == pluralForm) + // ReSharper disable once CompareOfFloatsByEqualityOperator + if (numberLiteral == context.Number + offset) { return keyedBlock.BlockText; } } - if (other == null) + if (keyedBlock.Key == pluralForm) { - throw new MessageFormatterException("'other' option not found in pattern."); + return keyedBlock.BlockText; } - - return other.BlockText; } - /// - /// Replaces the number literals with the actual number. - /// - /// - /// The pluralized. - /// - /// - /// The n. - /// - /// - /// The . - /// - internal string ReplaceNumberLiterals(string pluralized, double n) + if (other == null) { - var sb = StringBuilderPool.Get(); - - try - { - // I've done this a few times now.. - const char OpenBrace = '{'; - const char CloseBrace = '}'; - const char Pound = '#'; - const char EscapeChar = '\''; - var braceBalance = 0; - var insideEscapeSequence = false; - for (int i = 0; i < pluralized.Length; i++) - { - var c = pluralized[i]; + throw new MessageFormatterException("'other' option not found in pattern."); + } - if (c == EscapeChar) - { - // Append it anyway because the escae - sb.Append(EscapeChar); + return other.BlockText; + } - if (i == pluralized.Length - 1) - { - // The last char can't open a new escape sequence, it can only close one - if (insideEscapeSequence) - { - insideEscapeSequence = false; - } + /// + /// Replaces the number literals with the actual number. + /// + /// + /// The pluralized. + /// + /// + /// The n. + /// + /// + /// The . + /// + internal string ReplaceNumberLiterals(string pluralized, double n) + { + var sb = StringBuilderPool.Get(); - continue; - } + try + { + // I've done this a few times now.. + const char OpenBrace = '{'; + const char CloseBrace = '}'; + const char Pound = '#'; + const char EscapeChar = '\''; + var braceBalance = 0; + var insideEscapeSequence = false; + for (int i = 0; i < pluralized.Length; i++) + { + var c = pluralized[i]; - var nextChar = pluralized[i + 1]; - if (nextChar == EscapeChar) - { - sb.Append(EscapeChar); - ++i; - continue; - } + if (c == EscapeChar) + { + // Append it anyway because the escae + sb.Append(EscapeChar); + if (i == pluralized.Length - 1) + { + // The last char can't open a new escape sequence, it can only close one if (insideEscapeSequence) { insideEscapeSequence = false; - continue; - } - - if (nextChar is '{' or '}' or '#') - { - sb.Append(nextChar); - insideEscapeSequence = true; - ++i; } continue; } - if (insideEscapeSequence) + var nextChar = pluralized[i + 1]; + if (nextChar == EscapeChar) { - sb.Append(c); + sb.Append(EscapeChar); + ++i; continue; } - if (c == OpenBrace) - { - braceBalance++; - } - else if (c == CloseBrace) + if (insideEscapeSequence) { - braceBalance--; + insideEscapeSequence = false; + continue; } - else if (c == Pound) + + if (nextChar is '{' or '}' or '#') { - if (braceBalance == 0) - { - sb.Append(n); - continue; - } + sb.Append(nextChar); + insideEscapeSequence = true; + ++i; } - sb.Append(c); + continue; } - return sb.ToString(); - } - finally - { - StringBuilderPool.Return(sb); - } - } + if (insideEscapeSequence) + { + sb.Append(c); + continue; + } - /// - /// Adds the standard pluralizers. - /// - private void AddStandardPluralizers() - { - this.Pluralizers.Add( - "en", - n => + if (c == OpenBrace) + { + braceBalance++; + } + else if (c == CloseBrace) { - // ReSharper disable CompareOfFloatsByEqualityOperator - if (n == 0) + braceBalance--; + } + else if (c == Pound) + { + if (braceBalance == 0) { - return "zero"; + sb.Append(n); + continue; } + } - if (n == 1) - { - return "one"; - } + sb.Append(c); + } - // ReSharper restore CompareOfFloatsByEqualityOperator - return "other"; - }); + return sb.ToString(); } - - /// - /// Creates a for the specified value. - /// - /// - /// - /// - [ExcludeFromCodeCoverage] - private static PluralContext CreatePluralContext(object? value, double offset) + finally { - if (offset == 0) + StringBuilderPool.Return(sb); + } + } + + /// + /// Adds the standard pluralizers. + /// + private void AddStandardPluralizers() + { + this.Pluralizers.Add( + "en", + n => { - if (value is string v) + // ReSharper disable CompareOfFloatsByEqualityOperator + if (n == 0) { - return new PluralContext(v); + return "zero"; } - if (value is int i) + if (n == 1) { - return new PluralContext(i); + return "one"; } - if (value is decimal d) - { - return new PluralContext(d); - } + // ReSharper restore CompareOfFloatsByEqualityOperator + return "other"; + }); + } + + /// + /// Creates a for the specified value. + /// + /// + /// + /// + [ExcludeFromCodeCoverage] + private static PluralContext CreatePluralContext(object? value, double offset) + { + if (offset == 0) + { + if (value is string v) + { + return new PluralContext(v); + } - return new PluralContext(Convert.ToDouble(value)); + if (value is int i) + { + return new PluralContext(i); } - return new PluralContext(Convert.ToDouble(value) - offset); + if (value is decimal d) + { + return new PluralContext(d); + } + + return new PluralContext(Convert.ToDouble(value)); } - #endregion + return new PluralContext(Convert.ToDouble(value) - offset); } + + #endregion } \ No newline at end of file diff --git a/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralRulesMetadata.cs b/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralRulesMetadata.cs index db8b44b..7c98e93 100644 --- a/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralRulesMetadata.cs +++ b/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralRulesMetadata.cs @@ -1,8 +1,7 @@ -namespace Jeffijoe.MessageFormat.Formatting.Formatters +namespace Jeffijoe.MessageFormat.Formatting.Formatters; + +[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] +internal static partial class PluralRulesMetadata { - [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] - internal static partial class PluralRulesMetadata - { - public static partial bool TryGetRuleByLocale(string locale, out ContextPluralizer contextPluralizer); - } -} + public static partial bool TryGetRuleByLocale(string locale, out ContextPluralizer contextPluralizer); +} \ No newline at end of file diff --git a/src/Jeffijoe.MessageFormat/Formatting/Formatters/Pluralizer.cs b/src/Jeffijoe.MessageFormat/Formatting/Formatters/Pluralizer.cs index c034f36..f371439 100644 --- a/src/Jeffijoe.MessageFormat/Formatting/Formatters/Pluralizer.cs +++ b/src/Jeffijoe.MessageFormat/Formatting/Formatters/Pluralizer.cs @@ -2,19 +2,18 @@ // - Pluralizer.cs // Author: Jeff Hansen // Copyright (C) Jeff Hansen 2014. All rights reserved. -namespace Jeffijoe.MessageFormat.Formatting.Formatters -{ - /// - /// Given the specified number, determines what plural form is being used. - /// - /// The number used to determine the pluralization rule.. - /// The plural form to use. - public delegate string Pluralizer(double n); +namespace Jeffijoe.MessageFormat.Formatting.Formatters; - /// - /// Given the specified number context, determines what plural form is being used. - /// - /// The context of the number used to determine the pluralization rule.. - /// The plural form to use. - internal delegate string ContextPluralizer(PluralContext context); -} \ No newline at end of file +/// +/// Given the specified number, determines what plural form is being used. +/// +/// The number used to determine the pluralization rule.. +/// The plural form to use. +public delegate string Pluralizer(double n); + +/// +/// Given the specified number context, determines what plural form is being used. +/// +/// The context of the number used to determine the pluralization rule.. +/// The plural form to use. +internal delegate string ContextPluralizer(PluralContext context); \ No newline at end of file diff --git a/src/Jeffijoe.MessageFormat/Formatting/Formatters/SelectFormatter.cs b/src/Jeffijoe.MessageFormat/Formatting/Formatters/SelectFormatter.cs index 3c5c6f7..f90cace 100644 --- a/src/Jeffijoe.MessageFormat/Formatting/Formatters/SelectFormatter.cs +++ b/src/Jeffijoe.MessageFormat/Formatting/Formatters/SelectFormatter.cs @@ -7,89 +7,88 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -namespace Jeffijoe.MessageFormat.Formatting.Formatters +namespace Jeffijoe.MessageFormat.Formatting.Formatters; + +/// +/// Implementation of the SelectFormat. +/// +public class SelectFormatter : BaseFormatter, IFormatter { + #region Public Properties + /// - /// Implementation of the SelectFormat. + /// This formatter requires the input variable to exist. /// - public class SelectFormatter : BaseFormatter, IFormatter - { - #region Public Properties - - /// - /// This formatter requires the input variable to exist. - /// - [ExcludeFromCodeCoverage] - public bool VariableMustExist => true; + [ExcludeFromCodeCoverage] + public bool VariableMustExist => true; - #endregion + #endregion - #region Public Methods and Operators + #region Public Methods and Operators - /// - /// Determines whether this instance can format a message based on the specified parameters. - /// - /// - /// The parameters. - /// - /// - /// The . - /// - public bool CanFormat(FormatterRequest request) - { - return request.FormatterName == "select"; - } + /// + /// Determines whether this instance can format a message based on the specified parameters. + /// + /// + /// The parameters. + /// + /// + /// The . + /// + public bool CanFormat(FormatterRequest request) + { + return request.FormatterName == "select"; + } - /// - /// Using the specified parameters and arguments, a formatted string shall be returned. - /// The is being provided as well, to enable - /// nested formatting. This is only called if returns true. - /// The args will always contain the . - /// - /// The locale being used. It is up to the formatter what they do with this information. - /// The parameters. - /// The arguments. - /// The value of from the given args dictionary. Can be null. - /// The message formatter. - /// - /// The . - /// - /// 'other' option not found in pattern, and variable was not present in collection. - [SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1126:PrefixCallsCorrectly", - Justification = "Reviewed. Suppression is OK here.")] - public string Format(string locale, - FormatterRequest request, - IReadOnlyDictionary args, - object? value, - IMessageFormatter messageFormatter) + /// + /// Using the specified parameters and arguments, a formatted string shall be returned. + /// The is being provided as well, to enable + /// nested formatting. This is only called if returns true. + /// The args will always contain the . + /// + /// The locale being used. It is up to the formatter what they do with this information. + /// The parameters. + /// The arguments. + /// The value of from the given args dictionary. Can be null. + /// The message formatter. + /// + /// The . + /// + /// 'other' option not found in pattern, and variable was not present in collection. + [SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1126:PrefixCallsCorrectly", + Justification = "Reviewed. Suppression is OK here.")] + public string Format(string locale, + FormatterRequest request, + IReadOnlyDictionary args, + object? value, + IMessageFormatter messageFormatter) + { + var str = Convert.ToString(value); + var parsed = this.ParseArguments(request); + KeyedBlock? other = null; + foreach (var keyedBlock in parsed.KeyedBlocks) { - var str = Convert.ToString(value); - var parsed = this.ParseArguments(request); - KeyedBlock? other = null; - foreach (var keyedBlock in parsed.KeyedBlocks) + if (str == keyedBlock.Key) { - if (str == keyedBlock.Key) - { - return messageFormatter.FormatMessage(keyedBlock.BlockText, args); - } - - if (keyedBlock.Key == OtherKey) - { - other = keyedBlock; - } + return messageFormatter.FormatMessage(keyedBlock.BlockText, args); } - if (other == null) + if (keyedBlock.Key == OtherKey) { - throw new MessageFormatterException( - "'other' option not found in pattern, and variable was not present in collection."); + other = keyedBlock; } + } - return messageFormatter.FormatMessage(other.BlockText, args); + if (other == null) + { + throw new MessageFormatterException( + "'other' option not found in pattern, and variable was not present in collection."); } - #endregion + return messageFormatter.FormatMessage(other.BlockText, args); } + + #endregion } \ No newline at end of file diff --git a/src/Jeffijoe.MessageFormat/Formatting/Formatters/VariableFormatter.cs b/src/Jeffijoe.MessageFormat/Formatting/Formatters/VariableFormatter.cs index bf8231b..ea58b26 100644 --- a/src/Jeffijoe.MessageFormat/Formatting/Formatters/VariableFormatter.cs +++ b/src/Jeffijoe.MessageFormat/Formatting/Formatters/VariableFormatter.cs @@ -8,89 +8,88 @@ using System.Collections.Generic; using System.Globalization; -namespace Jeffijoe.MessageFormat.Formatting.Formatters +namespace Jeffijoe.MessageFormat.Formatting.Formatters; + +/// +/// Simple variable replacer. +/// +public class VariableFormatter : IFormatter { - /// - /// Simple variable replacer. - /// - public class VariableFormatter : IFormatter - { - #region Fields + #region Fields - private readonly ConcurrentDictionary cultures = new ConcurrentDictionary(); + private readonly ConcurrentDictionary cultures = new ConcurrentDictionary(); - #endregion + #endregion - #region Public Properties + #region Public Properties - /// - /// This formatter requires the input variable to exist. - /// - public bool VariableMustExist => true; + /// + /// This formatter requires the input variable to exist. + /// + public bool VariableMustExist => true; - #endregion + #endregion - #region Public Methods and Operators + #region Public Methods and Operators - /// - /// Determines whether this instance can format a message based on the specified parameters. - /// - /// - /// The parameters. - /// - /// - /// The . - /// - public bool CanFormat(FormatterRequest request) - { - return request.FormatterName == null; - } + /// + /// Determines whether this instance can format a message based on the specified parameters. + /// + /// + /// The parameters. + /// + /// + /// The . + /// + public bool CanFormat(FormatterRequest request) + { + return request.FormatterName == null; + } - /// - /// Using the specified parameters and arguments, a formatted string shall be returned. - /// The is being provided as well, to enable - /// nested formatting. This is only called if returns true. - /// The args will always contain the . - /// - /// The locale being used. It is up to the formatter what they do with this information. - /// The parameters. - /// The arguments. - /// The value of from the given args dictionary. Can be null. - /// The message formatter. - /// - /// The . - /// - public string Format(string locale, - FormatterRequest request, - IReadOnlyDictionary args, - object? value, - IMessageFormatter messageFormatter) + /// + /// Using the specified parameters and arguments, a formatted string shall be returned. + /// The is being provided as well, to enable + /// nested formatting. This is only called if returns true. + /// The args will always contain the . + /// + /// The locale being used. It is up to the formatter what they do with this information. + /// The parameters. + /// The arguments. + /// The value of from the given args dictionary. Can be null. + /// The message formatter. + /// + /// The . + /// + public string Format(string locale, + FormatterRequest request, + IReadOnlyDictionary args, + object? value, + IMessageFormatter messageFormatter) + { + switch (value) { - switch (value) - { - case IFormattable formattable: - return formattable.ToString(null, GetCultureInfo(locale)); - default: - return value?.ToString() ?? string.Empty; - } + case IFormattable formattable: + return formattable.ToString(null, GetCultureInfo(locale)); + default: + return value?.ToString() ?? string.Empty; } + } - /// - /// Get and cache the culture for a locale. - /// - /// Locale for which to get the culture. - /// - /// Culture of locale. - /// - private CultureInfo GetCultureInfo(string locale) + /// + /// Get and cache the culture for a locale. + /// + /// Locale for which to get the culture. + /// + /// Culture of locale. + /// + private CultureInfo GetCultureInfo(string locale) + { + if (!this.cultures.ContainsKey(locale)) { - if (!this.cultures.ContainsKey(locale)) - { - this.cultures[locale] = new CultureInfo(locale); - } - return this.cultures[locale]; + this.cultures[locale] = new CultureInfo(locale); } - - #endregion + return this.cultures[locale]; } + + #endregion } \ No newline at end of file diff --git a/src/Jeffijoe.MessageFormat/Formatting/IFormatter.cs b/src/Jeffijoe.MessageFormat/Formatting/IFormatter.cs index 7ea2976..abc8f2b 100644 --- a/src/Jeffijoe.MessageFormat/Formatting/IFormatter.cs +++ b/src/Jeffijoe.MessageFormat/Formatting/IFormatter.cs @@ -5,57 +5,56 @@ using System.Collections.Generic; -namespace Jeffijoe.MessageFormat.Formatting +namespace Jeffijoe.MessageFormat.Formatting; + +/// +/// A Formatter is what transforms a pattern into a string, using the proper arguments. +/// +public interface IFormatter { + #region Public Properties + + /// + /// Each Formatter must declare whether or not an input variable is required to exist. + /// Most of the time that is the case. + /// + bool VariableMustExist { get; } + + #endregion + + #region Public Methods and Operators + + /// + /// Determines whether this instance can format a message based on the specified parameters. + /// + /// + /// The parameters. + /// + /// + /// The . + /// + bool CanFormat(FormatterRequest request); + /// - /// A Formatter is what transforms a pattern into a string, using the proper arguments. + /// Using the specified parameters and arguments, a formatted string shall be returned. + /// The is being provided as well, to enable + /// nested formatting. This is only called if returns true. + /// The args will always contain the . /// - public interface IFormatter - { - #region Public Properties - - /// - /// Each Formatter must declare whether or not an input variable is required to exist. - /// Most of the time that is the case. - /// - bool VariableMustExist { get; } - - #endregion - - #region Public Methods and Operators - - /// - /// Determines whether this instance can format a message based on the specified parameters. - /// - /// - /// The parameters. - /// - /// - /// The . - /// - bool CanFormat(FormatterRequest request); - - /// - /// Using the specified parameters and arguments, a formatted string shall be returned. - /// The is being provided as well, to enable - /// nested formatting. This is only called if returns true. - /// The args will always contain the . - /// - /// The locale being used. It is up to the formatter what they do with this information. - /// The parameters. - /// The arguments. - /// The value of from the given args dictionary. Can be null. - /// The message formatter. - /// - /// The . - /// - string Format( - string locale, - FormatterRequest request, - IReadOnlyDictionary args, - object? value, - IMessageFormatter messageFormatter); - - #endregion - } + /// The locale being used. It is up to the formatter what they do with this information. + /// The parameters. + /// The arguments. + /// The value of from the given args dictionary. Can be null. + /// The message formatter. + /// + /// The . + /// + string Format( + string locale, + FormatterRequest request, + IReadOnlyDictionary args, + object? value, + IMessageFormatter messageFormatter); + + #endregion } \ No newline at end of file diff --git a/src/Jeffijoe.MessageFormat/Formatting/IFormatterLibrary.cs b/src/Jeffijoe.MessageFormat/Formatting/IFormatterLibrary.cs index 82ee28c..94d22b0 100644 --- a/src/Jeffijoe.MessageFormat/Formatting/IFormatterLibrary.cs +++ b/src/Jeffijoe.MessageFormat/Formatting/IFormatterLibrary.cs @@ -5,26 +5,25 @@ using System.Collections.Generic; -namespace Jeffijoe.MessageFormat.Formatting +namespace Jeffijoe.MessageFormat.Formatting; + +/// +/// Manages formatters to use. +/// +public interface IFormatterLibrary : IList { + #region Public Methods and Operators + /// - /// Manages formatters to use. + /// Gets the formatter to use. If none was found, throws an exception. /// - public interface IFormatterLibrary : IList - { - #region Public Methods and Operators - - /// - /// Gets the formatter to use. If none was found, throws an exception. - /// - /// - /// The request. - /// - /// - /// The . - /// - IFormatter GetFormatter(FormatterRequest request); + /// + /// The request. + /// + /// + /// The . + /// + IFormatter GetFormatter(FormatterRequest request); - #endregion - } + #endregion } \ No newline at end of file diff --git a/src/Jeffijoe.MessageFormat/Formatting/KeyedBlock.cs b/src/Jeffijoe.MessageFormat/Formatting/KeyedBlock.cs index e239d87..f56e6d2 100644 --- a/src/Jeffijoe.MessageFormat/Formatting/KeyedBlock.cs +++ b/src/Jeffijoe.MessageFormat/Formatting/KeyedBlock.cs @@ -2,52 +2,51 @@ // - KeyedBlock.cs // Author: Jeff Hansen // Copyright (C) Jeff Hansen 2014. All rights reserved. -namespace Jeffijoe.MessageFormat.Formatting +namespace Jeffijoe.MessageFormat.Formatting; + +/// +/// A keyed block contains a key and a block +/// containing the text that the formatter will return +/// when the block is being used. +/// +public class KeyedBlock { + #region Constructors and Destructors + /// - /// A keyed block contains a key and a block - /// containing the text that the formatter will return - /// when the block is being used. + /// Initializes a new instance of the class. /// - public class KeyedBlock + /// + /// The key. + /// + /// + /// The block text. + /// + public KeyedBlock(string key, string blockText) { - #region Constructors and Destructors - - /// - /// Initializes a new instance of the class. - /// - /// - /// The key. - /// - /// - /// The block text. - /// - public KeyedBlock(string key, string blockText) - { - this.Key = key; - this.BlockText = blockText; - } + this.Key = key; + this.BlockText = blockText; + } - #endregion + #endregion - #region Public Properties + #region Public Properties - /// - /// Gets the block text to be returned by the formatter. - /// - /// - /// The block text. - /// - public string BlockText { get; private set; } + /// + /// Gets the block text to be returned by the formatter. + /// + /// + /// The block text. + /// + public string BlockText { get; private set; } - /// - /// Gets the key used by the formatter to make decisions. - /// - /// - /// The key. - /// - public string Key { get; private set; } + /// + /// Gets the key used by the formatter to make decisions. + /// + /// + /// The key. + /// + public string Key { get; private set; } - #endregion - } + #endregion } \ No newline at end of file diff --git a/src/Jeffijoe.MessageFormat/Formatting/ParsedArguments.cs b/src/Jeffijoe.MessageFormat/Formatting/ParsedArguments.cs index 2fa1b3a..0fee45d 100644 --- a/src/Jeffijoe.MessageFormat/Formatting/ParsedArguments.cs +++ b/src/Jeffijoe.MessageFormat/Formatting/ParsedArguments.cs @@ -6,50 +6,49 @@ using System.Collections.Generic; using System.Linq; -namespace Jeffijoe.MessageFormat.Formatting +namespace Jeffijoe.MessageFormat.Formatting; + +/// +/// Container class for formatter argument parsing result. +/// +public class ParsedArguments { + #region Constructors and Destructors + /// - /// Container class for formatter argument parsing result. + /// Initializes a new instance of the class. /// - public class ParsedArguments + /// + /// The keyed Blocks. + /// + /// + /// The extensions. + /// + public ParsedArguments(IEnumerable keyedBlocks, IEnumerable extensions) { - #region Constructors and Destructors - - /// - /// Initializes a new instance of the class. - /// - /// - /// The keyed Blocks. - /// - /// - /// The extensions. - /// - public ParsedArguments(IEnumerable keyedBlocks, IEnumerable extensions) - { - this.KeyedBlocks = keyedBlocks.ToList(); - this.Extensions = extensions.ToList(); - } - - #endregion - - #region Public Properties - - /// - /// Gets the extensions. - /// - /// - /// The extensions. - /// - public IEnumerable Extensions { get; private set; } - - /// - /// Gets the keyed blocks. - /// - /// - /// The keyed blocks. - /// - public IEnumerable KeyedBlocks { get; private set; } - - #endregion + this.KeyedBlocks = keyedBlocks.ToList(); + this.Extensions = extensions.ToList(); } + + #endregion + + #region Public Properties + + /// + /// Gets the extensions. + /// + /// + /// The extensions. + /// + public IEnumerable Extensions { get; private set; } + + /// + /// Gets the keyed blocks. + /// + /// + /// The keyed blocks. + /// + public IEnumerable KeyedBlocks { get; private set; } + + #endregion } \ No newline at end of file diff --git a/src/Jeffijoe.MessageFormat/Formatting/VariableNotFoundException.cs b/src/Jeffijoe.MessageFormat/Formatting/VariableNotFoundException.cs index 10b66c8..a68b210 100644 --- a/src/Jeffijoe.MessageFormat/Formatting/VariableNotFoundException.cs +++ b/src/Jeffijoe.MessageFormat/Formatting/VariableNotFoundException.cs @@ -3,57 +3,56 @@ // // Author: Jeff Hansen // Copyright (C) Jeff Hansen 2015. All rights reserved. -namespace Jeffijoe.MessageFormat.Formatting +namespace Jeffijoe.MessageFormat.Formatting; + +/// +/// Thrown when a variable declared in a pattern was non-existent. +/// +public class VariableNotFoundException : MessageFormatterException { + #region Constructors and Destructors + /// - /// Thrown when a variable declared in a pattern was non-existent. + /// Initializes a new instance of the class. /// - public class VariableNotFoundException : MessageFormatterException + /// + /// The variable. + /// + public VariableNotFoundException(string missingVariable) + : base(BuildMessage(missingVariable)) { - #region Constructors and Destructors - - /// - /// Initializes a new instance of the class. - /// - /// - /// The variable. - /// - public VariableNotFoundException(string missingVariable) - : base(BuildMessage(missingVariable)) - { - this.MissingVariable = missingVariable; - } - - #endregion - - #region Public Properties - - /// - /// Gets the name of the missing variable. - /// - /// - /// The missing variable. - /// - public string MissingVariable { get; private set; } - - #endregion - - #region Methods - - /// - /// Builds the message. - /// - /// - /// The variable. - /// - /// - /// The . - /// - private static string BuildMessage(string variable) - { - return string.Format("The variable '{0}' was not found in the arguments collection.", variable); - } - - #endregion + this.MissingVariable = missingVariable; } + + #endregion + + #region Public Properties + + /// + /// Gets the name of the missing variable. + /// + /// + /// The missing variable. + /// + public string MissingVariable { get; private set; } + + #endregion + + #region Methods + + /// + /// Builds the message. + /// + /// + /// The variable. + /// + /// + /// The . + /// + private static string BuildMessage(string variable) + { + return string.Format("The variable '{0}' was not found in the arguments collection.", variable); + } + + #endregion } \ No newline at end of file diff --git a/src/Jeffijoe.MessageFormat/Helpers/CharHelper.cs b/src/Jeffijoe.MessageFormat/Helpers/CharHelper.cs index cc6d8e3..6532324 100644 --- a/src/Jeffijoe.MessageFormat/Helpers/CharHelper.cs +++ b/src/Jeffijoe.MessageFormat/Helpers/CharHelper.cs @@ -2,47 +2,46 @@ // - CharHelper.cs // Author: Jeff Hansen // Copyright (C) Jeff Hansen 2014. All rights reserved. -namespace Jeffijoe.MessageFormat.Helpers +namespace Jeffijoe.MessageFormat.Helpers; + +/// +/// Char helper +/// +internal static class CharHelper { + #region Static Fields + /// - /// Char helper + /// The alphanumberic. /// - internal static class CharHelper - { - #region Static Fields + private static readonly char[] Alphanumberic = + "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_".ToCharArray(); - /// - /// The alphanumberic. - /// - private static readonly char[] Alphanumberic = - "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_".ToCharArray(); + #endregion - #endregion + #region Methods - #region Methods - - /// - /// Determines whether the specified character is alpha numeric. - /// - /// - /// The c. - /// - /// - /// The . - /// - internal static bool IsAlphaNumeric(this char c) + /// + /// Determines whether the specified character is alpha numeric. + /// + /// + /// The c. + /// + /// + /// The . + /// + internal static bool IsAlphaNumeric(this char c) + { + foreach (var chr in Alphanumberic) { - foreach (var chr in Alphanumberic) + if (chr == c) { - if (chr == c) - { - return true; - } + return true; } - - return false; } - #endregion + return false; } + + #endregion } \ No newline at end of file diff --git a/src/Jeffijoe.MessageFormat/Helpers/ObjectHelper.cs b/src/Jeffijoe.MessageFormat/Helpers/ObjectHelper.cs index 5e5af0f..3fd6b5c 100644 --- a/src/Jeffijoe.MessageFormat/Helpers/ObjectHelper.cs +++ b/src/Jeffijoe.MessageFormat/Helpers/ObjectHelper.cs @@ -8,66 +8,65 @@ using System.Linq; using System.Reflection; -namespace Jeffijoe.MessageFormat.Helpers +namespace Jeffijoe.MessageFormat.Helpers; + +/// +/// Object helper +/// +internal static class ObjectHelper { + #region Methods + /// - /// Object helper + /// Gets the properties from the specified object. /// - internal static class ObjectHelper + /// + /// The object. + /// + /// + /// The . + /// + internal static IEnumerable GetProperties(object obj) { - #region Methods - - /// - /// Gets the properties from the specified object. - /// - /// - /// The object. - /// - /// - /// The . - /// - internal static IEnumerable GetProperties(object obj) + var properties = new List(); + var type = obj.GetType(); + var typeInfo = type.GetTypeInfo(); + while (true) { - var properties = new List(); - var type = obj.GetType(); - var typeInfo = type.GetTypeInfo(); - while (true) + properties.AddRange(typeInfo.DeclaredProperties); + if (typeInfo.BaseType == null) { - properties.AddRange(typeInfo.DeclaredProperties); - if (typeInfo.BaseType == null) - { - break; - } - - typeInfo = typeInfo.BaseType.GetTypeInfo(); + break; } - return properties; + typeInfo = typeInfo.BaseType.GetTypeInfo(); } - /// - /// Creates a dictionary from the specified object's properties. 1 level only. - /// - /// - /// The object. - /// - /// - /// The . - /// - internal static Dictionary ToDictionary(this object obj) - { - // We want to be able to read the property, and it should not be an indexer. - var properties = GetProperties(obj).Where(x => x.CanRead && x.GetIndexParameters().Any() == false); + return properties; + } - var result = new Dictionary(); - foreach (var propertyInfo in properties) - { - result[propertyInfo.Name] = propertyInfo.GetValue(obj); - } + /// + /// Creates a dictionary from the specified object's properties. 1 level only. + /// + /// + /// The object. + /// + /// + /// The . + /// + internal static Dictionary ToDictionary(this object obj) + { + // We want to be able to read the property, and it should not be an indexer. + var properties = GetProperties(obj).Where(x => x.CanRead && x.GetIndexParameters().Any() == false); - return result; + var result = new Dictionary(); + foreach (var propertyInfo in properties) + { + result[propertyInfo.Name] = propertyInfo.GetValue(obj); } - #endregion + return result; } + + #endregion } \ No newline at end of file diff --git a/src/Jeffijoe.MessageFormat/Helpers/StringBuilderHelper.cs b/src/Jeffijoe.MessageFormat/Helpers/StringBuilderHelper.cs index 285c2de..e920140 100644 --- a/src/Jeffijoe.MessageFormat/Helpers/StringBuilderHelper.cs +++ b/src/Jeffijoe.MessageFormat/Helpers/StringBuilderHelper.cs @@ -9,37 +9,37 @@ using System.Text; -namespace Jeffijoe.MessageFormat.Helpers +namespace Jeffijoe.MessageFormat.Helpers; + +/// +/// String Builder helper +/// +internal static class StringBuilderHelper { + #region Methods + /// - /// String Builder helper + /// Determines whether the specified source contains any of the specified characters. /// - internal static class StringBuilderHelper + /// + /// The source. + /// + /// + /// The chars. + /// + /// + /// The . + /// + private static bool Contains(this StringBuilder src, params char[] chars) { - #region Methods - - /// - /// Determines whether the specified source contains any of the specified characters. - /// - /// - /// The source. - /// - /// - /// The chars. - /// - /// - /// The . - /// - private static bool Contains(this StringBuilder src, params char[] chars) - { #if NET5_0_OR_GREATER - foreach (var chunk in src.GetChunks()) + foreach (var chunk in src.GetChunks()) + { + if (chunk.Span.IndexOfAny(chars) != -1) { - if (chunk.Span.IndexOfAny(chars) != -1) - { - return true; - } + return true; } + } #else for (int i = 0; i < src.Length; i++) { @@ -53,29 +53,29 @@ private static bool Contains(this StringBuilder src, params char[] chars) } #endif - return false; - } + return false; + } - /// - /// Determines whether the specified source contains the specified character. - /// - /// - /// The source. - /// - /// - /// The character. - /// - /// - /// The . - /// - internal static bool Contains(this StringBuilder src, char character) - { + /// + /// Determines whether the specified source contains the specified character. + /// + /// + /// The source. + /// + /// + /// The character. + /// + /// + /// The . + /// + internal static bool Contains(this StringBuilder src, char character) + { #if NET5_0_OR_GREATER - foreach (var chunk in src.GetChunks()) - { - if (chunk.Span.IndexOf(character) != -1) - return true; - } + foreach (var chunk in src.GetChunks()) + { + if (chunk.Span.IndexOf(character) != -1) + return true; + } #else for (int i = 0; i < src.Length; i++) { @@ -86,72 +86,71 @@ internal static bool Contains(this StringBuilder src, char character) } #endif - return false; - } - - /// - /// Determines whether the specified source contains whitespace. - /// - /// - /// The source. - /// - /// - /// The . - /// - internal static bool ContainsWhitespace(this StringBuilder src) - { - return src.Contains(' ', '\r', '\n', '\t'); - } + return false; + } - /// - /// Trims the whitespace. - /// - /// - /// The source. - /// - /// - /// The . - /// - internal static StringBuilder TrimWhitespace(this StringBuilder src) - { - var length = 0; + /// + /// Determines whether the specified source contains whitespace. + /// + /// + /// The source. + /// + /// + /// The . + /// + internal static bool ContainsWhitespace(this StringBuilder src) + { + return src.Contains(' ', '\r', '\n', '\t'); + } - for (int i = 0; i < src.Length; i++) - { - var c = src[i]; - if (char.IsWhiteSpace(c) == false) - { - length = i; - break; - } - } + /// + /// Trims the whitespace. + /// + /// + /// The source. + /// + /// + /// The . + /// + internal static StringBuilder TrimWhitespace(this StringBuilder src) + { + var length = 0; - if (length != 0) + for (int i = 0; i < src.Length; i++) + { + var c = src[i]; + if (char.IsWhiteSpace(c) == false) { - src = src.Remove(0, length); + length = i; + break; } + } - var startIndex = 0; - for (int i = src.Length - 1; i >= 0; i--) - { - var c = src[i]; - if (char.IsWhiteSpace(c) == false) - { - startIndex = i + 1; - break; - } - } + if (length != 0) + { + src = src.Remove(0, length); + } - if (startIndex == src.Length) + var startIndex = 0; + for (int i = src.Length - 1; i >= 0; i--) + { + var c = src[i]; + if (char.IsWhiteSpace(c) == false) { - return src; + startIndex = i + 1; + break; } + } - length = src.Length - startIndex; - src.Remove(startIndex, length); + if (startIndex == src.Length) + { return src; } -#endregion + length = src.Length - startIndex; + src.Remove(startIndex, length); + return src; } + + #endregion } \ No newline at end of file diff --git a/src/Jeffijoe.MessageFormat/IMessageFormatter.cs b/src/Jeffijoe.MessageFormat/IMessageFormatter.cs index d50a7ba..41d6bbc 100644 --- a/src/Jeffijoe.MessageFormat/IMessageFormatter.cs +++ b/src/Jeffijoe.MessageFormat/IMessageFormatter.cs @@ -5,38 +5,37 @@ using System.Collections.Generic; -namespace Jeffijoe.MessageFormat +namespace Jeffijoe.MessageFormat; + +/// +/// The magical Message Formatter. +/// +public interface IMessageFormatter { + #region Public properties + + /// + /// The custom value formatter to use for formats like `number`, `date`, `time` etc. + /// + CustomValueFormatter? CustomValueFormatter { get; } + + #endregion + + #region Public Methods and Operators + /// - /// The magical Message Formatter. + /// Formats the message with the specified arguments. It's so magical. /// - public interface IMessageFormatter - { - #region Public properties - - /// - /// The custom value formatter to use for formats like `number`, `date`, `time` etc. - /// - CustomValueFormatter? CustomValueFormatter { get; } - - #endregion - - #region Public Methods and Operators - - /// - /// Formats the message with the specified arguments. It's so magical. - /// - /// - /// The pattern. - /// - /// - /// The arguments. - /// - /// - /// The . - /// - string FormatMessage(string pattern, IReadOnlyDictionary argsMap); - - #endregion - } + /// + /// The pattern. + /// + /// + /// The arguments. + /// + /// + /// The . + /// + string FormatMessage(string pattern, IReadOnlyDictionary argsMap); + + #endregion } \ No newline at end of file diff --git a/src/Jeffijoe.MessageFormat/Jeffijoe.MessageFormat.csproj b/src/Jeffijoe.MessageFormat/Jeffijoe.MessageFormat.csproj index 2d26eee..2b453a3 100644 --- a/src/Jeffijoe.MessageFormat/Jeffijoe.MessageFormat.csproj +++ b/src/Jeffijoe.MessageFormat/Jeffijoe.MessageFormat.csproj @@ -18,7 +18,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Jeffijoe.MessageFormat/MessageFormatterException.cs b/src/Jeffijoe.MessageFormat/MessageFormatterException.cs index 8239338..764f478 100644 --- a/src/Jeffijoe.MessageFormat/MessageFormatterException.cs +++ b/src/Jeffijoe.MessageFormat/MessageFormatterException.cs @@ -5,26 +5,25 @@ using System; -namespace Jeffijoe.MessageFormat +namespace Jeffijoe.MessageFormat; + +/// +/// Thrown when an issue has occured in the message formatting process. +/// +public class MessageFormatterException : Exception { + #region Constructors and Destructors + /// - /// Thrown when an issue has occured in the message formatting process. + /// Initializes a new instance of the class. /// - public class MessageFormatterException : Exception + /// + /// The message that describes the error. + /// + public MessageFormatterException(string message) + : base(message) { - #region Constructors and Destructors - - /// - /// Initializes a new instance of the class. - /// - /// - /// The message that describes the error. - /// - public MessageFormatterException(string message) - : base(message) - { - } - - #endregion } + + #endregion } \ No newline at end of file diff --git a/src/Jeffijoe.MessageFormat/Parsing/FormatterRequestCollection.cs b/src/Jeffijoe.MessageFormat/Parsing/FormatterRequestCollection.cs index 5b388d4..9880799 100644 --- a/src/Jeffijoe.MessageFormat/Parsing/FormatterRequestCollection.cs +++ b/src/Jeffijoe.MessageFormat/Parsing/FormatterRequestCollection.cs @@ -7,59 +7,58 @@ using Jeffijoe.MessageFormat.Formatting; -namespace Jeffijoe.MessageFormat.Parsing +namespace Jeffijoe.MessageFormat.Parsing; + +/// +/// Formatter requests collection. +/// +public class FormatterRequestCollection : List, IFormatterRequestCollection { + #region Public Methods and Operators + /// - /// Formatter requests collection. + /// Clones this instance and all of it's items. This lets us reuse pattern parsing result, without having to remember + /// the item's initial state before being modified to match the results of the formatters. /// - public class FormatterRequestCollection : List, IFormatterRequestCollection + /// + /// The . + /// + public IFormatterRequestCollection Clone() { - #region Public Methods and Operators - - /// - /// Clones this instance and all of it's items. This lets us reuse pattern parsing result, without having to remember - /// the item's initial state before being modified to match the results of the formatters. - /// - /// - /// The . - /// - public IFormatterRequestCollection Clone() + var result = new FormatterRequestCollection(); + foreach (var request in this) { - var result = new FormatterRequestCollection(); - foreach (var request in this) - { - result.Add(request.Clone()); - } - - return result; + result.Add(request.Clone()); } - /// - /// Updates the indices of all - /// formatter requests' source literals, starting at - /// next request after the specified index in this collection. - /// - /// - /// The index to start from. - /// - /// - /// Length of the formatter result. - /// Used to compare each literal's inner text length, so we know what to set the - /// indices to on the rest of the requests. - /// - public void ShiftIndices(int indexToStartFrom, int formatterResultLength) - { - var start = this[indexToStartFrom]; + return result; + } - // "- 2" will compensate for { and }. (This works, don't ask why). - int resultLength = formatterResultLength - 2; - for (int i = indexToStartFrom + 1; i < this.Count; i++) - { - var next = this[i]; - next.SourceLiteral.ShiftIndices(resultLength, start.SourceLiteral); - } - } + /// + /// Updates the indices of all + /// formatter requests' source literals, starting at + /// next request after the specified index in this collection. + /// + /// + /// The index to start from. + /// + /// + /// Length of the formatter result. + /// Used to compare each literal's inner text length, so we know what to set the + /// indices to on the rest of the requests. + /// + public void ShiftIndices(int indexToStartFrom, int formatterResultLength) + { + var start = this[indexToStartFrom]; - #endregion + // "- 2" will compensate for { and }. (This works, don't ask why). + int resultLength = formatterResultLength - 2; + for (int i = indexToStartFrom + 1; i < this.Count; i++) + { + var next = this[i]; + next.SourceLiteral.ShiftIndices(resultLength, start.SourceLiteral); + } } + + #endregion } \ No newline at end of file diff --git a/src/Jeffijoe.MessageFormat/Parsing/IFormatterRequestCollection.cs b/src/Jeffijoe.MessageFormat/Parsing/IFormatterRequestCollection.cs index f237978..a613e09 100644 --- a/src/Jeffijoe.MessageFormat/Parsing/IFormatterRequestCollection.cs +++ b/src/Jeffijoe.MessageFormat/Parsing/IFormatterRequestCollection.cs @@ -7,39 +7,38 @@ using Jeffijoe.MessageFormat.Formatting; -namespace Jeffijoe.MessageFormat.Parsing +namespace Jeffijoe.MessageFormat.Parsing; + +/// +/// Formatter requests collection. +/// +public interface IFormatterRequestCollection : IReadOnlyList { + #region Public Methods and Operators + /// - /// Formatter requests collection. + /// Clones this instance and all of it's items. This lets us reuse pattern parsing result, without having to remember + /// the item's initial state before being modified to match the results of the formatters. /// - public interface IFormatterRequestCollection : IReadOnlyList - { - #region Public Methods and Operators - - /// - /// Clones this instance and all of it's items. This lets us reuse pattern parsing result, without having to remember - /// the item's initial state before being modified to match the results of the formatters. - /// - /// - /// The . - /// - IFormatterRequestCollection Clone(); + /// + /// The . + /// + IFormatterRequestCollection Clone(); - /// - /// Updates the indices of all - /// formatter requests' source literals, starting at - /// the specified index in this collection. - /// - /// - /// The index to start from. - /// - /// - /// Length of the formatter result. - /// Used to compare each literal's inner text length, so we know what to set the - /// indices to on the rest of the requests. - /// - void ShiftIndices(int indexToStartFrom, int formatterResultLength); + /// + /// Updates the indices of all + /// formatter requests' source literals, starting at + /// the specified index in this collection. + /// + /// + /// The index to start from. + /// + /// + /// Length of the formatter result. + /// Used to compare each literal's inner text length, so we know what to set the + /// indices to on the rest of the requests. + /// + void ShiftIndices(int indexToStartFrom, int formatterResultLength); - #endregion - } + #endregion } \ No newline at end of file diff --git a/src/Jeffijoe.MessageFormat/Parsing/ILiteralParser.cs b/src/Jeffijoe.MessageFormat/Parsing/ILiteralParser.cs index 07f427c..fae5459 100644 --- a/src/Jeffijoe.MessageFormat/Parsing/ILiteralParser.cs +++ b/src/Jeffijoe.MessageFormat/Parsing/ILiteralParser.cs @@ -7,26 +7,25 @@ using System.Collections.Generic; using System.Text; -namespace Jeffijoe.MessageFormat.Parsing +namespace Jeffijoe.MessageFormat.Parsing; + +/// +/// Brace parser contract. +/// +public interface ILiteralParser { + #region Public Methods and Operators + /// - /// Brace parser contract. + /// Finds the brace matches. /// - public interface ILiteralParser - { - #region Public Methods and Operators - - /// - /// Finds the brace matches. - /// - /// - /// The sb. - /// - /// - /// The . - /// - IEnumerable ParseLiterals(StringBuilder sb); + /// + /// The sb. + /// + /// + /// The . + /// + IEnumerable ParseLiterals(StringBuilder sb); - #endregion - } + #endregion } \ No newline at end of file diff --git a/src/Jeffijoe.MessageFormat/Parsing/IPatternParser.cs b/src/Jeffijoe.MessageFormat/Parsing/IPatternParser.cs index 78d29db..ad42e47 100644 --- a/src/Jeffijoe.MessageFormat/Parsing/IPatternParser.cs +++ b/src/Jeffijoe.MessageFormat/Parsing/IPatternParser.cs @@ -5,27 +5,26 @@ using System.Text; -namespace Jeffijoe.MessageFormat.Parsing +namespace Jeffijoe.MessageFormat.Parsing; + +/// +/// The pattern parser extracts patterns from a string. +/// +public interface IPatternParser { + #region Public Methods and Operators + /// - /// The pattern parser extracts patterns from a string. + /// Parses the source, extracting formatter parameters + /// describing what formatter to use, as well as it's options. /// - public interface IPatternParser - { - #region Public Methods and Operators - - /// - /// Parses the source, extracting formatter parameters - /// describing what formatter to use, as well as it's options. - /// - /// - /// The source. - /// - /// - /// The . - /// - IFormatterRequestCollection Parse(StringBuilder source); + /// + /// The source. + /// + /// + /// The . + /// + IFormatterRequestCollection Parse(StringBuilder source); - #endregion - } + #endregion } \ No newline at end of file diff --git a/src/Jeffijoe.MessageFormat/Parsing/Literal.cs b/src/Jeffijoe.MessageFormat/Parsing/Literal.cs index 660e6da..ff1e667 100644 --- a/src/Jeffijoe.MessageFormat/Parsing/Literal.cs +++ b/src/Jeffijoe.MessageFormat/Parsing/Literal.cs @@ -3,128 +3,127 @@ // Author: Jeff Hansen // Copyright (C) Jeff Hansen 2014. All rights reserved. -namespace Jeffijoe.MessageFormat.Parsing +namespace Jeffijoe.MessageFormat.Parsing; + +/// +/// Represents a position in the source text where we should look for format patterns. +/// +public class Literal { + #region Constructors and Destructors + /// - /// Represents a position in the source text where we should look for format patterns. + /// Initializes a new instance of the class. /// - public class Literal + /// + /// The start index. + /// + /// + /// The end index. + /// + /// + /// The source line number. + /// + /// + /// The source column number. + /// + /// + /// The inner text. + /// + public Literal( + int startIndex, + int endIndex, + int sourceLineNumber, + int sourceColumnNumber, + string innerText) { - #region Constructors and Destructors - - /// - /// Initializes a new instance of the class. - /// - /// - /// The start index. - /// - /// - /// The end index. - /// - /// - /// The source line number. - /// - /// - /// The source column number. - /// - /// - /// The inner text. - /// - public Literal( - int startIndex, - int endIndex, - int sourceLineNumber, - int sourceColumnNumber, - string innerText) - { - this.StartIndex = startIndex; - this.EndIndex = endIndex; - this.SourceLineNumber = sourceLineNumber; - this.SourceColumnNumber = sourceColumnNumber; - this.InnerText = innerText; - } - - #endregion + this.StartIndex = startIndex; + this.EndIndex = endIndex; + this.SourceLineNumber = sourceLineNumber; + this.SourceColumnNumber = sourceColumnNumber; + this.InnerText = innerText; + } - #region Public Properties + #endregion - /// - /// Gets the end index in the source string. - /// - /// - /// The end index. - /// - public int EndIndex { get; private set; } + #region Public Properties - /// - /// Gets the inner text (the content between the braces). - /// - /// - /// The inner text. - /// - public string InnerText { get; private set; } + /// + /// Gets the end index in the source string. + /// + /// + /// The end index. + /// + public int EndIndex { get; private set; } - /// - /// Gets the source column number. - /// - /// - /// The source column number. - /// - public int SourceColumnNumber { get; private set; } + /// + /// Gets the inner text (the content between the braces). + /// + /// + /// The inner text. + /// + public string InnerText { get; private set; } - /// - /// Gets the source line number in the original input string. - /// - /// - /// The source line number. - /// - public int SourceLineNumber { get; private set; } + /// + /// Gets the source column number. + /// + /// + /// The source column number. + /// + public int SourceColumnNumber { get; private set; } - /// - /// Gets the start index in the source string. - /// - /// - /// The start index. - /// - public int StartIndex { get; private set; } + /// + /// Gets the source line number in the original input string. + /// + /// + /// The source line number. + /// + public int SourceLineNumber { get; private set; } - #endregion + /// + /// Gets the start index in the source string. + /// + /// + /// The start index. + /// + public int StartIndex { get; private set; } - #region Public Methods and Operators + #endregion - /// - /// Clones this instance. - /// - /// - /// The . - /// - public Literal Clone() - { - // Assuming that InnerText will never be tampered with. - return new Literal( - this.StartIndex, - this.EndIndex, - this.SourceLineNumber, - this.SourceColumnNumber, - this.InnerText); - } + #region Public Methods and Operators - /// - /// Updates the start and end index. - /// - /// - /// Length of the result. - /// - /// - /// The literal that was just formatted. - /// - public void ShiftIndices(int resultLength, Literal literal) - { - int offset = (literal.EndIndex - literal.StartIndex) - 1; - this.StartIndex = (this.StartIndex - offset) + resultLength; - this.EndIndex = (this.EndIndex - offset) + resultLength; - } + /// + /// Clones this instance. + /// + /// + /// The . + /// + public Literal Clone() + { + // Assuming that InnerText will never be tampered with. + return new Literal( + this.StartIndex, + this.EndIndex, + this.SourceLineNumber, + this.SourceColumnNumber, + this.InnerText); + } - #endregion + /// + /// Updates the start and end index. + /// + /// + /// Length of the result. + /// + /// + /// The literal that was just formatted. + /// + public void ShiftIndices(int resultLength, Literal literal) + { + int offset = (literal.EndIndex - literal.StartIndex) - 1; + this.StartIndex = (this.StartIndex - offset) + resultLength; + this.EndIndex = (this.EndIndex - offset) + resultLength; } + + #endregion } \ No newline at end of file diff --git a/src/Jeffijoe.MessageFormat/Parsing/LiteralParser.cs b/src/Jeffijoe.MessageFormat/Parsing/LiteralParser.cs index a434090..9a9e411 100644 --- a/src/Jeffijoe.MessageFormat/Parsing/LiteralParser.cs +++ b/src/Jeffijoe.MessageFormat/Parsing/LiteralParser.cs @@ -7,187 +7,186 @@ using System.Collections.Generic; using System.Text; -namespace Jeffijoe.MessageFormat.Parsing +namespace Jeffijoe.MessageFormat.Parsing; + +/// +/// Parser for extracting brace matches from a string builder. +/// +public class LiteralParser : ILiteralParser { + #region Public Methods and Operators + /// - /// Parser for extracting brace matches from a string builder. + /// Finds the brace matches. /// - public class LiteralParser : ILiteralParser + /// + /// The sb. + /// + /// + /// The . + /// + public IEnumerable ParseLiterals(StringBuilder sb) { - #region Public Methods and Operators - - /// - /// Finds the brace matches. - /// - /// - /// The sb. - /// - /// - /// The . - /// - public IEnumerable ParseLiterals(StringBuilder sb) + const char OpenBrace = '{'; + const char CloseBrace = '}'; + const char EscapingChar = '\''; + + var result = new List(); + var openBraces = 0; + var closeBraces = 0; + var start = 0; + var braceBalance = 0; + var lineNumber = 1; + var startLineNumber = 1; + var startColumnNumber = 0; + var columnNumber = 0; + var insideEscapeSequence = false; + var currentEscapeSequenceLineNumber = 0; + var currentEscapeSequenceColumnNumber = 0; + const char Cr = '\r'; // Carriage return + const char Lf = '\n'; // Line feed + + var matchTextBuf = StringBuilderPool.Get(); + try { - const char OpenBrace = '{'; - const char CloseBrace = '}'; - const char EscapingChar = '\''; - - var result = new List(); - var openBraces = 0; - var closeBraces = 0; - var start = 0; - var braceBalance = 0; - var lineNumber = 1; - var startLineNumber = 1; - var startColumnNumber = 0; - var columnNumber = 0; - var insideEscapeSequence = false; - var currentEscapeSequenceLineNumber = 0; - var currentEscapeSequenceColumnNumber = 0; - const char Cr = '\r'; // Carriage return - const char Lf = '\n'; // Line feed - - var matchTextBuf = StringBuilderPool.Get(); - try + for (var i = 0; i < sb.Length; i++) { - for (var i = 0; i < sb.Length; i++) - { - var c = sb[i]; + var c = sb[i]; - if (c == Cr) - { - continue; - } + if (c == Cr) + { + continue; + } - if (c == Lf) - { - lineNumber++; - columnNumber = 0; + if (c == Lf) + { + lineNumber++; + columnNumber = 0; - } - else - { - columnNumber++; - } + } + else + { + columnNumber++; + } - if (c == EscapingChar) + if (c == EscapingChar) + { + if (i == sb.Length - 1) { - if (i == sb.Length - 1) - { - if (!insideEscapeSequence) - matchTextBuf.Append(EscapingChar); - - // The last char can't open a new escape sequence, it can only close one - if (insideEscapeSequence) - { - insideEscapeSequence = false; - } - - continue; - } - - matchTextBuf.Append(EscapingChar); - - var nextChar = sb[i + 1]; - if (nextChar == EscapingChar) - { + if (!insideEscapeSequence) matchTextBuf.Append(EscapingChar); - ++i; - continue; - } + // The last char can't open a new escape sequence, it can only close one if (insideEscapeSequence) { insideEscapeSequence = false; - continue; } - if (nextChar == '{' || nextChar == '}' || nextChar == '#') - { - matchTextBuf.Append(nextChar); - insideEscapeSequence = true; - currentEscapeSequenceLineNumber = lineNumber; - currentEscapeSequenceColumnNumber = columnNumber; - ++i; - } + continue; + } + + matchTextBuf.Append(EscapingChar); + var nextChar = sb[i + 1]; + if (nextChar == EscapingChar) + { + matchTextBuf.Append(EscapingChar); + ++i; continue; } if (insideEscapeSequence) { - matchTextBuf.Append(c); + insideEscapeSequence = false; continue; } - if (c == OpenBrace) + if (nextChar == '{' || nextChar == '}' || nextChar == '#') { - openBraces++; - braceBalance++; - - // Record starting position of possible new brace match. - if (braceBalance == 1) - { - start = i; - startColumnNumber = columnNumber; - startLineNumber = lineNumber; - matchTextBuf.Clear(); - } + matchTextBuf.Append(nextChar); + insideEscapeSequence = true; + currentEscapeSequenceLineNumber = lineNumber; + currentEscapeSequenceColumnNumber = columnNumber; + ++i; } - if (c == CloseBrace) - { - closeBraces++; - braceBalance--; + continue; + } - // Write the brace to the match buffer if it's not the closing brace - // we are looking for. - if (braceBalance > 0) - { - matchTextBuf.Append(c); - } - } - else - { - if (i > start && braceBalance > 0) - { - matchTextBuf.Append(c); - } + if (insideEscapeSequence) + { + matchTextBuf.Append(c); + continue; + } - continue; - } + if (c == OpenBrace) + { + openBraces++; + braceBalance++; - if (openBraces != closeBraces) + // Record starting position of possible new brace match. + if (braceBalance == 1) { - continue; + start = i; + startColumnNumber = columnNumber; + startLineNumber = lineNumber; + matchTextBuf.Clear(); } - - result.Add(new Literal(start, i, startLineNumber, startColumnNumber, matchTextBuf.ToString())); - matchTextBuf.Clear(); - start = 0; } - if (insideEscapeSequence) + if (c == CloseBrace) + { + closeBraces++; + braceBalance--; + + // Write the brace to the match buffer if it's not the closing brace + // we are looking for. + if (braceBalance > 0) + { + matchTextBuf.Append(c); + } + } + else { - throw new MalformedLiteralException( - "There is an unclosed escape sequence.", - currentEscapeSequenceLineNumber, - currentEscapeSequenceColumnNumber, - matchTextBuf.ToString()); + if (i > start && braceBalance > 0) + { + matchTextBuf.Append(c); + } + + continue; } if (openBraces != closeBraces) { - throw new UnbalancedBracesException(openBraces, closeBraces); + continue; } - return result; + result.Add(new Literal(start, i, startLineNumber, startColumnNumber, matchTextBuf.ToString())); + matchTextBuf.Clear(); + start = 0; } - finally + + if (insideEscapeSequence) { - StringBuilderPool.Return(matchTextBuf); + throw new MalformedLiteralException( + "There is an unclosed escape sequence.", + currentEscapeSequenceLineNumber, + currentEscapeSequenceColumnNumber, + matchTextBuf.ToString()); + } + + if (openBraces != closeBraces) + { + throw new UnbalancedBracesException(openBraces, closeBraces); } - } - #endregion + return result; + } + finally + { + StringBuilderPool.Return(matchTextBuf); + } } + + #endregion } \ No newline at end of file diff --git a/src/Jeffijoe.MessageFormat/Parsing/MalformedLiteralException.cs b/src/Jeffijoe.MessageFormat/Parsing/MalformedLiteralException.cs index 9d1415b..f90b14c 100644 --- a/src/Jeffijoe.MessageFormat/Parsing/MalformedLiteralException.cs +++ b/src/Jeffijoe.MessageFormat/Parsing/MalformedLiteralException.cs @@ -2,108 +2,107 @@ // - MalformedLiteralException.cs // Author: Jeff Hansen // Copyright (C) Jeff Hansen 2014. All rights reserved. -namespace Jeffijoe.MessageFormat.Parsing +namespace Jeffijoe.MessageFormat.Parsing; + +/// +/// Thrown when the pattern parser finds an invalid character in a literal. +/// +public class MalformedLiteralException : MessageFormatterException { + #region Constructors and Destructors + /// - /// Thrown when the pattern parser finds an invalid character in a literal. + /// Initializes a new instance of the class. /// - public class MalformedLiteralException : MessageFormatterException + /// + /// The message that describes the error. + /// + /// + /// The line number. + /// + /// + /// The column number. + /// + /// + /// A snippet of the text that contained the error. Can be null. + /// + internal MalformedLiteralException( + string message, + int lineNumber = 0, + int columnNumber = 0, + string? sourceSnippet = null) + : base(BuildMessage(message, lineNumber, columnNumber, sourceSnippet)) { - #region Constructors and Destructors - - /// - /// Initializes a new instance of the class. - /// - /// - /// The message that describes the error. - /// - /// - /// The line number. - /// - /// - /// The column number. - /// - /// - /// A snippet of the text that contained the error. Can be null. - /// - internal MalformedLiteralException( - string message, - int lineNumber = 0, - int columnNumber = 0, - string? sourceSnippet = null) - : base(BuildMessage(message, lineNumber, columnNumber, sourceSnippet)) - { - this.LineNumber = lineNumber; - this.ColumnNumber = columnNumber; - this.SourceSnippet = sourceSnippet; - } + this.LineNumber = lineNumber; + this.ColumnNumber = columnNumber; + this.SourceSnippet = sourceSnippet; + } - #endregion + #endregion - #region Public Properties + #region Public Properties - /// - /// Gets the column number. - /// - /// - /// The column number. - /// - public int ColumnNumber { get; private set; } + /// + /// Gets the column number. + /// + /// + /// The column number. + /// + public int ColumnNumber { get; private set; } - /// - /// Gets the line number. - /// - /// - /// The line number. - /// - public int LineNumber { get; private set; } + /// + /// Gets the line number. + /// + /// + /// The line number. + /// + public int LineNumber { get; private set; } - /// - /// Gets the source snippet. - /// - /// - /// The source snippet. - /// - public string? SourceSnippet { get; private set; } + /// + /// Gets the source snippet. + /// + /// + /// The source snippet. + /// + public string? SourceSnippet { get; private set; } - #endregion + #endregion - #region Methods + #region Methods - /// - /// Builds the message. - /// - /// - /// The message. - /// - /// - /// The line number. - /// - /// - /// The column number. - /// - /// - /// The source snippet. - /// - /// - /// The . - /// - private static string BuildMessage(string message, int lineNumber, int columnNumber, string? sourceSnippet) + /// + /// Builds the message. + /// + /// + /// The message. + /// + /// + /// The line number. + /// + /// + /// The column number. + /// + /// + /// The source snippet. + /// + /// + /// The . + /// + private static string BuildMessage(string message, int lineNumber, int columnNumber, string? sourceSnippet) + { + var str = message; + if (lineNumber != 0 && columnNumber != 0) { - var str = message; - if (lineNumber != 0 && columnNumber != 0) - { - str = string.Format("{0}\r\nLine {1}, column {2}", message, lineNumber, columnNumber); - } - - if (string.IsNullOrWhiteSpace(sourceSnippet)) - { - return str; - } + str = string.Format("{0}\r\nLine {1}, column {2}", message, lineNumber, columnNumber); + } - return string.Format("Parser error: {0}\r\nOffending snippet: \"{1}\"", str, sourceSnippet); + if (string.IsNullOrWhiteSpace(sourceSnippet)) + { + return str; } - #endregion + return string.Format("Parser error: {0}\r\nOffending snippet: \"{1}\"", str, sourceSnippet); } + + #endregion } \ No newline at end of file diff --git a/src/Jeffijoe.MessageFormat/Parsing/PatternParser.cs b/src/Jeffijoe.MessageFormat/Parsing/PatternParser.cs index 1aa0524..7ca770c 100644 --- a/src/Jeffijoe.MessageFormat/Parsing/PatternParser.cs +++ b/src/Jeffijoe.MessageFormat/Parsing/PatternParser.cs @@ -11,218 +11,217 @@ using Jeffijoe.MessageFormat.Formatting; using Jeffijoe.MessageFormat.Helpers; -namespace Jeffijoe.MessageFormat.Parsing +namespace Jeffijoe.MessageFormat.Parsing; + +/// +/// Parser for extracting formatter patterns. +/// +public class PatternParser : IPatternParser { + #region Fields + /// - /// Parser for extracting formatter patterns. + /// The _literal parser. /// - public class PatternParser : IPatternParser - { - #region Fields + private readonly ILiteralParser literalParser; - /// - /// The _literal parser. - /// - private readonly ILiteralParser literalParser; + #endregion - #endregion + #region Constructors and Destructors - #region Constructors and Destructors - - /// - /// Initializes a new instance of the class. - /// - public PatternParser() : this(new LiteralParser()) - { - } + /// + /// Initializes a new instance of the class. + /// + public PatternParser() : this(new LiteralParser()) + { + } - /// - /// Initializes a new instance of the class. - /// - /// - /// The literal parser. - /// - public PatternParser(ILiteralParser literalParser) - { - this.literalParser = literalParser; - } + /// + /// Initializes a new instance of the class. + /// + /// + /// The literal parser. + /// + public PatternParser(ILiteralParser literalParser) + { + this.literalParser = literalParser; + } - #endregion + #endregion - #region Public Methods and Operators + #region Public Methods and Operators - /// - /// Parses the specified source. - /// - /// - /// The source. - /// - /// - /// The . - /// - public IFormatterRequestCollection Parse(StringBuilder source) + /// + /// Parses the specified source. + /// + /// + /// The source. + /// + /// + /// The . + /// + public IFormatterRequestCollection Parse(StringBuilder source) + { + var literals = this.literalParser.ParseLiterals(source).ToArray(); + if (literals.Length == 0) { - var literals = this.literalParser.ParseLiterals(source).ToArray(); - if (literals.Length == 0) - { - return new FormatterRequestCollection(); - } + return new FormatterRequestCollection(); + } - var result = new FormatterRequestCollection(); - foreach (var literal in literals) - { - // The first token to follow an opening brace will be the variable name. - var variableName = ReadLiteralSection(literal, 0, false, out var lastIndex)!; + var result = new FormatterRequestCollection(); + foreach (var literal in literals) + { + // The first token to follow an opening brace will be the variable name. + var variableName = ReadLiteralSection(literal, 0, false, out var lastIndex)!; - // The next (if any), is the formatter to use. Null is allowed. - string? formatterKey = null; + // The next (if any), is the formatter to use. Null is allowed. + string? formatterKey = null; - // The rest of the string is what we pass into the formatter. Can be null. - string? formatterArgs = null; - if (variableName.Length != literal.InnerText.Length) + // The rest of the string is what we pass into the formatter. Can be null. + string? formatterArgs = null; + if (variableName.Length != literal.InnerText.Length) + { + formatterKey = ReadLiteralSection(literal, lastIndex + 1, true, out lastIndex); + if (formatterKey != null) { - formatterKey = ReadLiteralSection(literal, lastIndex + 1, true, out lastIndex); - if (formatterKey != null) - { #if NET5_0_OR_GREATER formatterArgs = literal.InnerText.AsSpan(lastIndex + 1, literal.InnerText.Length - lastIndex - 1).Trim() .ToString(); #else - formatterArgs = - literal.InnerText.Substring(lastIndex + 1, literal.InnerText.Length - lastIndex - 1).Trim(); + formatterArgs = + literal.InnerText.Substring(lastIndex + 1, literal.InnerText.Length - lastIndex - 1).Trim(); #endif - } } - - result.Add(new FormatterRequest(literal, variableName, formatterKey, formatterArgs)); } - return result; + result.Add(new FormatterRequest(literal, variableName, formatterKey, formatterArgs)); } - #endregion - - #region Methods - - /// - /// Gets the key from the literal. - /// - /// - /// The literal. - /// - /// - /// The offset. - /// - /// - /// if set to true, allows an empty result, in which case the return value is - /// null - /// - /// - /// The last index. - /// - /// - /// The . - /// - /// - /// Parsing the variable key yielded an empty string. - /// - internal static string? ReadLiteralSection(Literal literal, int offset, bool allowEmptyResult, - out int lastIndex) + return result; + } + + #endregion + + #region Methods + + /// + /// Gets the key from the literal. + /// + /// + /// The literal. + /// + /// + /// The offset. + /// + /// + /// if set to true, allows an empty result, in which case the return value is + /// null + /// + /// + /// The last index. + /// + /// + /// The . + /// + /// + /// Parsing the variable key yielded an empty string. + /// + internal static string? ReadLiteralSection(Literal literal, int offset, bool allowEmptyResult, + out int lastIndex) + { + const char Comma = ','; + + var innerText = literal.InnerText; + var column = literal.SourceColumnNumber; + var foundWhitespace = false; + lastIndex = 0; + var sb = StringBuilderPool.Get(); + try { - const char Comma = ','; - - var innerText = literal.InnerText; - var column = literal.SourceColumnNumber; - var foundWhitespace = false; - lastIndex = 0; - var sb = StringBuilderPool.Get(); - try + for (var i = offset; i < innerText.Length; i++) { - for (var i = offset; i < innerText.Length; i++) + var c = innerText[i]; + column++; + lastIndex = i; + if (c == Comma) { - var c = innerText[i]; - column++; - lastIndex = i; - if (c == Comma) - { - break; - } - - // Disregard whitespace. - var whitespace = char.IsWhiteSpace(c); - if (!whitespace) - { - if (c.IsAlphaNumeric() == false) - { - var msg = $"Invalid literal character '{c}'."; - - // Line number can't have changed. - throw new MalformedLiteralException( - msg, - literal.SourceLineNumber, - column, - innerText); - } - } - else - { - foundWhitespace = true; - } - - sb.Append(c); + break; } - if (sb.Length != 0) + // Disregard whitespace. + var whitespace = char.IsWhiteSpace(c); + if (!whitespace) { - // Trim whitespace from beginning and end of the string, if necessary. - if (!foundWhitespace) - { - return sb.ToString(); - } - - StringBuilder trimmed = sb.TrimWhitespace(); - if (trimmed.Length == 0) + if (c.IsAlphaNumeric() == false) { - if (allowEmptyResult) - { - return null; - } + var msg = $"Invalid literal character '{c}'."; + // Line number can't have changed. throw new MalformedLiteralException( - "Parsing the literal yielded a string that was pure whitespace.", - literal.SourceLineNumber, - column); + msg, + literal.SourceLineNumber, + column, + innerText); } + } + else + { + foundWhitespace = true; + } - if (trimmed.ContainsWhitespace()) + sb.Append(c); + } + + if (sb.Length != 0) + { + // Trim whitespace from beginning and end of the string, if necessary. + if (!foundWhitespace) + { + return sb.ToString(); + } + + StringBuilder trimmed = sb.TrimWhitespace(); + if (trimmed.Length == 0) + { + if (allowEmptyResult) { - throw new MalformedLiteralException( - "Parsed literal must not contain whitespace.", - 0, - 0, - trimmed.ToString()); + return null; } - return sb.ToString(); + throw new MalformedLiteralException( + "Parsing the literal yielded a string that was pure whitespace.", + literal.SourceLineNumber, + column); } - if (allowEmptyResult) + if (trimmed.ContainsWhitespace()) { - return null; + throw new MalformedLiteralException( + "Parsed literal must not contain whitespace.", + 0, + 0, + trimmed.ToString()); } - throw new MalformedLiteralException( - "Parsing the literal yielded an empty string.", - literal.SourceLineNumber, - column); + return sb.ToString(); } - finally + + if (allowEmptyResult) { - StringBuilderPool.Return(sb); + return null; } - } - #endregion + throw new MalformedLiteralException( + "Parsing the literal yielded an empty string.", + literal.SourceLineNumber, + column); + } + finally + { + StringBuilderPool.Return(sb); + } } + + #endregion } \ No newline at end of file diff --git a/src/Jeffijoe.MessageFormat/Parsing/UnbalancedBracesException.cs b/src/Jeffijoe.MessageFormat/Parsing/UnbalancedBracesException.cs index 957849e..6a0f7a5 100644 --- a/src/Jeffijoe.MessageFormat/Parsing/UnbalancedBracesException.cs +++ b/src/Jeffijoe.MessageFormat/Parsing/UnbalancedBracesException.cs @@ -5,82 +5,81 @@ using System; -namespace Jeffijoe.MessageFormat.Parsing +namespace Jeffijoe.MessageFormat.Parsing; + +/// +/// Thrown when the amount of open and close braces does not match. +/// +public class UnbalancedBracesException : ArgumentException { + #region Constructors and Destructors + /// - /// Thrown when the amount of open and close braces does not match. + /// Initializes a new instance of the class. /// - public class UnbalancedBracesException : ArgumentException + /// + /// The brace counter. + /// + /// + /// The close brace count. + /// + internal UnbalancedBracesException(int openBraceCount, int closeBraceCount) + : base(BuildMessage(openBraceCount, closeBraceCount)) { - #region Constructors and Destructors - - /// - /// Initializes a new instance of the class. - /// - /// - /// The brace counter. - /// - /// - /// The close brace count. - /// - internal UnbalancedBracesException(int openBraceCount, int closeBraceCount) - : base(BuildMessage(openBraceCount, closeBraceCount)) - { - this.OpenBraceCount = openBraceCount; - this.CloseBraceCount = closeBraceCount; - } + this.OpenBraceCount = openBraceCount; + this.CloseBraceCount = closeBraceCount; + } - #endregion + #endregion - #region Public Properties + #region Public Properties - /// - /// Gets the close brace count. - /// - /// - /// The close brace count. - /// - public int CloseBraceCount { get; private set; } + /// + /// Gets the close brace count. + /// + /// + /// The close brace count. + /// + public int CloseBraceCount { get; private set; } - /// - /// Gets the brace count. - /// - /// - /// The brace count. - /// - public int OpenBraceCount { get; private set; } + /// + /// Gets the brace count. + /// + /// + /// The brace count. + /// + public int OpenBraceCount { get; private set; } - #endregion + #endregion - #region Methods + #region Methods - /// - /// Builds the message. - /// - /// - /// The bracket counter. - /// - /// - /// The close brace count. - /// - /// - /// The . - /// - /// - /// Bracket counter was 0, which would indicate success. - /// - private static string BuildMessage(int openBraceCount, int closeBraceCount) + /// + /// Builds the message. + /// + /// + /// The bracket counter. + /// + /// + /// The close brace count. + /// + /// + /// The . + /// + /// + /// Bracket counter was 0, which would indicate success. + /// + private static string BuildMessage(int openBraceCount, int closeBraceCount) + { + if (openBraceCount > closeBraceCount) { - if (openBraceCount > closeBraceCount) - { - return "There are " + (openBraceCount - closeBraceCount) - + " more opening braces than there are closing braces."; - } - - return "There are " + (closeBraceCount - openBraceCount) - + " more closing braces than there are opening braces."; + return "There are " + (openBraceCount - closeBraceCount) + + " more opening braces than there are closing braces."; } - #endregion + return "There are " + (closeBraceCount - openBraceCount) + + " more closing braces than there are opening braces."; } + + #endregion } \ No newline at end of file diff --git a/src/Jeffijoe.MessageFormat/StringBuilderPool.cs b/src/Jeffijoe.MessageFormat/StringBuilderPool.cs index 8720a86..f91fc26 100644 --- a/src/Jeffijoe.MessageFormat/StringBuilderPool.cs +++ b/src/Jeffijoe.MessageFormat/StringBuilderPool.cs @@ -1,26 +1,25 @@ using System.Text; using Microsoft.Extensions.ObjectPool; -namespace Jeffijoe.MessageFormat +namespace Jeffijoe.MessageFormat; + +internal static class StringBuilderPool { - internal static class StringBuilderPool - { - private static readonly ObjectPool SbPool; + private static readonly ObjectPool SbPool; - static StringBuilderPool() - { - var shared = new DefaultObjectPoolProvider(); - SbPool = shared.CreateStringBuilderPool(); - } + static StringBuilderPool() + { + var shared = new DefaultObjectPoolProvider(); + SbPool = shared.CreateStringBuilderPool(); + } - public static StringBuilder Get() - { - return SbPool.Get(); - } + public static StringBuilder Get() + { + return SbPool.Get(); + } - public static void Return(StringBuilder sb) - { - SbPool.Return(sb); - } + public static void Return(StringBuilder sb) + { + SbPool.Return(sb); } -} +} \ No newline at end of file From d398f62a460c40a6661774ccf9022d4817049b1f Mon Sep 17 00:00:00 2001 From: Tomasz Cielecki <249719+Cheesebaron@users.noreply.github.com> Date: Mon, 3 Mar 2025 14:53:45 +0100 Subject: [PATCH 71/98] Add PolySharp to polyfill attributes --- src/Jeffijoe.MessageFormat/Jeffijoe.MessageFormat.csproj | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Jeffijoe.MessageFormat/Jeffijoe.MessageFormat.csproj b/src/Jeffijoe.MessageFormat/Jeffijoe.MessageFormat.csproj index 2b453a3..5bd5bc8 100644 --- a/src/Jeffijoe.MessageFormat/Jeffijoe.MessageFormat.csproj +++ b/src/Jeffijoe.MessageFormat/Jeffijoe.MessageFormat.csproj @@ -11,6 +11,7 @@ latest enable net6.0;net8.0;netstandard2.0;netstandard2.1 + true @@ -23,6 +24,10 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive + + all + runtime; build; native; contentfiles; analyzers + From 1117a4a21e2b3f06c2f13c1784d7c4d40a46e38e Mon Sep 17 00:00:00 2001 From: Tomasz Cielecki <249719+Cheesebaron@users.noreply.github.com> Date: Mon, 3 Mar 2025 14:53:45 +0100 Subject: [PATCH 72/98] Mark ToDictionary and GetProperties methods with RequiresUnreferencedCode --- .../Helpers/ObjectHelper.cs | 5 ++++- .../Jeffijoe.MessageFormat.csproj | 15 ++++++++------- src/Jeffijoe.MessageFormat/MessageFormatter.cs | 2 ++ .../MessageFormatterExtensions.cs | 2 ++ 4 files changed, 16 insertions(+), 8 deletions(-) diff --git a/src/Jeffijoe.MessageFormat/Helpers/ObjectHelper.cs b/src/Jeffijoe.MessageFormat/Helpers/ObjectHelper.cs index 3fd6b5c..45af327 100644 --- a/src/Jeffijoe.MessageFormat/Helpers/ObjectHelper.cs +++ b/src/Jeffijoe.MessageFormat/Helpers/ObjectHelper.cs @@ -5,6 +5,7 @@ using System.Collections; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reflection; @@ -26,6 +27,7 @@ internal static class ObjectHelper /// /// The . /// + [RequiresUnreferencedCode("This method uses reflection to read property information on object")] internal static IEnumerable GetProperties(object obj) { var properties = new List(); @@ -54,6 +56,7 @@ internal static IEnumerable GetProperties(object obj) /// /// The . /// + [RequiresUnreferencedCode("This method uses reflection to convert object into dictionary")] internal static Dictionary ToDictionary(this object obj) { // We want to be able to read the property, and it should not be an indexer. @@ -69,4 +72,4 @@ internal static IEnumerable GetProperties(object obj) } #endregion -} \ No newline at end of file +} diff --git a/src/Jeffijoe.MessageFormat/Jeffijoe.MessageFormat.csproj b/src/Jeffijoe.MessageFormat/Jeffijoe.MessageFormat.csproj index 5bd5bc8..fd00743 100644 --- a/src/Jeffijoe.MessageFormat/Jeffijoe.MessageFormat.csproj +++ b/src/Jeffijoe.MessageFormat/Jeffijoe.MessageFormat.csproj @@ -4,14 +4,15 @@ True MessageFormat.snk MessageFormat - True - Jeff Hansen - messageformat,messageformatter,xamarin,ui,messages,formatting,plural,pluralization,singular,select,strings,stringformat,format - https://github.com/jeffijoe/messageformat.net - latest - enable - net6.0;net8.0;netstandard2.0;netstandard2.1 + True + Jeff Hansen + messageformat,messageformatter,xamarin,ui,messages,formatting,plural,pluralization,singular,select,strings,stringformat,format + https://github.com/jeffijoe/messageformat.net + latest + enable + net6.0;net8.0;netstandard2.0;netstandard2.1 true + true diff --git a/src/Jeffijoe.MessageFormat/MessageFormatter.cs b/src/Jeffijoe.MessageFormat/MessageFormatter.cs index 234b9d6..7962c12 100644 --- a/src/Jeffijoe.MessageFormat/MessageFormatter.cs +++ b/src/Jeffijoe.MessageFormat/MessageFormatter.cs @@ -6,6 +6,7 @@ using System; using System.Collections.Generic; using System.Collections.Concurrent; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Text; using Jeffijoe.MessageFormat.Formatting; @@ -202,6 +203,7 @@ public static string Format(string pattern, IReadOnlyDictionary /// /// The formatted message. /// + [RequiresUnreferencedCode("This method uses the FormatMessage extension which uses reflection to convert object into dictionary")] public static string Format(string pattern, object data) { lock (Lock) diff --git a/src/Jeffijoe.MessageFormat/MessageFormatterExtensions.cs b/src/Jeffijoe.MessageFormat/MessageFormatterExtensions.cs index 1e44038..2bc3b79 100644 --- a/src/Jeffijoe.MessageFormat/MessageFormatterExtensions.cs +++ b/src/Jeffijoe.MessageFormat/MessageFormatterExtensions.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using Jeffijoe.MessageFormat.Helpers; namespace Jeffijoe.MessageFormat; @@ -46,6 +47,7 @@ public static string FormatMessage( /// /// The . /// + [RequiresUnreferencedCode("This method uses the ToDictionary extension which uses reflection to convert object into dictionary")] public static string FormatMessage(this IMessageFormatter formatter, string pattern, object args) { return formatter.FormatMessage(pattern, args.ToDictionary()); From f00da5948407fa9f6b2d0c521f622ab826b34cdd Mon Sep 17 00:00:00 2001 From: Tomasz Cielecki <249719+Cheesebaron@users.noreply.github.com> Date: Mon, 3 Mar 2025 14:53:45 +0100 Subject: [PATCH 73/98] Lower extension method overloads for signature with object lower --- src/Jeffijoe.MessageFormat/MessageFormatter.cs | 4 +++- src/Jeffijoe.MessageFormat/MessageFormatterExtensions.cs | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/Jeffijoe.MessageFormat/MessageFormatter.cs b/src/Jeffijoe.MessageFormat/MessageFormatter.cs index 7962c12..f258efe 100644 --- a/src/Jeffijoe.MessageFormat/MessageFormatter.cs +++ b/src/Jeffijoe.MessageFormat/MessageFormatter.cs @@ -8,6 +8,7 @@ using System.Collections.Concurrent; using System.Diagnostics.CodeAnalysis; using System.Linq; +using System.Runtime.CompilerServices; using System.Text; using Jeffijoe.MessageFormat.Formatting; using Jeffijoe.MessageFormat.Formatting.Formatters; @@ -203,6 +204,7 @@ public static string Format(string pattern, IReadOnlyDictionary /// /// The formatted message. /// + [OverloadResolutionPriority(-1)] [RequiresUnreferencedCode("This method uses the FormatMessage extension which uses reflection to convert object into dictionary")] public static string Format(string pattern, object data) { @@ -392,4 +394,4 @@ private IFormatterRequestCollection ParseRequests(string pattern, StringBuilder } #endregion -} \ No newline at end of file +} diff --git a/src/Jeffijoe.MessageFormat/MessageFormatterExtensions.cs b/src/Jeffijoe.MessageFormat/MessageFormatterExtensions.cs index 2bc3b79..52787e8 100644 --- a/src/Jeffijoe.MessageFormat/MessageFormatterExtensions.cs +++ b/src/Jeffijoe.MessageFormat/MessageFormatterExtensions.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; using Jeffijoe.MessageFormat.Helpers; namespace Jeffijoe.MessageFormat; @@ -47,9 +48,10 @@ public static string FormatMessage( /// /// The . /// + [OverloadResolutionPriority(-1)] [RequiresUnreferencedCode("This method uses the ToDictionary extension which uses reflection to convert object into dictionary")] public static string FormatMessage(this IMessageFormatter formatter, string pattern, object args) { return formatter.FormatMessage(pattern, args.ToDictionary()); } -} \ No newline at end of file +} From f2f23dd8b403eb622b375a6f55103bb09df95335 Mon Sep 17 00:00:00 2001 From: Steven Fuqua Date: Fri, 17 Oct 2025 23:18:26 -0700 Subject: [PATCH 74/98] Sync CLDR and add ordinals.xml + README --- .../data/README.md | 3 + .../data/ordinals.xml | 176 ++++++++++++++++++ .../data/plurals.xml | 15 +- 3 files changed, 189 insertions(+), 5 deletions(-) create mode 100644 src/Jeffijoe.MessageFormat.MetadataGenerator/data/README.md create mode 100644 src/Jeffijoe.MessageFormat.MetadataGenerator/data/ordinals.xml diff --git a/src/Jeffijoe.MessageFormat.MetadataGenerator/data/README.md b/src/Jeffijoe.MessageFormat.MetadataGenerator/data/README.md new file mode 100644 index 0000000..d934d39 --- /dev/null +++ b/src/Jeffijoe.MessageFormat.MetadataGenerator/data/README.md @@ -0,0 +1,3 @@ +CLDR supplemental data files obtained from https://cldr.unicode.org/downloads/cldr-47 + +CLDR v47 was released 2025-03-13; refer to https://cldr.unicode.org/index/downloads \ No newline at end of file diff --git a/src/Jeffijoe.MessageFormat.MetadataGenerator/data/ordinals.xml b/src/Jeffijoe.MessageFormat.MetadataGenerator/data/ordinals.xml new file mode 100644 index 0000000..a6636b8 --- /dev/null +++ b/src/Jeffijoe.MessageFormat.MetadataGenerator/data/ordinals.xml @@ -0,0 +1,176 @@ + + + + + + + + + + + + @integer 0~15, 100, 1000, 10000, 100000, 1000000, … + + + + + + n % 10 = 1,2 and n % 100 != 11,12 @integer 1, 2, 21, 22, 31, 32, 41, 42, 51, 52, 61, 62, 71, 72, 81, 82, 101, 1001, … + @integer 0, 3~17, 100, 1000, 10000, 100000, 1000000, … + + + n = 1 @integer 1 + @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … + + + n = 1,5 @integer 1, 5 + @integer 0, 2~4, 6~17, 100, 1000, 10000, 100000, 1000000, … + + + n = 1..4 @integer 1~4 + @integer 0, 5~19, 100, 1000, 10000, 100000, 1000000, … + + + + + + n % 10 = 2,3 and n % 100 != 12,13 @integer 2, 3, 22, 23, 32, 33, 42, 43, 52, 53, 62, 63, 72, 73, 82, 83, 102, 1002, … + @integer 0, 1, 4~17, 100, 1000, 10000, 100000, 1000000, … + + + n % 10 = 3 and n % 100 != 13 @integer 3, 23, 33, 43, 53, 63, 73, 83, 103, 1003, … + @integer 0~2, 4~16, 100, 1000, 10000, 100000, 1000000, … + + + n % 10 = 6,9 or n = 10 @integer 6, 9, 10, 16, 19, 26, 29, 36, 39, 106, 1006, … + @integer 0~5, 7, 8, 11~15, 17, 18, 20, 100, 1000, 10000, 100000, 1000000, … + + + + + + n % 10 = 6 or n % 10 = 9 or n % 10 = 0 and n != 0 @integer 6, 9, 10, 16, 19, 20, 26, 29, 30, 36, 39, 40, 100, 1000, 10000, 100000, 1000000, … + @integer 0~5, 7, 8, 11~15, 17, 18, 21, 101, 1001, … + + + n = 11,8,80,800 @integer 8, 11, 80, 800 + @integer 0~7, 9, 10, 12~17, 100, 1000, 10000, 100000, 1000000, … + + + n = 11,8,80..89,800..899 @integer 8, 11, 80~89, 800~803 + @integer 0~7, 9, 10, 12~17, 100, 1000, 10000, 100000, 1000000, … + + + + + + i = 1 @integer 1 + i = 0 or i % 100 = 2..20,40,60,80 @integer 0, 2~16, 102, 1002, … + @integer 21~36, 100, 1000, 10000, 100000, 1000000, … + + + n = 1 @integer 1 + n % 10 = 4 and n % 100 != 14 @integer 4, 24, 34, 44, 54, 64, 74, 84, 104, 1004, … + @integer 0, 2, 3, 5~17, 100, 1000, 10000, 100000, 1000000, … + + + n = 1..4 or n % 100 = 1..4,21..24,41..44,61..64,81..84 @integer 1~4, 21~24, 41~44, 61~64, 101, 1001, … + n = 5 or n % 100 = 5 @integer 5, 105, 205, 305, 405, 505, 605, 705, 1005, … + @integer 0, 6~20, 100, 1000, 10000, 100000, 1000000, … + + + + + + i = 0 @integer 0 + i = 1 @integer 1 + i = 2,3,4,5,6 @integer 2~6 + @integer 7~22, 100, 1000, 10000, 100000, 1000000, … + + + + + + n % 10 = 1 and n % 100 != 11 @integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, … + n % 10 = 2 and n % 100 != 12 @integer 2, 22, 32, 42, 52, 62, 72, 82, 102, 1002, … + n % 10 = 3 and n % 100 != 13 @integer 3, 23, 33, 43, 53, 63, 73, 83, 103, 1003, … + @integer 0, 4~18, 100, 1000, 10000, 100000, 1000000, … + + + n = 1 @integer 1 + n = 2,3 @integer 2, 3 + n = 4 @integer 4 + @integer 0, 5~19, 100, 1000, 10000, 100000, 1000000, … + + + n = 1,11 @integer 1, 11 + n = 2,12 @integer 2, 12 + n = 3,13 @integer 3, 13 + @integer 0, 4~10, 14~21, 100, 1000, 10000, 100000, 1000000, … + + + n = 1,3 @integer 1, 3 + n = 2 @integer 2 + n = 4 @integer 4 + @integer 0, 5~19, 100, 1000, 10000, 100000, 1000000, … + + + + + + i % 10 = 1 and i % 100 != 11 @integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, … + i % 10 = 2 and i % 100 != 12 @integer 2, 22, 32, 42, 52, 62, 72, 82, 102, 1002, … + i % 10 = 7,8 and i % 100 != 17,18 @integer 7, 8, 27, 28, 37, 38, 47, 48, 57, 58, 67, 68, 77, 78, 87, 88, 107, 1007, … + @integer 0, 3~6, 9~19, 100, 1000, 10000, 100000, 1000000, … + + + + + + i % 10 = 1,2,5,7,8 or i % 100 = 20,50,70,80 @integer 1, 2, 5, 7, 8, 11, 12, 15, 17, 18, 20~22, 25, 101, 1001, … + i % 10 = 3,4 or i % 1000 = 100,200,300,400,500,600,700,800,900 @integer 3, 4, 13, 14, 23, 24, 33, 34, 43, 44, 53, 54, 63, 64, 73, 74, 100, 1003, … + i = 0 or i % 10 = 6 or i % 100 = 40,60,90 @integer 0, 6, 16, 26, 36, 40, 46, 56, 106, 1006, … + @integer 9, 10, 19, 29, 30, 39, 49, 59, 69, 79, 109, 1000, 10000, 100000, 1000000, … + + + + + + n = 1 @integer 1 + n = 2,3 @integer 2, 3 + n = 4 @integer 4 + n = 6 @integer 6 + @integer 0, 5, 7~20, 100, 1000, 10000, 100000, 1000000, … + + + n = 1,5,7,8,9,10 @integer 1, 5, 7~10 + n = 2,3 @integer 2, 3 + n = 4 @integer 4 + n = 6 @integer 6 + @integer 0, 11~25, 100, 1000, 10000, 100000, 1000000, … + + + n = 1,5,7..9 @integer 1, 5, 7~9 + n = 2,3 @integer 2, 3 + n = 4 @integer 4 + n = 6 @integer 6 + @integer 0, 10~24, 100, 1000, 10000, 100000, 1000000, … + + + + + + n = 0,7,8,9 @integer 0, 7~9 + n = 1 @integer 1 + n = 2 @integer 2 + n = 3,4 @integer 3, 4 + n = 5,6 @integer 5, 6 + @integer 10~25, 100, 1000, 10000, 100000, 1000000, … + + + diff --git a/src/Jeffijoe.MessageFormat.MetadataGenerator/data/plurals.xml b/src/Jeffijoe.MessageFormat.MetadataGenerator/data/plurals.xml index 49c49ea..9b1100a 100644 --- a/src/Jeffijoe.MessageFormat.MetadataGenerator/data/plurals.xml +++ b/src/Jeffijoe.MessageFormat.MetadataGenerator/data/plurals.xml @@ -1,9 +1,9 @@ @@ -27,7 +27,7 @@ CLDR data files are interpreted according to the LDML specification (http://unic i = 0,1 @integer 0, 1 @decimal 0.0~1.5 @integer 2~17, 100, 1000, 10000, 100000, 1000000, … @decimal 2.0~3.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, … - + i = 1 and v = 0 @integer 1 @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, … @@ -35,7 +35,7 @@ CLDR data files are interpreted according to the LDML specification (http://unic n = 0,1 or i = 0 and f = 1 @integer 0, 1 @decimal 0.0, 0.1, 1.0, 0.00, 0.01, 1.00, 0.000, 0.001, 1.000, 0.0000, 0.0001, 1.0000 @integer 2~17, 100, 1000, 10000, 100000, 1000000, … @decimal 0.2~0.9, 1.1~1.8, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, … - + n = 0..1 @integer 0, 1 @decimal 0.0, 1.0, 0.00, 1.00, 0.000, 1.000, 0.0000, 1.0000 @integer 2~17, 100, 1000, 10000, 100000, 1000000, … @decimal 0.1~0.9, 1.1~1.7, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, … @@ -76,6 +76,11 @@ CLDR data files are interpreted according to the LDML specification (http://unic i = 0,1 and n != 0 @integer 1 @decimal 0.1~1.6 @integer 2~17, 100, 1000, 10000, 100000, 1000000, … @decimal 2.0~3.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, … + + n = 0 @integer 0 @decimal 0.0, 0.00, 0.000, 0.0000 + n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000 + @integer 2~17, 100, 1000, 10000, 100000, 1000000, … @decimal 0.1~0.9, 1.1~1.7, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, … + n = 0 @integer 0 @decimal 0.0, 0.00, 0.000, 0.0000 n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000 @@ -125,7 +130,7 @@ CLDR data files are interpreted according to the LDML specification (http://unic e = 0 and i != 0 and i % 1000000 = 0 and v = 0 or e != 0..5 @integer 1000000, 1c6, 2c6, 3c6, 4c6, 5c6, 6c6, … @decimal 1.0000001c6, 1.1c6, 2.0000001c6, 2.1c6, 3.0000001c6, 3.1c6, … @integer 2~17, 100, 1000, 10000, 100000, 1c3, 2c3, 3c3, 4c3, 5c3, 6c3, … @decimal 2.0~3.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, 1.0001c3, 1.1c3, 2.0001c3, 2.1c3, 3.0001c3, 3.1c3, … - + i = 1 and v = 0 @integer 1 e = 0 and i != 0 and i % 1000000 = 0 and v = 0 or e != 0..5 @integer 1000000, 1c6, 2c6, 3c6, 4c6, 5c6, 6c6, … @decimal 1.0000001c6, 1.1c6, 2.0000001c6, 2.1c6, 3.0000001c6, 3.1c6, … @integer 0, 2~16, 100, 1000, 10000, 100000, 1c3, 2c3, 3c3, 4c3, 5c3, 6c3, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, 1.0001c3, 1.1c3, 2.0001c3, 2.1c3, 3.0001c3, 3.1c3, … From a05bfe647da994004bd349ff336c3f21b97f150c Mon Sep 17 00:00:00 2001 From: Steven Fuqua Date: Sun, 15 Feb 2026 01:04:16 -0800 Subject: [PATCH 75/98] Update CLDR to 48.1 --- .../data/README.md | 4 ++-- .../data/ordinals.xml | 10 +++++----- .../data/plurals.xml | 20 ++++++++++--------- 3 files changed, 18 insertions(+), 16 deletions(-) diff --git a/src/Jeffijoe.MessageFormat.MetadataGenerator/data/README.md b/src/Jeffijoe.MessageFormat.MetadataGenerator/data/README.md index d934d39..75ddf78 100644 --- a/src/Jeffijoe.MessageFormat.MetadataGenerator/data/README.md +++ b/src/Jeffijoe.MessageFormat.MetadataGenerator/data/README.md @@ -1,3 +1,3 @@ -CLDR supplemental data files obtained from https://cldr.unicode.org/downloads/cldr-47 +CLDR supplemental data files obtained from https://cldr.unicode.org/downloads/cldr-48 -CLDR v47 was released 2025-03-13; refer to https://cldr.unicode.org/index/downloads \ No newline at end of file +CLDR v48.1 was released 2025-01-08; refer to https://cldr.unicode.org/index/downloads \ No newline at end of file diff --git a/src/Jeffijoe.MessageFormat.MetadataGenerator/data/ordinals.xml b/src/Jeffijoe.MessageFormat.MetadataGenerator/data/ordinals.xml index a6636b8..c8ea54b 100644 --- a/src/Jeffijoe.MessageFormat.MetadataGenerator/data/ordinals.xml +++ b/src/Jeffijoe.MessageFormat.MetadataGenerator/data/ordinals.xml @@ -1,7 +1,7 @@ - + @integer 0~15, 100, 1000, 10000, 100000, 1000000, … @@ -57,11 +57,11 @@ CLDR data files are interpreted according to the LDML specification (http://unic n % 10 = 6 or n % 10 = 9 or n % 10 = 0 and n != 0 @integer 6, 9, 10, 16, 19, 20, 26, 29, 30, 36, 39, 40, 100, 1000, 10000, 100000, 1000000, … @integer 0~5, 7, 8, 11~15, 17, 18, 21, 101, 1001, … - + n = 11,8,80,800 @integer 8, 11, 80, 800 @integer 0~7, 9, 10, 12~17, 100, 1000, 10000, 100000, 1000000, … - + n = 11,8,80..89,800..899 @integer 8, 11, 80~89, 800~803 @integer 0~7, 9, 10, 12~17, 100, 1000, 10000, 100000, 1000000, … @@ -101,7 +101,7 @@ CLDR data files are interpreted according to the LDML specification (http://unic n % 10 = 3 and n % 100 != 13 @integer 3, 23, 33, 43, 53, 63, 73, 83, 103, 1003, … @integer 0, 4~18, 100, 1000, 10000, 100000, 1000000, … - + n = 1 @integer 1 n = 2,3 @integer 2, 3 n = 4 @integer 4 diff --git a/src/Jeffijoe.MessageFormat.MetadataGenerator/data/plurals.xml b/src/Jeffijoe.MessageFormat.MetadataGenerator/data/plurals.xml index 9b1100a..26cca25 100644 --- a/src/Jeffijoe.MessageFormat.MetadataGenerator/data/plurals.xml +++ b/src/Jeffijoe.MessageFormat.MetadataGenerator/data/plurals.xml @@ -1,7 +1,7 @@ - + i = 0 or n = 1 @integer 0, 1 @decimal 0.0~1.0, 0.00~0.04 @integer 2~17, 100, 1000, 10000, 100000, 1000000, … @decimal 1.1~2.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, … @@ -27,7 +27,7 @@ CLDR data files are interpreted according to the LDML specification (http://unic i = 0,1 @integer 0, 1 @decimal 0.0~1.5 @integer 2~17, 100, 1000, 10000, 100000, 1000000, … @decimal 2.0~3.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, … - + i = 1 and v = 0 @integer 1 @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, … @@ -76,12 +76,7 @@ CLDR data files are interpreted according to the LDML specification (http://unic i = 0,1 and n != 0 @integer 1 @decimal 0.1~1.6 @integer 2~17, 100, 1000, 10000, 100000, 1000000, … @decimal 2.0~3.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, … - - n = 0 @integer 0 @decimal 0.0, 0.00, 0.000, 0.0000 - n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000 - @integer 2~17, 100, 1000, 10000, 100000, 1000000, … @decimal 0.1~0.9, 1.1~1.7, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, … - - + n = 0 @integer 0 @decimal 0.0, 0.00, 0.000, 0.0000 n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000 @integer 2~17, 100, 1000, 10000, 100000, 1000000, … @decimal 0.1~0.9, 1.1~1.7, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, … @@ -197,6 +192,13 @@ CLDR data files are interpreted according to the LDML specification (http://unic + + n % 10 = 1 and n % 100 != 11 @integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, … @decimal 1.0, 21.0, 31.0, 41.0, 51.0, 61.0, 71.0, 81.0, 101.0, 1001.0, … + n = 2 @integer 2 @decimal 2.0, 2.00, 2.000, 2.0000 + n != 2 and n % 10 = 2..9 and n % 100 != 11..19 @integer 3~9, 22~29, 32, 102, 1002, … @decimal 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 22.0, 102.0, 1002.0, … + f != 0 @decimal 0.1~0.9, 1.1~1.7, 10.1, 100.1, 1000.1, … + @integer 0, 10~20, 30, 40, 50, 60, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0, 10.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, … + n % 10 = 1 and n % 100 != 11,71,91 @integer 1, 21, 31, 41, 51, 61, 81, 101, 1001, … @decimal 1.0, 21.0, 31.0, 41.0, 51.0, 61.0, 81.0, 101.0, 1001.0, … n % 10 = 2 and n % 100 != 12,72,92 @integer 2, 22, 32, 42, 52, 62, 82, 102, 1002, … @decimal 2.0, 22.0, 32.0, 42.0, 52.0, 62.0, 82.0, 102.0, 1002.0, … From b18aad816bf4eb0e00970f1ccb3ca4cc2afa8394 Mon Sep 17 00:00:00 2001 From: Steven Fuqua Date: Sun, 15 Feb 2026 01:06:02 -0800 Subject: [PATCH 76/98] Add plurals xml to metadata csproj --- .../Jeffijoe.MessageFormat.MetadataGenerator.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Jeffijoe.MessageFormat.MetadataGenerator/Jeffijoe.MessageFormat.MetadataGenerator.csproj b/src/Jeffijoe.MessageFormat.MetadataGenerator/Jeffijoe.MessageFormat.MetadataGenerator.csproj index 57486f9..0b5537b 100644 --- a/src/Jeffijoe.MessageFormat.MetadataGenerator/Jeffijoe.MessageFormat.MetadataGenerator.csproj +++ b/src/Jeffijoe.MessageFormat.MetadataGenerator/Jeffijoe.MessageFormat.MetadataGenerator.csproj @@ -11,6 +11,7 @@ + From f707fecc492aeb924e6ad4ca57b91b969e7aec1e Mon Sep 17 00:00:00 2001 From: Steven Fuqua Date: Sun, 15 Feb 2026 01:21:46 -0800 Subject: [PATCH 77/98] xmldoc for AST types --- .../Plural/Parsing/AST/Condition.cs | 15 +++++++++++++++ .../Plural/Parsing/AST/PluralRule.cs | 10 ++++++++++ 2 files changed, 25 insertions(+) diff --git a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/Condition.cs b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/Condition.cs index b3c0bea..cb9f0ac 100644 --- a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/Condition.cs +++ b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/Condition.cs @@ -3,6 +3,12 @@ namespace Jeffijoe.MessageFormat.MetadataGenerator.Plural.Parsing.AST; +/// +/// Corresponds to a pluralRule tag in CLDR XML. +/// +/// +/// <pluralRule count="one">i = 1 and v = 0 @integer 1</pluralRule> +/// [DebuggerDisplay("{{RuleDescription}}")] public class Condition { @@ -13,9 +19,18 @@ public Condition(string count, string ruleDescription, IReadOnlyList + /// The plural form this condition or rule defines, e.g., "one", "two", "few", "many", "other". + /// public string Count { get; } + /// + /// The original text of this rule, e.g., "i = 1 and v = 0 @integer 1". + /// public string RuleDescription { get; } + /// + /// Parsed representation of . + /// public IReadOnlyList OrConditions { get; } } \ No newline at end of file diff --git a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/PluralRule.cs b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/PluralRule.cs index 54ba99e..09c40ea 100644 --- a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/PluralRule.cs +++ b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/PluralRule.cs @@ -2,6 +2,16 @@ namespace Jeffijoe.MessageFormat.MetadataGenerator.Plural.Parsing.AST; +/// +/// Corresponds to a pluralRules tag in CLDR XML (not to be confused with pluralRule). +/// Each instance of this class represents multiple individual rules for a set of locales. +/// +/// +/// <pluralRules locales="ast de en et fi fy gl ia ie io ji lij nl sc sv sw ur yi"> +/// <pluralRule count = "one"> i = 1 and v = 0 @integer 1</pluralRule> +/// ... +/// </pluralRules> +/// public class PluralRule { public PluralRule(string[] locales, IReadOnlyList conditions) From 744e76103b4bf02ff7a7c091f695098774e81a4f Mon Sep 17 00:00:00 2001 From: Steven Fuqua Date: Sun, 15 Feb 2026 01:47:53 -0800 Subject: [PATCH 78/98] parsing basics --- .../Plural/Parsing/PluralParser.cs | 37 +++++++- .../Plural/Parsing/PluralRuleSet.cs | 94 +++++++++++++++++++ .../Plural/PluralLanguagesGenerator.cs | 25 ++--- 3 files changed, 140 insertions(+), 16 deletions(-) create mode 100644 src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/PluralRuleSet.cs diff --git a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/PluralParser.cs b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/PluralParser.cs index 38f02cc..de116e5 100644 --- a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/PluralParser.cs +++ b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/PluralParser.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Xml; using System.Linq; using Jeffijoe.MessageFormat.MetadataGenerator.Plural.Parsing.AST; @@ -16,17 +17,42 @@ public PluralParser(XmlDocument rulesDocument, string[] excludedLocales) _excludedLocales = new HashSet(excludedLocales); } - public IEnumerable Parse() + /// + /// Parses the represented XML document into a new , + /// and returns it. + /// + /// A containing the parsed plural rules of a single type. + public PluralRuleSet Parse() + { + var index = new PluralRuleSet(); + ParseInto(index); + return index; + } + + /// + /// Parses the represented XML document and merges the rules into the given . + /// + /// + /// If the CLDR XML is missing expected attributes. + public void ParseInto(PluralRuleSet ruleIndex) { var root = _rulesDocument.DocumentElement!; - + foreach(XmlNode dataElement in root.ChildNodes) { if (dataElement.Name != "plurals") { continue; } - + + var typeAttr = dataElement.Attributes["type"]; + if (!typeAttr.Specified) + { + throw new ArgumentException("CLDR ruleset document is unexpectedly missing 'type' attribute on 'plurals' element."); + } + + string pluralType = typeAttr.Value; + foreach (XmlNode rule in dataElement.ChildNodes) { if(rule.Name == "pluralRules") @@ -34,7 +60,7 @@ public IEnumerable Parse() var parsed = ParseSingleRule(rule); if (parsed != null) { - yield return parsed; + ruleIndex.Add(pluralType, parsed); } } } @@ -51,6 +77,7 @@ public IEnumerable Parse() } var conditions = new List(); + foreach (XmlNode condition in rule.ChildNodes) { if (condition.Name == "pluralRule") diff --git a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/PluralRuleSet.cs b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/PluralRuleSet.cs new file mode 100644 index 0000000..e18ce3b --- /dev/null +++ b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/PluralRuleSet.cs @@ -0,0 +1,94 @@ +using System.Collections.Generic; +using Jeffijoe.MessageFormat.MetadataGenerator.Plural.Parsing.AST; + +namespace Jeffijoe.MessageFormat.MetadataGenerator.Plural.Parsing; + +/// +/// Represents multiple fully parsed documents of instances, each with a given type (i.e., 'cardinal' vs 'ordinals'). +/// +public class PluralRuleSet +{ + /// + /// Indexes a set of instances by plural type & locale. + /// + /// + /// Intended for runtime lookup of plural rules when formatting messages. + /// + private readonly Dictionary _byLocaleAndType = []; + + /// + /// Keeps track of which languages we've seen. + /// + private readonly HashSet _indexedLocales = []; + + /// + /// Gets the unique conditions that have been indexed. Can be used to generate unique helper functions + /// to match specific rules based on an input number. + /// + public IEnumerable RulesWithUniqueConditions => _byLocaleAndType.Values; + + /// + /// Gets the set of observed locale strings. + /// + public IEnumerable IndexedLocales => this._indexedLocales; + + /// + /// Adds the given rule to our index under the given plural type. + /// + /// e.g., 'cardinal' or 'ordinal'. + /// The parsed rule. + public void Add(string pluralType, PluralRule rule) + { + foreach (var locale in rule.Locales) + { + this._indexedLocales.Add(locale); + this._byLocaleAndType.Add( + new RuleKey + { + PluralType = pluralType, + Locale = locale, + }, + rule + ); + } + } + + + + /// + /// Walks the types of plurals we've indexed for a specific locale, along with corresponding rule. + /// Each returned rule is guaranteed to have a unique value. + /// + public IEnumerable GetIndexedRulesByTypeForLocale(string locale) + { + if (_byLocaleType.TryGetValue(locale, out var byType)) + { + foreach (var kvp in byType) + { + yield return kvp.Value; + } + } + } + + /// + /// Attempts to lookup the rule for a specific locale and a specific type. + /// + /// Null if no match. + public PluralRule? Get(string locale, string pluralType) + { + if (_byLocaleType.TryGetValue(locale, out var byType)) + { + byType.TryGetValue(pluralType, out var rule); + return rule; + } + + return null; + } + + /// + /// Used to retrieve a specific . + /// + /// e.g., 'cardinal' or 'ordinal'. + /// + public record struct RuleKey(string PluralType, string Locale); +} \ No newline at end of file diff --git a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/PluralLanguagesGenerator.cs b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/PluralLanguagesGenerator.cs index 9d1e439..d47015c 100644 --- a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/PluralLanguagesGenerator.cs +++ b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/PluralLanguagesGenerator.cs @@ -1,13 +1,10 @@ using Jeffijoe.MessageFormat.MetadataGenerator.Plural.Parsing; -using Jeffijoe.MessageFormat.MetadataGenerator.Plural.Parsing.AST; using Jeffijoe.MessageFormat.MetadataGenerator.Plural.SourceGeneration; using Microsoft.CodeAnalysis; using System; -using System.Collections.Generic; using System.IO; -using System.Linq; using System.Xml; namespace Jeffijoe.MessageFormat.MetadataGenerator.Plural; @@ -36,20 +33,26 @@ private string[] ReadExcludeLocales(GeneratorExecutionContext context) return Array.Empty(); } - private IReadOnlyList GetRules(string[] excludedLocales) + private PluralRuleSet GetRules(string[] excludedLocales) { - using var rulesStream = GetRulesContentStream(); - var xml = new XmlDocument(); - xml.Load(rulesStream); + PluralRuleSet ruleIndex = new(); + foreach (var ruleset in new[] { "plurals.xml", "ordinals.xml" }) + { + using var rulesStream = GetRulesContentStream(ruleset); + var xml = new XmlDocument(); + xml.Load(rulesStream); + + var parser = new PluralParser(xml, excludedLocales); + parser.ParseInto(ruleIndex); + } - var parser = new PluralParser(xml, excludedLocales); - return parser.Parse().ToArray(); + return ruleIndex; } - private Stream GetRulesContentStream() + private Stream GetRulesContentStream(string cldrFileName) { - return typeof(PluralLanguagesGenerator).Assembly.GetManifestResourceStream("Jeffijoe.MessageFormat.MetadataGenerator.data.plurals.xml")!; + return typeof(PluralLanguagesGenerator).Assembly.GetManifestResourceStream($"Jeffijoe.MessageFormat.MetadataGenerator.data.{cldrFileName}")!; } public void Initialize(GeneratorInitializationContext context) From d177f40ce42f867376841a7d354aa1da63897c38 Mon Sep 17 00:00:00 2001 From: Steven Fuqua Date: Sun, 15 Feb 2026 15:38:43 -0800 Subject: [PATCH 79/98] Add some documentation to LMDL types --- .../Plural/Parsing/AST/Condition.cs | 13 +++- .../Formatting/Formatters/PluralContext.cs | 68 +++++++++++++++++++ 2 files changed, 80 insertions(+), 1 deletion(-) diff --git a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/Condition.cs b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/Condition.cs index cb9f0ac..06fc945 100644 --- a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/Condition.cs +++ b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/Condition.cs @@ -4,11 +4,18 @@ namespace Jeffijoe.MessageFormat.MetadataGenerator.Plural.Parsing.AST; /// -/// Corresponds to a pluralRule tag in CLDR XML. +/// Represents the 'condition' part of the LDML grammar. /// /// +/// Given the following 'pluralRule' tag: /// <pluralRule count="one">i = 1 and v = 0 @integer 1</pluralRule> +/// +/// A Condition instance would represent 'i = 1 and v = 0' as a single . /// +/// +/// The grammar defines a condition as a union of 'and_conditions', which we model as a +/// list of that each internally tracks . +/// [DebuggerDisplay("{{RuleDescription}}")] public class Condition { @@ -27,6 +34,10 @@ public Condition(string count, string ruleDescription, IReadOnlyList /// The original text of this rule, e.g., "i = 1 and v = 0 @integer 1". /// + /// + /// Note - this includes the sample text ('@integer 1') which gets stripped out + /// when parsing the rule's conditional logic. + /// public string RuleDescription { get; } /// diff --git a/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralContext.cs b/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralContext.cs index ca7a49c..a09c307 100644 --- a/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralContext.cs +++ b/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralContext.cs @@ -3,6 +3,9 @@ namespace Jeffijoe.MessageFormat.Formatting.Formatters; +/// +/// Represents the 'operations' for a given source number, as defined by Unicode TR35/LDML. +/// internal readonly struct PluralContext { public PluralContext(int number) @@ -26,10 +29,24 @@ public PluralContext(double number) : this(number.ToString(CultureInfo.Invariant { } + /// + /// Represents operands for a source number in string format. + /// This library treats the input as a stringified double and does not currently parse out + /// compact decimal forms (e.g., "1.25c4"). + /// public PluralContext(string number) : this(number, double.Parse(number, CultureInfo.InvariantCulture)) { } + /// + /// Common constructor for parsing out operands from a stringified number. + /// + /// + /// The values of , , , and are all derived + /// from the fractional part of the number, so it's important be parsable as a number. + /// + /// The number in string form, as a decimal (not scientific/compact form). + /// The number pre-parsed as a double. private PluralContext(string number, double parsed) { Number = parsed; @@ -60,26 +77,77 @@ private PluralContext(string number, double parsed) W = fractionSpanWithoutZeroes.Length; F = int.Parse(fractionSpan); T = int.Parse(fractionSpanWithoutZeroes); + + // The compact decimal exponent representations are not used in this library as operands are + // always assumed to be parsable numbers. C = 0; E = 0; } } + /// + /// The 'source number' being evaluated for pluralization. + /// public double Number { get; } + /// + /// The absolute value of . + /// public double N { get; } + /// + /// The integer digits of . + /// + /// + /// 22.6 -> I = 22 + /// public int I { get; } + /// + /// The count of visible fraction digits of , with trailing zeroes. + /// + /// + /// 1.450 -> V = 3 + /// public int V { get; } + /// + /// The count of visible fraction digits of , without trailing zeroes. + /// + /// + /// 1.450 -> W = 2 + /// public int W { get; } + /// + /// The visible fraction digits of , with trailing zeroes, as an integer. + /// + /// + /// 1.450 -> F = 450 + /// public int F { get; } + /// + /// The visible fraction digits of , without trailing zeroes, as an integer. + /// + /// + /// 1.450 -> T = 45 + /// public int T { get; } + /// + /// The compact decimal exponent of , in such cases where + /// is represented as "[x]cC" such that == x * 10^C. + /// + /// + /// 1.25c4 -> C = 4 + /// 125c2 -> C = 2 + /// 12500 -> C = 0, as the number is not represented in compact decimal form. + /// public int C { get; } + /// + /// Deprecated (in LDML) synonym for , reserved for future use by the standard. + /// public int E { get; } } \ No newline at end of file From 03a621c694d437b1da057b86bd153e039e89cbf1 Mon Sep 17 00:00:00 2001 From: Steven Fuqua Date: Sun, 15 Feb 2026 15:39:08 -0800 Subject: [PATCH 80/98] Codegen likely happy now --- .../Plural/Parsing/PluralRuleSet.cs | 58 +++++-------------- .../PluralRulesMetadataGenerator.cs | 46 +++++++-------- .../Formatting/Formatters/PluralLookupKey.cs | 9 +++ 3 files changed, 44 insertions(+), 69 deletions(-) create mode 100644 src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralLookupKey.cs diff --git a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/PluralRuleSet.cs b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/PluralRuleSet.cs index e18ce3b..c0772e5 100644 --- a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/PluralRuleSet.cs +++ b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/PluralRuleSet.cs @@ -8,29 +8,27 @@ namespace Jeffijoe.MessageFormat.MetadataGenerator.Plural.Parsing; /// public class PluralRuleSet { + private readonly List _allRules = []; + /// /// Indexes a set of instances by plural type & locale. /// /// /// Intended for runtime lookup of plural rules when formatting messages. /// - private readonly Dictionary _byLocaleAndType = []; - - /// - /// Keeps track of which languages we've seen. - /// - private readonly HashSet _indexedLocales = []; + private readonly Dictionary _byLocaleAndType = []; /// /// Gets the unique conditions that have been indexed. Can be used to generate unique helper functions /// to match specific rules based on an input number. /// - public IEnumerable RulesWithUniqueConditions => _byLocaleAndType.Values; + public IReadOnlyList UniqueRules => this._allRules; /// - /// Gets the set of observed locale strings. + /// Walks indexes plural rules by locale + type, and gets the index into + /// for each tuple. /// - public IEnumerable IndexedLocales => this._indexedLocales; + public IEnumerable> RuleIndicesByKey => this._byLocaleAndType; /// /// Adds the given rule to our index under the given plural type. @@ -39,56 +37,26 @@ public class PluralRuleSet /// The parsed rule. public void Add(string pluralType, PluralRule rule) { + this._allRules.Add(rule); + int newRuleIndex = this._allRules.Count - 1; + foreach (var locale in rule.Locales) { - this._indexedLocales.Add(locale); this._byLocaleAndType.Add( - new RuleKey + new PluralRuleKey { PluralType = pluralType, Locale = locale, }, - rule + newRuleIndex ); } } - - - /// - /// Walks the types of plurals we've indexed for a specific locale, along with corresponding rule. - /// Each returned rule is guaranteed to have a unique value. - /// - public IEnumerable GetIndexedRulesByTypeForLocale(string locale) - { - if (_byLocaleType.TryGetValue(locale, out var byType)) - { - foreach (var kvp in byType) - { - yield return kvp.Value; - } - } - } - - /// - /// Attempts to lookup the rule for a specific locale and a specific type. - /// - /// Null if no match. - public PluralRule? Get(string locale, string pluralType) - { - if (_byLocaleType.TryGetValue(locale, out var byType)) - { - byType.TryGetValue(pluralType, out var rule); - return rule; - } - - return null; - } - /// /// Used to retrieve a specific . /// /// e.g., 'cardinal' or 'ordinal'. /// - public record struct RuleKey(string PluralType, string Locale); + public record struct PluralRuleKey(string PluralType, string Locale); } \ No newline at end of file diff --git a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/SourceGeneration/PluralRulesMetadataGenerator.cs b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/SourceGeneration/PluralRulesMetadataGenerator.cs index e797b99..463f9f0 100644 --- a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/SourceGeneration/PluralRulesMetadataGenerator.cs +++ b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/SourceGeneration/PluralRulesMetadataGenerator.cs @@ -1,16 +1,15 @@ -using System.Collections.Generic; -using System.Text; -using Jeffijoe.MessageFormat.MetadataGenerator.Plural.Parsing.AST; +using System.Text; +using Jeffijoe.MessageFormat.MetadataGenerator.Plural.Parsing; namespace Jeffijoe.MessageFormat.MetadataGenerator.Plural.SourceGeneration; public class PluralRulesMetadataGenerator { - private readonly IReadOnlyList _rules; + private readonly PluralRuleSet _rules; private readonly StringBuilder _sb; private int _indent; - public PluralRulesMetadataGenerator(IReadOnlyList rules) + public PluralRulesMetadataGenerator(PluralRuleSet rules) { _rules = rules; _sb = new StringBuilder(); @@ -29,18 +28,14 @@ public string GenerateClass() WriteLine("{"); AddIndent(); - for (var ruleIdx = 0; ruleIdx < _rules.Count; ruleIdx++) + // Generate a method for each unique rule, by index, that chooses the plural form + // for a given input source number (the PluralContext) according to that rule. + var uniqueRules = _rules.UniqueRules; + for (var ruleIdx = 0; ruleIdx < uniqueRules.Count; ruleIdx++) { - var rule = _rules[ruleIdx]; - + var rule = uniqueRules[ruleIdx]; var ruleGenerator = new RuleGenerator(rule); - foreach(var locale in rule.Locales) - { - WriteLine($"public static string Locale_{locale.ToUpper()}(PluralContext context) => Rule{ruleIdx}(context);"); - WriteLine(string.Empty); - } - WriteLine($"private static string Rule{ruleIdx}(PluralContext context)"); WriteLine("{"); AddIndent(); @@ -52,18 +47,19 @@ public string GenerateClass() WriteLine(string.Empty); } - WriteLine("private static readonly Dictionary Pluralizers = new Dictionary()"); + // Generate a static lookup dictionary of each (locale, plural type) to the corresponding rule method + // to use for that locale and type. + WriteLine("private static readonly Dictionary Pluralizers = new Dictionary()"); WriteLine("{"); AddIndent(); - for (int ruleIdx = 0; ruleIdx < _rules.Count; ruleIdx++) + foreach (var kvp in _rules.RuleIndicesByKey) { - PluralRule rule = _rules[ruleIdx]; - foreach (var locale in rule.Locales) - { - WriteLine($"{{\"{locale}\", Rule{ruleIdx}}},"); - } - + string locale = kvp.Key.Locale; + string pluralType = kvp.Key.PluralType; + int ruleIdx = kvp.Value; + + WriteLine($"{{new PluralLookupKey(Locale: \"{locale}\", PluralType: \"{pluralType}\"), Rule{ruleIdx}}},"); WriteLine(string.Empty); } @@ -71,11 +67,13 @@ public string GenerateClass() WriteLine("};"); WriteLine(string.Empty); - WriteLine("public static partial bool TryGetRuleByLocale(string locale, out ContextPluralizer contextPluralizer)"); + // Finally generate our public API to the rest of the library, that takes a locale and pluralType + // and tries to retrieve an appropriate localizer to map an input source number to the form for the request. + WriteLine("public static partial bool TryGetRuleByLocale(PluralLookupKey key, out ContextPluralizer contextPluralizer)"); WriteLine("{"); AddIndent(); - WriteLine("return Pluralizers.TryGetValue(locale, out contextPluralizer);"); + WriteLine("return Pluralizers.TryGetValue(key, out contextPluralizer);"); DecreaseIndent(); WriteLine("}"); diff --git a/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralLookupKey.cs b/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralLookupKey.cs new file mode 100644 index 0000000..aef80bf --- /dev/null +++ b/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralLookupKey.cs @@ -0,0 +1,9 @@ +namespace Jeffijoe.MessageFormat.Formatting.Formatters +{ + /// + /// Used to retrieve a specific . + /// + /// e.g., 'cardinal' or 'ordinal'. + /// + public record struct PluralRuleKey(string PluralType, string Locale); +} From c6fff547faade1364ef86b425a6ea16c2787f345 Mon Sep 17 00:00:00 2001 From: Steven Fuqua Date: Sun, 15 Feb 2026 16:19:10 -0800 Subject: [PATCH 81/98] Building and tests passing --- .../PluralRulesMetadataGenerator.cs | 7 +- .../Formatters/PluralFormatterTests.cs | 6 +- .../MessageFormatterFullIntegrationTests.cs | 3 +- .../GeneratedPluralRulesTests.cs | 6 +- .../MetadataGenerator/ParserTests.cs | 30 ++--- .../PluralMetadataClassGeneratorTests.cs | 45 ++++++-- .../Formatting/Formatters/PluralFormatter.cs | 106 +++++++++++++++--- .../Formatting/Formatters/PluralLookupKey.cs | 9 -- .../Formatting/Formatters/PluralRuleKey.cs | 20 ++++ .../Formatters/PluralRulesMetadata.cs | 6 +- .../MessageFormatter.cs | 4 +- 11 files changed, 176 insertions(+), 66 deletions(-) delete mode 100644 src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralLookupKey.cs create mode 100644 src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralRuleKey.cs diff --git a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/SourceGeneration/PluralRulesMetadataGenerator.cs b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/SourceGeneration/PluralRulesMetadataGenerator.cs index 463f9f0..6ead4a6 100644 --- a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/SourceGeneration/PluralRulesMetadataGenerator.cs +++ b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/SourceGeneration/PluralRulesMetadataGenerator.cs @@ -49,7 +49,7 @@ public string GenerateClass() // Generate a static lookup dictionary of each (locale, plural type) to the corresponding rule method // to use for that locale and type. - WriteLine("private static readonly Dictionary Pluralizers = new Dictionary()"); + WriteLine("private static readonly Dictionary Pluralizers = new Dictionary()"); WriteLine("{"); AddIndent(); @@ -59,8 +59,7 @@ public string GenerateClass() string pluralType = kvp.Key.PluralType; int ruleIdx = kvp.Value; - WriteLine($"{{new PluralLookupKey(Locale: \"{locale}\", PluralType: \"{pluralType}\"), Rule{ruleIdx}}},"); - WriteLine(string.Empty); + WriteLine($"{{new PluralRuleKey(Locale: \"{locale}\", PluralType: \"{pluralType}\"), Rule{ruleIdx}}},"); } DecreaseIndent(); @@ -69,7 +68,7 @@ public string GenerateClass() // Finally generate our public API to the rest of the library, that takes a locale and pluralType // and tries to retrieve an appropriate localizer to map an input source number to the form for the request. - WriteLine("public static partial bool TryGetRuleByLocale(PluralLookupKey key, out ContextPluralizer contextPluralizer)"); + WriteLine("public static partial bool TryGetRuleByLocale(PluralRuleKey key, out ContextPluralizer contextPluralizer)"); WriteLine("{"); AddIndent(); diff --git a/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/PluralFormatterTests.cs b/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/PluralFormatterTests.cs index e870bde..94cfe51 100644 --- a/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/PluralFormatterTests.cs +++ b/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/PluralFormatterTests.cs @@ -48,7 +48,7 @@ public void Pluralize(double n, string expected) }, Array.Empty()); var request = new FormatterRequest(new Literal(1, 1, 1, 1, ""), "test", "plural", null); - var actual = subject.Pluralize("en", arguments, new PluralContext(Convert.ToDecimal(Convert.ToDouble(args[request.Variable]))), 0); + var actual = subject.Pluralize(PluralRuleKey.Cardinal("en"), arguments, new PluralContext(Convert.ToDecimal(Convert.ToDouble(args[request.Variable]))), 0); Assert.Equal(expected, actual); } @@ -70,7 +70,7 @@ public void Pluralize_defaults_to_en_locale_when_specified_locale_is_not_found() }, Array.Empty()); var request = new FormatterRequest(new Literal(1, 1, 1, 1, ""), "test", "plural", null); - var actual = subject.Pluralize("unknown", arguments, new PluralContext(Convert.ToDecimal(Convert.ToDouble(args[request.Variable]))), 0); + var actual = subject.Pluralize(PluralRuleKey.Cardinal("unknown"), arguments, new PluralContext(Convert.ToDecimal(Convert.ToDouble(args[request.Variable]))), 0); Assert.Equal("just one", actual); } @@ -91,7 +91,7 @@ public void Pluralize_throws_when_missing_other_block() }, Array.Empty()); var request = new FormatterRequest(new Literal(1, 1, 1, 1, ""), "test", "plural", null); - Assert.Throws(() => subject.Pluralize("unknown", arguments, new PluralContext(Convert.ToDecimal(Convert.ToDouble(args[request.Variable]))), 0)); + Assert.Throws(() => subject.Pluralize(PluralRuleKey.Cardinal("unknown"), arguments, new PluralContext(Convert.ToDecimal(Convert.ToDouble(args[request.Variable]))), 0)); } /// diff --git a/src/Jeffijoe.MessageFormat.Tests/MessageFormatterFullIntegrationTests.cs b/src/Jeffijoe.MessageFormat.Tests/MessageFormatterFullIntegrationTests.cs index 6651ff3..4e591c9 100644 --- a/src/Jeffijoe.MessageFormat.Tests/MessageFormatterFullIntegrationTests.cs +++ b/src/Jeffijoe.MessageFormat.Tests/MessageFormatterFullIntegrationTests.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using Jeffijoe.MessageFormat.Formatting; +using Jeffijoe.MessageFormat.Formatting.Formatters; using Jeffijoe.MessageFormat.Tests.TestHelpers; using Xunit; using Xunit.Abstractions; @@ -603,7 +604,7 @@ public void ReadMe_test_to_make_sure_I_dont_look_like_a_fool() { var mf = new MessageFormatter(useCache: true, locale: "en"); - mf.Pluralizers!["en"] = n => + mf.Pluralizers![PluralRuleKey.Cardinal("en")] = n => { // ´n´ is the number being pluralized. // ReSharper disable once CompareOfFloatsByEqualityOperator diff --git a/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/GeneratedPluralRulesTests.cs b/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/GeneratedPluralRulesTests.cs index 0db8c6b..868c581 100644 --- a/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/GeneratedPluralRulesTests.cs +++ b/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/GeneratedPluralRulesTests.cs @@ -30,7 +30,7 @@ public void Uk_PluralizerTests(double n, string expected) }, new FormatterExtension[0]); var request = new FormatterRequest(new Literal(1, 1, 1, 1, ""), "test", "plural", null); - var actual = subject.Pluralize("uk", arguments, new PluralContext(Convert.ToDecimal(Convert.ToDouble(args[request.Variable]))), 0); + var actual = subject.Pluralize(PluralRuleKey.Cardinal("uk"), arguments, new PluralContext(Convert.ToDecimal(Convert.ToDouble(args[request.Variable]))), 0); Assert.Equal(expected, actual); } @@ -55,7 +55,7 @@ public void Ru_PluralizerTests(double n, string expected) }, new FormatterExtension[0]); var request = new FormatterRequest(new Literal(1, 1, 1, 1, ""), "test", "plural", null); - var actual = subject.Pluralize("ru", arguments, new PluralContext(Convert.ToDecimal(args[request.Variable])), 0); + var actual = subject.Pluralize(PluralRuleKey.Cardinal("ru"), arguments, new PluralContext(Convert.ToDecimal(args[request.Variable])), 0); Assert.Equal(expected, actual); } @@ -78,7 +78,7 @@ public void En_PluralizerTests(double n, string expected) }, new FormatterExtension[0]); var request = new FormatterRequest(new Literal(1, 1, 1, 1, ""), "test", "plural", null); - var actual = subject.Pluralize("en", arguments, new PluralContext(Convert.ToDecimal(args[request.Variable])), 0); + var actual = subject.Pluralize(PluralRuleKey.Cardinal("en"), arguments, new PluralContext(Convert.ToDecimal(args[request.Variable])), 0); Assert.Equal(expected, actual); } } \ No newline at end of file diff --git a/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/ParserTests.cs b/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/ParserTests.cs index 4cecb75..a17f8a5 100644 --- a/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/ParserTests.cs +++ b/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/ParserTests.cs @@ -23,7 +23,7 @@ public void CanParseLocales() "); - var rule = Assert.Single(rules); + var rule = Assert.Single(rules.UniqueRules); var expected = new[] { "am", "as", "bn", "doi", "fa", "gu", "hi", "kn", "pcm", "zu" @@ -44,7 +44,7 @@ public void OtherCountIsIgnored() "); - var rule = Assert.Single(rules); + var rule = Assert.Single(rules.UniqueRules); Assert.Empty(rule.Conditions); } @@ -53,7 +53,7 @@ public void CanParseSingleCount_RuleDescription_WithoutRelations() { var rules = ParseRules(GenerateXmlWithRuleContent("@integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, …")); - var rule = Assert.Single(rules); + var rule = Assert.Single(rules.UniqueRules); var condition = Assert.Single(rule.Conditions); var expected = "@integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, …"; Assert.Equal(expected, condition.RuleDescription); @@ -64,7 +64,7 @@ public void CanParseSingleCount_VisibleDigitsNumber() { var rules = ParseRules( GenerateXmlWithRuleContent(@"v = 0 @integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, …")); - var rule = Assert.Single(rules); + var rule = Assert.Single(rules.UniqueRules); var condition = Assert.Single(rule.Conditions); var orCondition = Assert.Single(condition.OrConditions); var actual = Assert.Single(orCondition.AndConditions); @@ -78,7 +78,7 @@ public void CanParseSingleCount_IntegerDigits() { var rules = ParseRules( GenerateXmlWithRuleContent(@"i = 0 @integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, …")); - var rule = Assert.Single(rules); + var rule = Assert.Single(rules.UniqueRules); var condition = Assert.Single(rule.Conditions); var orCondition = Assert.Single(condition.OrConditions); var actual = Assert.Single(orCondition.AndConditions); @@ -92,7 +92,7 @@ public void CanParseSingleCount_AbsoluteNumber() { var rules = ParseRules( GenerateXmlWithRuleContent("n = 1 @integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, …")); - var rule = Assert.Single(rules); + var rule = Assert.Single(rules.UniqueRules); var condition = Assert.Single(rule.Conditions); var orCondition = Assert.Single(condition.OrConditions); var actual = Assert.Single(orCondition.AndConditions); @@ -107,7 +107,7 @@ public void CanParseSingleCount_AbsoluteNumber() public void CanParseVariousRelations(string ruleText, Relation expectedRelation) { var rules = ParseRules(GenerateXmlWithRuleContent(ruleText)); - var rule = Assert.Single(rules); + var rule = Assert.Single(rules.UniqueRules); var condition = Assert.Single(rule.Conditions); var orCondition = Assert.Single(condition.OrConditions); var actual = Assert.Single(orCondition.AndConditions); @@ -120,7 +120,7 @@ public void CanParseVariousRelations(string ruleText, Relation expectedRelation) public void CanParseOrRules() { var rules = ParseRules(GenerateXmlWithRuleContent("n = 2 or n = 1 or n = 0 @integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, …")); - var rule = Assert.Single(rules); + var rule = Assert.Single(rules.UniqueRules); var condition = Assert.Single(rule.Conditions); Assert.Equal(3, condition.OrConditions.Count); @@ -142,7 +142,7 @@ public void CanParseOrRules() public void CanParseAndRules() { var rules = ParseRules(GenerateXmlWithRuleContent("n = 2 and n = 1 and n = 0 @integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, …")); - var rule = Assert.Single(rules); + var rule = Assert.Single(rules.UniqueRules); var condition = Assert.Single(rule.Conditions); var orCondition = Assert.Single(condition.OrConditions); @@ -166,7 +166,7 @@ public void CanParseModuloInLeftOperator() { var rules = ParseRules( GenerateXmlWithRuleContent("n % 5 = 3 @integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, …")); - var rule = Assert.Single(rules); + var rule = Assert.Single(rules.UniqueRules); var condition = Assert.Single(rule.Conditions); var orCondition = Assert.Single(condition.OrConditions); var actual = Assert.Single(orCondition.AndConditions); @@ -181,7 +181,7 @@ public void CanParseRangeInRightOperator() { var rules = ParseRules( GenerateXmlWithRuleContent("n = 3..5 @integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, …")); - var rule = Assert.Single(rules); + var rule = Assert.Single(rules.UniqueRules); var condition = Assert.Single(rule.Conditions); var orCondition = Assert.Single(condition.OrConditions); var actual = Assert.Single(orCondition.AndConditions); @@ -196,7 +196,7 @@ public void CanParseCommaSeparatedInRightOperator() { var rules = ParseRules( GenerateXmlWithRuleContent("n = 3,5,8, 10 @integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, …")); - var rule = Assert.Single(rules); + var rule = Assert.Single(rules.UniqueRules); var condition = Assert.Single(rule.Conditions); var orCondition = Assert.Single(condition.OrConditions); var actual = Assert.Single(orCondition.AndConditions); @@ -211,7 +211,7 @@ public void CanParseMixedCommaSeparatedAndRangeInRightOperator() { var rules = ParseRules( GenerateXmlWithRuleContent("n = 3,5..7,12,15 @integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, …")); - var rule = Assert.Single(rules); + var rule = Assert.Single(rules.UniqueRules); var condition = Assert.Single(rule.Conditions); var orCondition = Assert.Single(condition.OrConditions); var actual = Assert.Single(orCondition.AndConditions); @@ -234,7 +234,7 @@ public void MapsVariable_ToCorrectOperator(char variable, OperandSymbol symbol) { var rules = ParseRules( GenerateXmlWithRuleContent($"{variable} = 3")); - var rule = Assert.Single(rules); + var rule = Assert.Single(rules.UniqueRules); var condition = Assert.Single(rule.Conditions); var orCondition = Assert.Single(condition.OrConditions); var actual = Assert.Single(orCondition.AndConditions); @@ -264,7 +264,7 @@ private static void AssertOperationEqual(Operation expected, Operation actual) Assert.Equal(expected.OperandRight, actual.OperandRight); } - private static IEnumerable ParseRules(string xmlText) + private static PluralRuleSet ParseRules(string xmlText) { var xml = new XmlDocument(); xml.LoadXml(xmlText); diff --git a/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/PluralMetadataClassGeneratorTests.cs b/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/PluralMetadataClassGeneratorTests.cs index b10ed33..5779466 100644 --- a/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/PluralMetadataClassGeneratorTests.cs +++ b/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/PluralMetadataClassGeneratorTests.cs @@ -1,4 +1,5 @@ -using Jeffijoe.MessageFormat.MetadataGenerator.Plural.Parsing.AST; +using Jeffijoe.MessageFormat.MetadataGenerator.Plural.Parsing; +using Jeffijoe.MessageFormat.MetadataGenerator.Plural.Parsing.AST; using Jeffijoe.MessageFormat.MetadataGenerator.Plural.SourceGeneration; using Xunit; @@ -22,9 +23,25 @@ public void CanGenerateClassFromRules() new Operation(new VariableOperand(OperandSymbol.AbsoluteValue), Relation.Equals, new[] {new NumberOperand(3) }) }) }) - }) + }), + new PluralRule(new[] {"en"}, + new[] + { + new Condition("many", string.Empty, new [] + { + new OrCondition(new[] + { + new Operation(new VariableOperand(OperandSymbol.AbsoluteValue), Relation.Equals, new[] {new NumberOperand(120) }) + }) + }) + }), }; - var generator = new PluralRulesMetadataGenerator(rules); + + var ruleSet = new PluralRuleSet(); + ruleSet.Add("cardinal", rules[0]); + ruleSet.Add("ordinal", rules[1]); + + var generator = new PluralRulesMetadataGenerator(ruleSet); var actual = generator.GenerateClass(); @@ -35,10 +52,6 @@ namespace Jeffijoe.MessageFormat.Formatting.Formatters { internal static partial class PluralRulesMetadata { - public static string Locale_EN(PluralContext context) => Rule0(context); - - public static string Locale_UK(PluralContext context) => Rule0(context); - private static string Rule0(PluralContext context) { if ((context.N == 3)) @@ -47,16 +60,24 @@ private static string Rule0(PluralContext context) return ""other""; } - private static readonly Dictionary Pluralizers = new Dictionary() + private static string Rule1(PluralContext context) { - {""en"", Rule0}, - {""uk"", Rule0}, + if ((context.N == 120)) + return ""many""; + return ""other""; + } + + private static readonly Dictionary Pluralizers = new Dictionary() + { + {new PluralRuleKey(Locale: ""en"", PluralType: ""cardinal""), Rule0}, + {new PluralRuleKey(Locale: ""uk"", PluralType: ""cardinal""), Rule0}, + {new PluralRuleKey(Locale: ""en"", PluralType: ""ordinal""), Rule1}, }; - public static partial bool TryGetRuleByLocale(string locale, out ContextPluralizer contextPluralizer) + public static partial bool TryGetRuleByLocale(PluralRuleKey key, out ContextPluralizer contextPluralizer) { - return Pluralizers.TryGetValue(locale, out contextPluralizer); + return Pluralizers.TryGetValue(key, out contextPluralizer); } } } diff --git a/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralFormatter.cs b/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralFormatter.cs index 844f854..47129f1 100644 --- a/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralFormatter.cs +++ b/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralFormatter.cs @@ -15,6 +15,36 @@ namespace Jeffijoe.MessageFormat.Formatting.Formatters; /// public class PluralFormatter : BaseFormatter, IFormatter { + /// + /// CLDR plural type attribute for counting number ruleset. + /// + internal const string CardinalType = "cardinal"; + + /// + /// CLDR plural type attribute for ordered number ruleset. + /// + internal const string OrdinalType = "ordinal"; + + /// + /// ICU MessageFormat function name for "default" pluralization, based on cardinal numbers. + /// + private const string PluralFunction = "plural"; + + /// + /// ICU MessageFormat function name for ordinal pluralization. + /// + private const string OrdinalFunction = "selectordinal"; + + /// + /// Maps supported parser names to CLDR plural types. + /// The plural language rule schema is identical between these types and we just need to pick the correct set. + /// + private static readonly Dictionary CldrTypeForFunction = new() + { + { PluralFunction, CardinalType }, + { OrdinalFunction, OrdinalType } + }; + #region Constructors and Destructors /// @@ -22,7 +52,7 @@ public class PluralFormatter : BaseFormatter, IFormatter /// public PluralFormatter() { - this.Pluralizers = new Dictionary(); + this.Pluralizers = new Dictionary(); this.AddStandardPluralizers(); } @@ -37,12 +67,12 @@ public PluralFormatter() /// - /// Gets the pluralizers dictionary. Key is the locale. + /// Gets the pluralizers dictionary. Key is the locale and plural type. /// /// /// The pluralizers. /// - public IDictionary Pluralizers { get; private set; } + public IDictionary Pluralizers { get; private set; } #endregion @@ -59,7 +89,12 @@ public PluralFormatter() /// public bool CanFormat(FormatterRequest request) { - return request.FormatterName == "plural"; + if (request.FormatterName is null) + { + return false; + } + + return CldrTypeForFunction.ContainsKey(request.FormatterName); } /// @@ -84,6 +119,9 @@ public bool CanFormat(FormatterRequest request) /// /// The . /// + /// + /// If does not specify a formatter name supported by . + /// public string Format(string locale, FormatterRequest request, IReadOnlyDictionary args, @@ -98,8 +136,19 @@ public string Format(string locale, offset = Convert.ToDouble(offsetExtension.Value); } + // Get CLDR plural ruleset from request. + // CanFormat() should have guaranteed this is valid, but we'll be defensive just in case. + if (!CldrTypeForFunction.TryGetValue(request.FormatterName ?? string.Empty, out var pluralType)) + { + throw new MessageFormatterException($"Unsupported plural formatter name: {request.FormatterName}"); + } + var ctx = CreatePluralContext(value, offset); - var pluralized = this.Pluralize(locale, arguments, ctx, offset); + var pluralized = this.Pluralize( + new PluralRuleKey(PluralType: pluralType, Locale: locale), + arguments, + ctx, + offset); var result = this.ReplaceNumberLiterals(pluralized, ctx.Number); var formatted = messageFormatter.FormatMessage(result, args); return formatted; @@ -112,8 +161,8 @@ public string Format(string locale, /// /// Returns the correct plural block. /// - /// - /// The locale. + /// + /// The locale and pluralType. /// /// /// The parsed arguments string. @@ -132,20 +181,20 @@ public string Format(string locale, /// [SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1126:PrefixCallsCorrectly", Justification = "Reviewed. Suppression is OK here.")] - internal string Pluralize(string locale, ParsedArguments arguments, PluralContext context, double offset) + internal string Pluralize(PluralRuleKey ruleKey, ParsedArguments arguments, PluralContext context, double offset) { string pluralForm; - if (this.Pluralizers.TryGetValue(locale, out var pluralizer)) + if (this.Pluralizers.TryGetValue(ruleKey, out var pluralizer)) { pluralForm = pluralizer(context.Number); } - else if (PluralRulesMetadata.TryGetRuleByLocale(locale, out var contextPluralizer)) + else if (PluralRulesMetadata.TryGetRuleByLocale(ruleKey, out var contextPluralizer)) { - pluralForm= contextPluralizer(context); + pluralForm = contextPluralizer(context); } else { - pluralForm = this.Pluralizers["en"](context.Number); + pluralForm = this.Pluralizers[new PluralRuleKey(Locale: "en", PluralType: ruleKey.PluralType)](context.Number); } KeyedBlock? other = null; @@ -287,10 +336,10 @@ internal string ReplaceNumberLiterals(string pluralized, double n) /// /// Adds the standard pluralizers. /// - private void AddStandardPluralizers() + protected virtual void AddStandardPluralizers() { this.Pluralizers.Add( - "en", + PluralRuleKey.Cardinal("en"), n => { // ReSharper disable CompareOfFloatsByEqualityOperator @@ -306,7 +355,34 @@ private void AddStandardPluralizers() // ReSharper restore CompareOfFloatsByEqualityOperator return "other"; - }); + } + ); + this.Pluralizers.Add( + PluralRuleKey.Ordinal("en"), + n => + { + // e.g., 1st + if (n % 10 == 1 && n % 100 != 11) + { + return "one"; + } + + // e.g., 2nd + if (n % 10 == 2 && n % 100 != 12) + { + return "two"; + } + + // e.g., 3rd + if (n % 10 == 3 && n % 100 != 13) + { + return "few"; + } + + // e.g., 4th, 11th, etc + return "other"; + } + ); } /// diff --git a/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralLookupKey.cs b/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralLookupKey.cs deleted file mode 100644 index aef80bf..0000000 --- a/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralLookupKey.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Jeffijoe.MessageFormat.Formatting.Formatters -{ - /// - /// Used to retrieve a specific . - /// - /// e.g., 'cardinal' or 'ordinal'. - /// - public record struct PluralRuleKey(string PluralType, string Locale); -} diff --git a/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralRuleKey.cs b/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralRuleKey.cs new file mode 100644 index 0000000..a665ae4 --- /dev/null +++ b/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralRuleKey.cs @@ -0,0 +1,20 @@ +namespace Jeffijoe.MessageFormat.Formatting.Formatters +{ + /// + /// Used to retrieve a specific . + /// + /// e.g., 'cardinal' or 'ordinal'. + /// + public record struct PluralRuleKey(string PluralType, string Locale) + { + /// + /// Helper to generate a cardinal rule look up for a locale, suitable for the 'plural' MessageFormat function. + /// + public static PluralRuleKey Cardinal(string locale) => new(PluralType: PluralFormatter.CardinalType, Locale: locale); + + /// + /// Helper to generate an ordinal rule look up for a locale, suitable for the 'selectordinal' MessageFormat function. + /// + public static PluralRuleKey Ordinal(string locale) => new(PluralType: PluralFormatter.OrdinalType, Locale: locale); + } +} diff --git a/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralRulesMetadata.cs b/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralRulesMetadata.cs index 7c98e93..ccf5bb2 100644 --- a/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralRulesMetadata.cs +++ b/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralRulesMetadata.cs @@ -1,7 +1,9 @@ -namespace Jeffijoe.MessageFormat.Formatting.Formatters; +using System.Diagnostics.CodeAnalysis; + +namespace Jeffijoe.MessageFormat.Formatting.Formatters; [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] internal static partial class PluralRulesMetadata { - public static partial bool TryGetRuleByLocale(string locale, out ContextPluralizer contextPluralizer); + public static partial bool TryGetRuleByLocale(PluralRuleKey key, out ContextPluralizer contextPluralizer); } \ No newline at end of file diff --git a/src/Jeffijoe.MessageFormat/MessageFormatter.cs b/src/Jeffijoe.MessageFormat/MessageFormatter.cs index f258efe..4c37b78 100644 --- a/src/Jeffijoe.MessageFormat/MessageFormatter.cs +++ b/src/Jeffijoe.MessageFormat/MessageFormatter.cs @@ -145,12 +145,12 @@ public IFormatterLibrary Formatters public string Locale { get; set; } /// - /// Gets the pluralizers dictionary from the , if set. Key is the locale. + /// Gets the pluralizers dictionary from the , if set. Key is the locale, then the plural type. /// /// /// The pluralizers, or null if the plural formatter has not been added. /// - public IDictionary? Pluralizers + public IDictionary? Pluralizers { get { From eff20d1150aca8319ba15455600cac5df9abed7b Mon Sep 17 00:00:00 2001 From: Steven Fuqua Date: Sun, 15 Feb 2026 16:58:31 -0800 Subject: [PATCH 82/98] Add new README test for selectordinal --- README.md | 1 + .../MessageFormatterFullIntegrationTests.cs | 10 ++++++++++ 2 files changed, 11 insertions(+) diff --git a/README.md b/README.md index afbbe36..315eca6 100644 --- a/README.md +++ b/README.md @@ -87,6 +87,7 @@ MessageFormat.NET supports the most commonly used formats: * Select Format: `{gender, select, male{He likes} female{She likes} other{They like}} cheeseburgers` * Plural Format: `There {msgCount, plural, zero {are no unread messages} one {is 1 unread message} other{are # unread messages}}.` (where `#` is the actual number, with the offset (if any) subtracted). +* Ordinal Format: `You are the {position, selectordinal, one {#st} two {#nd} few {#rd} other {#th}} person in line.` * Simple variable replacement: `Your name is {name}` * Numbers: `Your age is {age, number}` * Dates: `You were born {birthday, date}` diff --git a/src/Jeffijoe.MessageFormat.Tests/MessageFormatterFullIntegrationTests.cs b/src/Jeffijoe.MessageFormat.Tests/MessageFormatterFullIntegrationTests.cs index 4e591c9..cba63f1 100644 --- a/src/Jeffijoe.MessageFormat.Tests/MessageFormatterFullIntegrationTests.cs +++ b/src/Jeffijoe.MessageFormat.Tests/MessageFormatterFullIntegrationTests.cs @@ -577,6 +577,16 @@ public void ReadMe_test_to_make_sure_I_dont_look_like_a_fool() Assert.Equal("Your messages go here.", formatted); } + { + var mf = new MessageFormatter(false); + const string Str = @"You are the {position, selectordinal, one {#st} two {#nd} few {#rd} other {#th}} person in line."; + var formatted = mf.FormatMessage(Str, new Dictionary { { "position", 23 } }); + Assert.Equal("You are the 23rd person in line.", formatted); + + formatted = mf.FormatMessage(Str, new Dictionary { { "position", 1 } }); + Assert.Equal("You are the 1st person in line.", formatted); + } + { var mf = new MessageFormatter(false); const string Str = @"His name is {LAST_NAME}... {FIRST_NAME} {LAST_NAME}"; From 22c151be0af4b9c6473310efaee534748d58e4c9 Mon Sep 17 00:00:00 2001 From: Steven Fuqua Date: Sun, 15 Feb 2026 20:07:59 -0800 Subject: [PATCH 83/98] Fix bad test --- .../MessageFormatterFullIntegrationTests.cs | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/Jeffijoe.MessageFormat.Tests/MessageFormatterFullIntegrationTests.cs b/src/Jeffijoe.MessageFormat.Tests/MessageFormatterFullIntegrationTests.cs index cba63f1..579be27 100644 --- a/src/Jeffijoe.MessageFormat.Tests/MessageFormatterFullIntegrationTests.cs +++ b/src/Jeffijoe.MessageFormat.Tests/MessageFormatterFullIntegrationTests.cs @@ -203,6 +203,14 @@ public static IEnumerable Tests other {and # others added this to their profiles} }."; + const string Case7 = @"Your {count, selectordinal, + =0 {nonexistent} + one {#st} + two {#nd} + few {#rd} + other {#th} + } notification is the most recent one."; + yield return new object[] { @@ -349,6 +357,20 @@ public static IEnumerable Tests new Dictionary { { "count", 3 } }, "You and 2 others added this to their profiles." }; + yield return + new object[] + { + Case7, + new Dictionary { { "count", 0 } }, + "Your nonexistent notification is the most recent one." + }; + yield return + new object[] + { + Case7, + new Dictionary { { "count", 2 } }, + "Your 2nd notification is the most recent one." + }; yield return new object[] { From 550fa53339de78f69ddbbe3ed180ce9c098403f2 Mon Sep 17 00:00:00 2001 From: Steven Fuqua Date: Sun, 15 Feb 2026 21:14:22 -0800 Subject: [PATCH 84/98] readonly record struct --- .../Plural/Parsing/PluralRuleSet.cs | 21 ++++++++++++------- .../Formatting/Formatters/PluralRuleKey.cs | 2 +- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/PluralRuleSet.cs b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/PluralRuleSet.cs index c0772e5..473963f 100644 --- a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/PluralRuleSet.cs +++ b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/PluralRuleSet.cs @@ -43,11 +43,7 @@ public void Add(string pluralType, PluralRule rule) foreach (var locale in rule.Locales) { this._byLocaleAndType.Add( - new PluralRuleKey - { - PluralType = pluralType, - Locale = locale, - }, + new PluralRuleKey(PluralType: pluralType, Locale: locale), newRuleIndex ); } @@ -56,7 +52,16 @@ public void Add(string pluralType, PluralRule rule) /// /// Used to retrieve a specific . /// - /// e.g., 'cardinal' or 'ordinal'. - /// - public record struct PluralRuleKey(string PluralType, string Locale); + public readonly record struct PluralRuleKey(string PluralType, string Locale) + { + /// + /// e.g., 'cardinal' or 'ordinal'. + /// + public string PluralType { get; } = PluralType; + + /// + /// The language to query. + /// + public string Locale { get; } = Locale; + } } \ No newline at end of file diff --git a/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralRuleKey.cs b/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralRuleKey.cs index a665ae4..7f7357c 100644 --- a/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralRuleKey.cs +++ b/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralRuleKey.cs @@ -5,7 +5,7 @@ /// /// e.g., 'cardinal' or 'ordinal'. /// - public record struct PluralRuleKey(string PluralType, string Locale) + public readonly record struct PluralRuleKey(string PluralType, string Locale) { /// /// Helper to generate a cardinal rule look up for a locale, suitable for the 'plural' MessageFormat function. From fec0a92e04b381d7525544487633bba53dc2da6e Mon Sep 17 00:00:00 2001 From: Steven Fuqua Date: Sun, 15 Feb 2026 21:36:48 -0800 Subject: [PATCH 85/98] More tests --- .../GeneratedPluralRulesTests.cs | 48 +++++++++++++++++-- .../Formatting/Formatters/PluralFormatter.cs | 5 +- 2 files changed, 46 insertions(+), 7 deletions(-) diff --git a/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/GeneratedPluralRulesTests.cs b/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/GeneratedPluralRulesTests.cs index 868c581..d243171 100644 --- a/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/GeneratedPluralRulesTests.cs +++ b/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/GeneratedPluralRulesTests.cs @@ -18,6 +18,8 @@ public class GeneratedPluralRulesTests public void Uk_PluralizerTests(double n, string expected) { var subject = new PluralFormatter(); + subject.Pluralizers.Clear(); + var args = new Dictionary { { "test", n } }; var arguments = new ParsedArguments( @@ -29,7 +31,7 @@ public void Uk_PluralizerTests(double n, string expected) new KeyedBlock("other", "дня") }, new FormatterExtension[0]); - var request = new FormatterRequest(new Literal(1, 1, 1, 1, ""), "test", "plural", null); + var request = new FormatterRequest(new Literal(1, 1, 1, 1, ""), "test", PluralFormatter.PluralFunction, null); var actual = subject.Pluralize(PluralRuleKey.Cardinal("uk"), arguments, new PluralContext(Convert.ToDecimal(Convert.ToDouble(args[request.Variable]))), 0); Assert.Equal(expected, actual); } @@ -43,6 +45,8 @@ public void Uk_PluralizerTests(double n, string expected) public void Ru_PluralizerTests(double n, string expected) { var subject = new PluralFormatter(); + subject.Pluralizers.Clear(); + var args = new Dictionary { { "test", n } }; var arguments = new ParsedArguments( @@ -54,7 +58,7 @@ public void Ru_PluralizerTests(double n, string expected) new KeyedBlock("other", "дня") }, new FormatterExtension[0]); - var request = new FormatterRequest(new Literal(1, 1, 1, 1, ""), "test", "plural", null); + var request = new FormatterRequest(new Literal(1, 1, 1, 1, ""), "test", PluralFormatter.PluralFunction, null); var actual = subject.Pluralize(PluralRuleKey.Cardinal("ru"), arguments, new PluralContext(Convert.ToDecimal(args[request.Variable])), 0); Assert.Equal(expected, actual); } @@ -65,20 +69,56 @@ public void Ru_PluralizerTests(double n, string expected) [InlineData(101, "days")] [InlineData(102, "days")] [InlineData(105, "days")] - public void En_PluralizerTests(double n, string expected) + public void En_Cardinal_PluralizerTests(double n, string expected) { var subject = new PluralFormatter(); + subject.Pluralizers.Clear(); + var args = new Dictionary { { "test", n } }; var arguments = new ParsedArguments( new[] { + // 'zero' is a red herring to confirm CLDR rules are used instead of built-in; + // CLDR does not specify an English 'zero' form, so 0 should fallthrough to 'other'. + new KeyedBlock("zero", "FAIL"), new KeyedBlock("one", "day"), new KeyedBlock("other", "days") }, new FormatterExtension[0]); - var request = new FormatterRequest(new Literal(1, 1, 1, 1, ""), "test", "plural", null); + var request = new FormatterRequest(new Literal(1, 1, 1, 1, ""), "test", PluralFormatter.PluralFunction, null); var actual = subject.Pluralize(PluralRuleKey.Cardinal("en"), arguments, new PluralContext(Convert.ToDecimal(args[request.Variable])), 0); Assert.Equal(expected, actual); } + + [Theory] + [InlineData(0, "0th")] + [InlineData(1, "1st")] + [InlineData(2, "2nd")] + [InlineData(3, "3rd")] + [InlineData(4, "4th")] + [InlineData(9, "9th")] + [InlineData(11, "11th")] + [InlineData(21, "21st")] + public void En_Ordinal_PluralizerTests(double n, string expected) + { + var subject = new PluralFormatter(); + subject.Pluralizers.Clear(); + + var args = new Dictionary { { "test", n } }; + var arguments = + new ParsedArguments( + new[] + { + new KeyedBlock("one", "#st"), + new KeyedBlock("two", "#nd"), + new KeyedBlock("few", "#rd"), + new KeyedBlock("other", "#th"), + }, + new FormatterExtension[0]); + var request = new FormatterRequest(new Literal(1, 1, 1, 1, ""), "test", PluralFormatter.OrdinalFunction, null); + var pluralized = subject.Pluralize(PluralRuleKey.Ordinal("en"), arguments, new PluralContext(Convert.ToDecimal(args[request.Variable])), 0); + var actual = subject.ReplaceNumberLiterals(pluralized, n); + Assert.Equal(expected, actual); + } } \ No newline at end of file diff --git a/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralFormatter.cs b/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralFormatter.cs index 47129f1..e00a786 100644 --- a/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralFormatter.cs +++ b/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralFormatter.cs @@ -28,12 +28,12 @@ public class PluralFormatter : BaseFormatter, IFormatter /// /// ICU MessageFormat function name for "default" pluralization, based on cardinal numbers. /// - private const string PluralFunction = "plural"; + internal const string PluralFunction = "plural"; /// /// ICU MessageFormat function name for ordinal pluralization. /// - private const string OrdinalFunction = "selectordinal"; + internal const string OrdinalFunction = "selectordinal"; /// /// Maps supported parser names to CLDR plural types. @@ -65,7 +65,6 @@ public PluralFormatter() /// public bool VariableMustExist => true; - /// /// Gets the pluralizers dictionary. Key is the locale and plural type. /// From 8b7ebb6914c7040d30f51fc7de73e113cd70fbc1 Mon Sep 17 00:00:00 2001 From: Steven Fuqua Date: Mon, 16 Feb 2026 09:02:14 -0800 Subject: [PATCH 86/98] PR feedback --- ...joe.MessageFormat.MetadataGenerator.csproj | 4 ++ .../Plural/Parsing/AST/Condition.cs | 22 ++++---- .../Plural/Parsing/AST/PluralRule.cs | 4 +- .../Plural/Parsing/PluralParser.cs | 3 +- .../Plural/Parsing/PluralRuleSet.cs | 15 ++---- .../Formatting/Formatters/PluralContext.cs | 50 +++++++++---------- .../Formatting/Formatters/PluralFormatter.cs | 2 +- .../Formatting/Formatters/PluralRuleKey.cs | 33 ++++++------ 8 files changed, 63 insertions(+), 70 deletions(-) diff --git a/src/Jeffijoe.MessageFormat.MetadataGenerator/Jeffijoe.MessageFormat.MetadataGenerator.csproj b/src/Jeffijoe.MessageFormat.MetadataGenerator/Jeffijoe.MessageFormat.MetadataGenerator.csproj index 0b5537b..4a55968 100644 --- a/src/Jeffijoe.MessageFormat.MetadataGenerator/Jeffijoe.MessageFormat.MetadataGenerator.csproj +++ b/src/Jeffijoe.MessageFormat.MetadataGenerator/Jeffijoe.MessageFormat.MetadataGenerator.csproj @@ -20,6 +20,10 @@ runtime; build; native; contentfiles; analyzers; buildtransitive + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/Condition.cs b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/Condition.cs index 06fc945..05b0ae6 100644 --- a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/Condition.cs +++ b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/Condition.cs @@ -4,17 +4,17 @@ namespace Jeffijoe.MessageFormat.MetadataGenerator.Plural.Parsing.AST; /// -/// Represents the 'condition' part of the LDML grammar. +/// Represents the 'condition' part of the LDML grammar. /// /// -/// Given the following 'pluralRule' tag: -/// <pluralRule count="one">i = 1 and v = 0 @integer 1</pluralRule> +/// Given the following 'pluralRule' tag: +/// <pluralRule count="one">i = 1 and v = 0 @integer 1</pluralRule> /// -/// A Condition instance would represent 'i = 1 and v = 0' as a single . +/// A Condition instance would represent 'i = 1 and v = 0' as a single . /// /// -/// The grammar defines a condition as a union of 'and_conditions', which we model as a -/// list of that each internally tracks . +/// The grammar defines a condition as a union of 'and_conditions', which we model as a +/// list of that each internally tracks . /// [DebuggerDisplay("{{RuleDescription}}")] public class Condition @@ -27,21 +27,21 @@ public Condition(string count, string ruleDescription, IReadOnlyList - /// The plural form this condition or rule defines, e.g., "one", "two", "few", "many", "other". + /// The plural form this condition or rule defines, e.g., "one", "two", "few", "many", "other". /// public string Count { get; } /// - /// The original text of this rule, e.g., "i = 1 and v = 0 @integer 1". + /// The original text of this rule, e.g., "i = 1 and v = 0 @integer 1". /// /// - /// Note - this includes the sample text ('@integer 1') which gets stripped out - /// when parsing the rule's conditional logic. + /// Note - this includes the sample text ('@integer 1') which gets stripped out + /// when parsing the rule's conditional logic. /// public string RuleDescription { get; } /// - /// Parsed representation of . + /// Parsed representation of . /// public IReadOnlyList OrConditions { get; } } \ No newline at end of file diff --git a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/PluralRule.cs b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/PluralRule.cs index 09c40ea..c37efeb 100644 --- a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/PluralRule.cs +++ b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/AST/PluralRule.cs @@ -3,8 +3,8 @@ namespace Jeffijoe.MessageFormat.MetadataGenerator.Plural.Parsing.AST; /// -/// Corresponds to a pluralRules tag in CLDR XML (not to be confused with pluralRule). -/// Each instance of this class represents multiple individual rules for a set of locales. +/// Corresponds to a pluralRules tag in CLDR XML (not to be confused with pluralRule). +/// Each instance of this class represents multiple individual rules for a set of locales. /// /// /// <pluralRules locales="ast de en et fi fy gl ia ie io ji lij nl sc sv sw ur yi"> diff --git a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/PluralParser.cs b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/PluralParser.cs index de116e5..793e79d 100644 --- a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/PluralParser.cs +++ b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/PluralParser.cs @@ -18,8 +18,7 @@ public PluralParser(XmlDocument rulesDocument, string[] excludedLocales) } /// - /// Parses the represented XML document into a new , - /// and returns it. + /// Parses the represented XML document into a new , and returns it. /// /// A containing the parsed plural rules of a single type. public PluralRuleSet Parse() diff --git a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/PluralRuleSet.cs b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/PluralRuleSet.cs index 473963f..c4636b1 100644 --- a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/PluralRuleSet.cs +++ b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/PluralRuleSet.cs @@ -52,16 +52,7 @@ public void Add(string pluralType, PluralRule rule) /// /// Used to retrieve a specific . /// - public readonly record struct PluralRuleKey(string PluralType, string Locale) - { - /// - /// e.g., 'cardinal' or 'ordinal'. - /// - public string PluralType { get; } = PluralType; - - /// - /// The language to query. - /// - public string Locale { get; } = Locale; - } + /// e.g., 'cardinal' or 'ordinal'. + /// Two-letter language tag. + public readonly record struct PluralRuleKey(string PluralType, string Locale); } \ No newline at end of file diff --git a/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralContext.cs b/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralContext.cs index a09c307..1c60f70 100644 --- a/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralContext.cs +++ b/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralContext.cs @@ -4,7 +4,7 @@ namespace Jeffijoe.MessageFormat.Formatting.Formatters; /// -/// Represents the 'operations' for a given source number, as defined by Unicode TR35/LDML. +/// Represents the 'operations' for a given source number, as defined by Unicode TR35/LDML. /// internal readonly struct PluralContext { @@ -30,20 +30,20 @@ public PluralContext(double number) : this(number.ToString(CultureInfo.Invariant } /// - /// Represents operands for a source number in string format. - /// This library treats the input as a stringified double and does not currently parse out - /// compact decimal forms (e.g., "1.25c4"). + /// Represents operands for a source number in string format. + /// This library treats the input as a stringified double and does not currently parse out + /// compact decimal forms (e.g., "1.25c4"). /// public PluralContext(string number) : this(number, double.Parse(number, CultureInfo.InvariantCulture)) { } /// - /// Common constructor for parsing out operands from a stringified number. + /// Common constructor for parsing out operands from a stringified number. /// /// - /// The values of , , , and are all derived - /// from the fractional part of the number, so it's important be parsable as a number. + /// The values of , , , and are all derived + /// from the fractional part of the number, so it's important be parsable as a number. /// /// The number in string form, as a decimal (not scientific/compact form). /// The number pre-parsed as a double. @@ -86,68 +86,68 @@ private PluralContext(string number, double parsed) } /// - /// The 'source number' being evaluated for pluralization. + /// The 'source number' being evaluated for pluralization. /// public double Number { get; } /// - /// The absolute value of . + /// The absolute value of . /// public double N { get; } /// - /// The integer digits of . + /// The integer digits of . /// /// - /// 22.6 -> I = 22 + /// 22.6 -> I = 22 /// public int I { get; } /// - /// The count of visible fraction digits of , with trailing zeroes. + /// The count of visible fraction digits of , with trailing zeroes. /// /// - /// 1.450 -> V = 3 + /// 1.450 -> V = 3 /// public int V { get; } /// - /// The count of visible fraction digits of , without trailing zeroes. + /// The count of visible fraction digits of , without trailing zeroes. /// /// - /// 1.450 -> W = 2 + /// 1.450 -> W = 2 /// public int W { get; } /// - /// The visible fraction digits of , with trailing zeroes, as an integer. + /// The visible fraction digits of , with trailing zeroes, as an integer. /// /// - /// 1.450 -> F = 450 + /// 1.450 -> F = 450 /// public int F { get; } /// - /// The visible fraction digits of , without trailing zeroes, as an integer. + /// The visible fraction digits of , without trailing zeroes, as an integer. /// /// - /// 1.450 -> T = 45 + /// 1.450 -> T = 45 /// public int T { get; } /// - /// The compact decimal exponent of , in such cases where - /// is represented as "[x]cC" such that == x * 10^C. + /// The compact decimal exponent of , in such cases where + /// is represented as "[x]cC" such that == x * 10^C. /// /// - /// 1.25c4 -> C = 4 - /// 125c2 -> C = 2 - /// 12500 -> C = 0, as the number is not represented in compact decimal form. + /// 1.25c4 -> C = 4 + /// 125c2 -> C = 2 + /// 12500 -> C = 0, as the number is not represented in compact decimal form. /// public int C { get; } /// - /// Deprecated (in LDML) synonym for , reserved for future use by the standard. + /// Deprecated (in LDML) synonym for , reserved for future use by the standard. /// public int E { get; } } \ No newline at end of file diff --git a/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralFormatter.cs b/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralFormatter.cs index e00a786..7932268 100644 --- a/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralFormatter.cs +++ b/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralFormatter.cs @@ -335,7 +335,7 @@ internal string ReplaceNumberLiterals(string pluralized, double n) /// /// Adds the standard pluralizers. /// - protected virtual void AddStandardPluralizers() + private void AddStandardPluralizers() { this.Pluralizers.Add( PluralRuleKey.Cardinal("en"), diff --git a/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralRuleKey.cs b/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralRuleKey.cs index 7f7357c..60a5fbb 100644 --- a/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralRuleKey.cs +++ b/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralRuleKey.cs @@ -1,20 +1,19 @@ -namespace Jeffijoe.MessageFormat.Formatting.Formatters +namespace Jeffijoe.MessageFormat.Formatting.Formatters; + +/// +/// Used to retrieve a specific . +/// +/// e.g., 'cardinal' or 'ordinal'. +/// Two-letter language tag. +public readonly record struct PluralRuleKey(string PluralType, string Locale) { /// - /// Used to retrieve a specific . + /// Helper to generate a cardinal rule look up for a locale, suitable for the 'plural' MessageFormat function. /// - /// e.g., 'cardinal' or 'ordinal'. - /// - public readonly record struct PluralRuleKey(string PluralType, string Locale) - { - /// - /// Helper to generate a cardinal rule look up for a locale, suitable for the 'plural' MessageFormat function. - /// - public static PluralRuleKey Cardinal(string locale) => new(PluralType: PluralFormatter.CardinalType, Locale: locale); - - /// - /// Helper to generate an ordinal rule look up for a locale, suitable for the 'selectordinal' MessageFormat function. - /// - public static PluralRuleKey Ordinal(string locale) => new(PluralType: PluralFormatter.OrdinalType, Locale: locale); - } -} + public static PluralRuleKey Cardinal(string locale) => new(PluralType: PluralFormatter.CardinalType, Locale: locale); + + /// + /// Helper to generate an ordinal rule look up for a locale, suitable for the 'selectordinal' MessageFormat function. + /// + public static PluralRuleKey Ordinal(string locale) => new(PluralType: PluralFormatter.OrdinalType, Locale: locale); +} \ No newline at end of file From 23cb4634c15082bdd0078ee8760d823486fb4553 Mon Sep 17 00:00:00 2001 From: Steven Fuqua Date: Mon, 16 Feb 2026 21:25:06 -0800 Subject: [PATCH 87/98] Remove inbox pluralizers; add root locale support; refactor codegen --- README.md | 13 +- .../Plural/Parsing/PluralRuleSet.cs | 117 +++++++++++--- .../PluralRulesMetadataGenerator.cs | 71 +++++++-- .../Formatters/PluralFormatterTests.cs | 18 +-- .../MessageFormatterFullIntegrationTests.cs | 26 +++- .../GeneratedPluralRulesTests.cs | 8 +- .../PluralMetadataClassGeneratorTests.cs | 40 ++++- .../Formatting/Formatters/PluralFormatter.cs | 143 +++++++----------- .../Formatting/Formatters/PluralRuleKey.cs | 19 --- .../Formatters/PluralRulesMetadata.cs | 4 +- .../MessageFormatter.cs | 19 ++- 11 files changed, 308 insertions(+), 170 deletions(-) delete mode 100644 src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralRuleKey.cs diff --git a/README.md b/README.md index 315eca6..c5ece6f 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ https://unicode-org.github.io/icu/userguide/format_parse/messages/ var mf = new MessageFormatter(); var str = @"You have {notifications, plural, - zero {no notifications} + =0 {no notifications} one {one notification} =42 {a universal amount of notifications} other {# notifications} @@ -86,7 +86,7 @@ and about 3 seconds (3236ms) without it. **These results are with a debug build, MessageFormat.NET supports the most commonly used formats: * Select Format: `{gender, select, male{He likes} female{She likes} other{They like}} cheeseburgers` -* Plural Format: `There {msgCount, plural, zero {are no unread messages} one {is 1 unread message} other{are # unread messages}}.` (where `#` is the actual number, with the offset (if any) subtracted). +* Plural Format: `There {msgCount, plural, =0 {are no unread messages} one {is 1 unread message} other{are # unread messages}}.` (where `#` is the actual number, with the offset (if any) subtracted). * Ordinal Format: `You are the {position, selectordinal, one {#st} two {#nd} few {#rd} other {#th}} person in line.` * Simple variable replacement: `Your name is {name}` * Numbers: `Your age is {age, number}` @@ -140,8 +140,11 @@ var message = formatter.FormatMessage("{value, number, $0.0}", new { value = 23 > with the package, so this is no longer needed. Same thing as with [MessageFormat.js][0], you can add your own pluralizer function. -The `Pluralizers` property is a `IDictionary`, so you can remove the built-in -ones if you want. +The `Pluralizers` property is a `IDictionary` that starts empty, along +with `OrdinalPluralizers` for ordinal numbers. + +Adding to these Dictionaries will take precedence over the CLDR data for exact matches on +the input locales. ````csharp var mf = new MessageFormatter(); @@ -163,8 +166,6 @@ var mf = new MessageFormatter(true, "en"); // true = use cache mf.Pluralizers["en"] = n => { // ´n´ is the number being pluralized. - if (n == 0) - return "zero"; if (n == 1) return "one"; if (n > 1000) diff --git a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/PluralRuleSet.cs b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/PluralRuleSet.cs index c4636b1..16eacd2 100644 --- a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/PluralRuleSet.cs +++ b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/PluralRuleSet.cs @@ -1,22 +1,35 @@ -using System.Collections.Generic; -using Jeffijoe.MessageFormat.MetadataGenerator.Plural.Parsing.AST; +using Jeffijoe.MessageFormat.MetadataGenerator.Plural.Parsing.AST; +using System; +using System.Collections.Generic; namespace Jeffijoe.MessageFormat.MetadataGenerator.Plural.Parsing; /// -/// Represents multiple fully parsed documents of instances, each with a given type (i.e., 'cardinal' vs 'ordinals'). +/// Represents multiple fully parsed documents of instances, each with a given type (i.e., 'cardinal' vs 'ordinals'). /// public class PluralRuleSet { - private readonly List _allRules = []; + /// + /// Special CLDR locale ID to use as the default for inheritance. All locales can chain to this + /// during lookups. + /// + public const string RootLocale = "root"; /// - /// Indexes a set of instances by plural type & locale. + /// CLDR plural type attribute for the counting number ruleset. + /// Used to translate strings that contain a count, e.g., to pluralize nouns. /// - /// - /// Intended for runtime lookup of plural rules when formatting messages. - /// - private readonly Dictionary _byLocaleAndType = []; + public const string CardinalType = "cardinal"; + + /// + /// CLDR plural type attribute for the ordered number ruleset. + /// Used to translate strings containing ordinal numbers, e.g., "1st", "2nd", "3rd". + /// + public const string OrdinalType = "ordinal"; + + // Backing fields for the public properties below. + private readonly List _allRules = []; + private readonly Dictionary _indicesByLocale = new(StringComparer.OrdinalIgnoreCase); /// /// Gets the unique conditions that have been indexed. Can be used to generate unique helper functions @@ -25,34 +38,96 @@ public class PluralRuleSet public IReadOnlyList UniqueRules => this._allRules; /// - /// Walks indexes plural rules by locale + type, and gets the index into - /// for each tuple. + /// Maps normalized CLDR locale IDs to indices within + /// for their cardinal and ordinal rules, if defined. /// - public IEnumerable> RuleIndicesByKey => this._byLocaleAndType; + public IReadOnlyDictionary RuleIndicesByLocale => this._indicesByLocale; /// - /// Adds the given rule to our index under the given plural type. + /// Adds the given rule to our indices under the given plural type. /// /// e.g., 'cardinal' or 'ordinal'. /// The parsed rule. + /// Thrown when a nonstandard plural type is provided. public void Add(string pluralType, PluralRule rule) { this._allRules.Add(rule); int newRuleIndex = this._allRules.Count - 1; + int? cardinalIndex = null; + int? ordinalIndex = null; + if (pluralType == CardinalType) + { + cardinalIndex = newRuleIndex; + } + else if (pluralType == OrdinalType) + { + ordinalIndex = newRuleIndex; + } + else + { + throw new ArgumentOutOfRangeException(nameof(pluralType), pluralType, "Unexpected plural type"); + } + + // Loop over each locale for this rule and update our indices with the new value. + // If we've seen it before (for a different plural type), we'll update it in-place. foreach (var locale in rule.Locales) { - this._byLocaleAndType.Add( - new PluralRuleKey(PluralType: pluralType, Locale: locale), - newRuleIndex - ); + var normalized = this.NormalizeCldrLocale(locale); + + PluralRuleIndices newIndices; + if (this._indicesByLocale.TryGetValue(normalized, out var existingIndices)) + { + // Merge any previous indices we've observed for this locale + newIndices = existingIndices with + { + CardinalRuleIndex = cardinalIndex ?? existingIndices.CardinalRuleIndex, + OrdinalRuleIndex = ordinalIndex ?? existingIndices.OrdinalRuleIndex + }; + } + else + { + newIndices = new PluralRuleIndices( + CardinalRuleIndex: cardinalIndex, + OrdinalRuleIndex: ordinalIndex + ); + + } + + this._indicesByLocale[normalized] = newIndices; + if (normalized != locale) + { + this._indicesByLocale[locale] = newIndices; + } } } /// - /// Used to retrieve a specific . + /// Converts a CLDR locale ID to a normalized form for indexing. + /// + /// See the LDML spec + /// for an explanation of the forms that Unicode locale IDs can take. + /// + /// Notably, CLDR locale IDs use underscores as separators, while BCP 47 (which is the primary form + /// we expect as inputs at runtime) use dashes. + /// + /// + /// The return string is intended to be used for case-insensitive runtime lookup of input locales, + /// but the string itself is not strictly BCP 47 or CLDR compliant. For example, the CLDR 'root' + /// language is passed through instead of being remapped to 'und'. + /// + private string NormalizeCldrLocale(string cldrLocaleId) + { + return cldrLocaleId.Replace('_', '-'); + } + + /// + /// Helper type to represent the pluralization rules for a given locale, which may include both + /// cardinal and ordinal rules, or just one of the two. /// - /// e.g., 'cardinal' or 'ordinal'. - /// Two-letter language tag. - public readonly record struct PluralRuleKey(string PluralType, string Locale); + /// + /// For example, in CLDR 48.1, "pt_PT" has a defined plural rule but is expected to chain to "pt" + /// for ordinal lookup. + /// + public record PluralRuleIndices(int? CardinalRuleIndex, int? OrdinalRuleIndex); } \ No newline at end of file diff --git a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/SourceGeneration/PluralRulesMetadataGenerator.cs b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/SourceGeneration/PluralRulesMetadataGenerator.cs index 6ead4a6..caf0dec 100644 --- a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/SourceGeneration/PluralRulesMetadataGenerator.cs +++ b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/SourceGeneration/PluralRulesMetadataGenerator.cs @@ -17,8 +17,10 @@ public PluralRulesMetadataGenerator(PluralRuleSet rules) public string GenerateClass() { + WriteLine("#nullable enable"); WriteLine("using System;"); WriteLine("using System.Collections.Generic;"); + WriteLine("using System.Diagnostics.CodeAnalysis;"); WriteLine("namespace Jeffijoe.MessageFormat.Formatting.Formatters"); WriteLine("{"); @@ -28,6 +30,12 @@ public string GenerateClass() WriteLine("{"); AddIndent(); + // Export a constant for the normalized root locale to match the logic we're using internally. + // This way the rest of the lib's locale chaining can continue to work if we swap out + // normalization internally. + var rootRules = _rules.RuleIndicesByLocale[PluralRuleSet.RootLocale]; + WriteLine($"public static readonly string RootLocale = \"{PluralRuleSet.RootLocale}\";"); + // Generate a method for each unique rule, by index, that chooses the plural form // for a given input source number (the PluralContext) according to that rule. var uniqueRules = _rules.UniqueRules; @@ -47,19 +55,29 @@ public string GenerateClass() WriteLine(string.Empty); } - // Generate a static lookup dictionary of each (locale, plural type) to the corresponding rule method - // to use for that locale and type. - WriteLine("private static readonly Dictionary Pluralizers = new Dictionary()"); + // Generate a static lookup dictionary of locale (case-insensitive) to LocalePluralizers for that locale. + // e.g., + // en -> { + // Cardinal = Rule0, + // Ordinal = Rule1, + // }, + // [etc for other locales, with some null values for unmapped locales] + WriteLine("private static readonly Dictionary Pluralizers = new(StringComparer.OrdinalIgnoreCase)"); WriteLine("{"); AddIndent(); - foreach (var kvp in _rules.RuleIndicesByKey) + foreach (var kvp in _rules.RuleIndicesByLocale) { - string locale = kvp.Key.Locale; - string pluralType = kvp.Key.PluralType; - int ruleIdx = kvp.Value; - - WriteLine($"{{new PluralRuleKey(Locale: \"{locale}\", PluralType: \"{pluralType}\"), Rule{ruleIdx}}},"); + string locale = kvp.Key; + + // When index is defined, we want "Rule#" as a reference to the delegate generated above; + // otherwise we want null. + int? cardinalIdx = kvp.Value.CardinalRuleIndex; + int? ordinalIdx = kvp.Value.OrdinalRuleIndex; + string cardinalValue = cardinalIdx is not null ? $"Rule{cardinalIdx}" : "null"; + string ordinalValue = ordinalIdx is not null ? $"Rule{ordinalIdx}" : "null"; + + WriteLine($"{{\"{locale}\", new LocalePluralizers(Cardinal: {cardinalValue}, Ordinal: {ordinalValue})}},"); } DecreaseIndent(); @@ -68,14 +86,45 @@ public string GenerateClass() // Finally generate our public API to the rest of the library, that takes a locale and pluralType // and tries to retrieve an appropriate localizer to map an input source number to the form for the request. - WriteLine("public static partial bool TryGetRuleByLocale(PluralRuleKey key, out ContextPluralizer contextPluralizer)"); + WriteLine("public static partial bool TryGetCardinalRuleByLocale(string locale, [NotNullWhen(true)] out ContextPluralizer? contextPluralizer)"); WriteLine("{"); AddIndent(); - WriteLine("return Pluralizers.TryGetValue(key, out contextPluralizer);"); + WriteLine("if (!Pluralizers.TryGetValue(locale, out var pluralizersForLocale))"); + WriteLine("{"); + AddIndent(); + WriteLine("contextPluralizer = null;"); + WriteLine("return false;"); + DecreaseIndent(); + WriteLine("}"); + WriteLine("contextPluralizer = pluralizersForLocale.Cardinal;"); + WriteLine("return contextPluralizer != null;"); DecreaseIndent(); WriteLine("}"); + WriteLine(string.Empty); + + // Repeat the above for ordinal rules. + WriteLine("public static partial bool TryGetOrdinalRuleByLocale(string locale, [NotNullWhen(true)] out ContextPluralizer? contextPluralizer)"); + WriteLine("{"); + AddIndent(); + + WriteLine("if (!Pluralizers.TryGetValue(locale, out var pluralizersForLocale))"); + WriteLine("{"); + AddIndent(); + WriteLine("contextPluralizer = null;"); + WriteLine("return false;"); + DecreaseIndent(); + WriteLine("}"); + WriteLine("contextPluralizer = pluralizersForLocale.Ordinal;"); + WriteLine("return contextPluralizer != null;"); + + DecreaseIndent(); + WriteLine("}"); + + // Generate the helper record and then clean up. + WriteLine(string.Empty); + WriteLine("private record LocalePluralizers(ContextPluralizer? Cardinal, ContextPluralizer? Ordinal);"); DecreaseIndent(); WriteLine("}"); diff --git a/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/PluralFormatterTests.cs b/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/PluralFormatterTests.cs index 94cfe51..01d6d0b 100644 --- a/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/PluralFormatterTests.cs +++ b/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/PluralFormatterTests.cs @@ -42,13 +42,13 @@ public void Pluralize(double n, string expected) new ParsedArguments( new[] { - new KeyedBlock("zero", "nothing"), + new KeyedBlock("=0", "nothing"), new KeyedBlock("one", "just one"), new KeyedBlock("other", "wow") }, Array.Empty()); var request = new FormatterRequest(new Literal(1, 1, 1, 1, ""), "test", "plural", null); - var actual = subject.Pluralize(PluralRuleKey.Cardinal("en"), arguments, new PluralContext(Convert.ToDecimal(Convert.ToDouble(args[request.Variable]))), 0); + var actual = subject.Pluralize("en", PluralRulesMetadata.TryGetCardinalRuleByLocale, subject.Pluralizers, arguments, new PluralContext(Convert.ToDecimal(Convert.ToDouble(args[request.Variable]))), 0); Assert.Equal(expected, actual); } @@ -56,7 +56,7 @@ public void Pluralize(double n, string expected) /// The pluralize_defaults_to_en_locale_when_specified_locale_is_not_found /// [Fact] - public void Pluralize_defaults_to_en_locale_when_specified_locale_is_not_found() + public void Pluralize_defaults_to_root_locale_when_specified_locale_is_not_found() { var subject = new PluralFormatter(); var args = new Dictionary { { "test", 1 } }; @@ -64,14 +64,14 @@ public void Pluralize_defaults_to_en_locale_when_specified_locale_is_not_found() new ParsedArguments( new[] { - new KeyedBlock("zero", "nothing"), + new KeyedBlock("=0", "nothing"), new KeyedBlock("one", "just one"), - new KeyedBlock("other", "wow") + new KeyedBlock("other", "wow") // Root locale should resolve "1" to "other" }, Array.Empty()); var request = new FormatterRequest(new Literal(1, 1, 1, 1, ""), "test", "plural", null); - var actual = subject.Pluralize(PluralRuleKey.Cardinal("unknown"), arguments, new PluralContext(Convert.ToDecimal(Convert.ToDouble(args[request.Variable]))), 0); - Assert.Equal("just one", actual); + var actual = subject.Pluralize("unknown", PluralRulesMetadata.TryGetCardinalRuleByLocale, subject.Pluralizers, arguments, new PluralContext(Convert.ToDecimal(Convert.ToDouble(args[request.Variable]))), 0); + Assert.Equal("wow", actual); } /// @@ -86,12 +86,12 @@ public void Pluralize_throws_when_missing_other_block() new ParsedArguments( new[] { - new KeyedBlock("zero", "nothing"), + new KeyedBlock("=0", "nothing"), new KeyedBlock("one", "just one") }, Array.Empty()); var request = new FormatterRequest(new Literal(1, 1, 1, 1, ""), "test", "plural", null); - Assert.Throws(() => subject.Pluralize(PluralRuleKey.Cardinal("unknown"), arguments, new PluralContext(Convert.ToDecimal(Convert.ToDouble(args[request.Variable]))), 0)); + Assert.Throws(() => subject.Pluralize(PluralRulesMetadata.RootLocale, PluralRulesMetadata.TryGetCardinalRuleByLocale, subject.Pluralizers, arguments, new PluralContext(Convert.ToDecimal(Convert.ToDouble(args[request.Variable]))), 0)); } /// diff --git a/src/Jeffijoe.MessageFormat.Tests/MessageFormatterFullIntegrationTests.cs b/src/Jeffijoe.MessageFormat.Tests/MessageFormatterFullIntegrationTests.cs index 579be27..2c296b8 100644 --- a/src/Jeffijoe.MessageFormat.Tests/MessageFormatterFullIntegrationTests.cs +++ b/src/Jeffijoe.MessageFormat.Tests/MessageFormatterFullIntegrationTests.cs @@ -403,6 +403,24 @@ public void FormatMessage(string source, Dictionary args, strin { var subject = new MessageFormatter(false); + // Historically these tests relied on a default English pluralizer that mapped + // 0 to "zero"; adding that back in manually to ensure we maintain test coverage + // for multiple forms. + subject.Pluralizers!.Add("en", (number) => + { + if (number == 0) + { + return "zero"; + } else if (number == 1) + { + return "one"; + } + else + { + return "other"; + } + }); + // Warmup subject.FormatMessage(source, args); Benchmark.Start("Formatting", this.outputHelper); @@ -486,7 +504,7 @@ public void FormatMessage_with_reflection_overload() { var subject = new MessageFormatter(false); const string Pattern = "You have {UnreadCount, plural, " - + "zero {no unread messages}" + + "=0 {no unread messages}" + "one {just one unread message}" + "other {# unread messages}" + "} today."; var actual = subject.FormatMessage(Pattern, new { UnreadCount = 0 }); Assert.Equal("You have no unread messages today.", actual); @@ -513,7 +531,7 @@ public void ReadMe_test_to_make_sure_I_dont_look_like_a_fool() { var mf = new MessageFormatter(false); const string Str = @"You have {notifications, plural, - zero {no notifications} + =0 {no notifications} one {one notification} =42 {a universal amount of notifications} other {# notifications} @@ -528,7 +546,7 @@ public void ReadMe_test_to_make_sure_I_dont_look_like_a_fool() var mf = new MessageFormatter(false); const string Str = @"You {NUM_ADDS, plural, offset:1 =0{didnt add this to your profile} - zero{added this to your profile} + =1{added this to your profile} one{and one other person added this to their profile} other{and # others added this to their profiles} }."; @@ -636,7 +654,7 @@ public void ReadMe_test_to_make_sure_I_dont_look_like_a_fool() { var mf = new MessageFormatter(useCache: true, locale: "en"); - mf.Pluralizers![PluralRuleKey.Cardinal("en")] = n => + mf.Pluralizers!["en"] = n => { // ´n´ is the number being pluralized. // ReSharper disable once CompareOfFloatsByEqualityOperator diff --git a/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/GeneratedPluralRulesTests.cs b/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/GeneratedPluralRulesTests.cs index d243171..7852024 100644 --- a/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/GeneratedPluralRulesTests.cs +++ b/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/GeneratedPluralRulesTests.cs @@ -32,7 +32,7 @@ public void Uk_PluralizerTests(double n, string expected) }, new FormatterExtension[0]); var request = new FormatterRequest(new Literal(1, 1, 1, 1, ""), "test", PluralFormatter.PluralFunction, null); - var actual = subject.Pluralize(PluralRuleKey.Cardinal("uk"), arguments, new PluralContext(Convert.ToDecimal(Convert.ToDouble(args[request.Variable]))), 0); + var actual = subject.Pluralize("uk", PluralRulesMetadata.TryGetCardinalRuleByLocale, subject.Pluralizers, arguments, new PluralContext(Convert.ToDecimal(Convert.ToDouble(args[request.Variable]))), 0); Assert.Equal(expected, actual); } @@ -59,7 +59,7 @@ public void Ru_PluralizerTests(double n, string expected) }, new FormatterExtension[0]); var request = new FormatterRequest(new Literal(1, 1, 1, 1, ""), "test", PluralFormatter.PluralFunction, null); - var actual = subject.Pluralize(PluralRuleKey.Cardinal("ru"), arguments, new PluralContext(Convert.ToDecimal(args[request.Variable])), 0); + var actual = subject.Pluralize("ru", PluralRulesMetadata.TryGetCardinalRuleByLocale, subject.Pluralizers, arguments, new PluralContext(Convert.ToDecimal(args[request.Variable])), 0); Assert.Equal(expected, actual); } @@ -87,7 +87,7 @@ public void En_Cardinal_PluralizerTests(double n, string expected) }, new FormatterExtension[0]); var request = new FormatterRequest(new Literal(1, 1, 1, 1, ""), "test", PluralFormatter.PluralFunction, null); - var actual = subject.Pluralize(PluralRuleKey.Cardinal("en"), arguments, new PluralContext(Convert.ToDecimal(args[request.Variable])), 0); + var actual = subject.Pluralize("en", PluralRulesMetadata.TryGetCardinalRuleByLocale, subject.Pluralizers, arguments, new PluralContext(Convert.ToDecimal(args[request.Variable])), 0); Assert.Equal(expected, actual); } @@ -117,7 +117,7 @@ public void En_Ordinal_PluralizerTests(double n, string expected) }, new FormatterExtension[0]); var request = new FormatterRequest(new Literal(1, 1, 1, 1, ""), "test", PluralFormatter.OrdinalFunction, null); - var pluralized = subject.Pluralize(PluralRuleKey.Ordinal("en"), arguments, new PluralContext(Convert.ToDecimal(args[request.Variable])), 0); + var pluralized = subject.Pluralize("en", PluralRulesMetadata.TryGetOrdinalRuleByLocale, subject.OrdinalPluralizers, arguments, new PluralContext(Convert.ToDecimal(args[request.Variable])), 0); var actual = subject.ReplaceNumberLiterals(pluralized, n); Assert.Equal(expected, actual); } diff --git a/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/PluralMetadataClassGeneratorTests.cs b/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/PluralMetadataClassGeneratorTests.cs index 5779466..7060ae2 100644 --- a/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/PluralMetadataClassGeneratorTests.cs +++ b/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/PluralMetadataClassGeneratorTests.cs @@ -13,7 +13,7 @@ public void CanGenerateClassFromRules() { var rules = new[] { - new PluralRule(new[] {"en", "uk"}, + new PluralRule(new[] {"root", "en", "uk"}, new[] { new Condition("one", string.Empty, new [] @@ -24,7 +24,7 @@ public void CanGenerateClassFromRules() }) }) }), - new PluralRule(new[] {"en"}, + new PluralRule(new[] {"root", "en", "pt_PT"}, new[] { new Condition("many", string.Empty, new [] @@ -46,12 +46,15 @@ public void CanGenerateClassFromRules() var actual = generator.GenerateClass(); var expected = @" +#nullable enable using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; namespace Jeffijoe.MessageFormat.Formatting.Formatters { internal static partial class PluralRulesMetadata { + public static readonly string RootLocale = ""root""; private static string Rule0(PluralContext context) { if ((context.N == 3)) @@ -68,17 +71,38 @@ private static string Rule1(PluralContext context) return ""other""; } - private static readonly Dictionary Pluralizers = new Dictionary() + private static readonly Dictionary Pluralizers = new(StringComparer.OrdinalIgnoreCase) { - {new PluralRuleKey(Locale: ""en"", PluralType: ""cardinal""), Rule0}, - {new PluralRuleKey(Locale: ""uk"", PluralType: ""cardinal""), Rule0}, - {new PluralRuleKey(Locale: ""en"", PluralType: ""ordinal""), Rule1}, + {""root"", new LocalePluralizers(Cardinal: Rule0, Ordinal: Rule1)}, + {""en"", new LocalePluralizers(Cardinal: Rule0, Ordinal: Rule1)}, + {""uk"", new LocalePluralizers(Cardinal: Rule0, Ordinal: null)}, + {""pt-PT"", new LocalePluralizers(Cardinal: null, Ordinal: Rule1)}, + {""pt_PT"", new LocalePluralizers(Cardinal: null, Ordinal: Rule1)}, }; - public static partial bool TryGetRuleByLocale(PluralRuleKey key, out ContextPluralizer contextPluralizer) + public static partial bool TryGetCardinalRuleByLocale(string locale, [NotNullWhen(true)] out ContextPluralizer? contextPluralizer) { - return Pluralizers.TryGetValue(key, out contextPluralizer); + if (!Pluralizers.TryGetValue(locale, out var pluralizersForLocale)) + { + contextPluralizer = null; + return false; + } + contextPluralizer = pluralizersForLocale.Cardinal; + return contextPluralizer != null; } + + public static partial bool TryGetOrdinalRuleByLocale(string locale, [NotNullWhen(true)] out ContextPluralizer? contextPluralizer) + { + if (!Pluralizers.TryGetValue(locale, out var pluralizersForLocale)) + { + contextPluralizer = null; + return false; + } + contextPluralizer = pluralizersForLocale.Ordinal; + return contextPluralizer != null; + } + + private record LocalePluralizers(ContextPluralizer? Cardinal, ContextPluralizer? Ordinal); } } ".TrimStart(); diff --git a/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralFormatter.cs b/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralFormatter.cs index 7932268..f61319d 100644 --- a/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralFormatter.cs +++ b/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralFormatter.cs @@ -15,16 +15,6 @@ namespace Jeffijoe.MessageFormat.Formatting.Formatters; /// public class PluralFormatter : BaseFormatter, IFormatter { - /// - /// CLDR plural type attribute for counting number ruleset. - /// - internal const string CardinalType = "cardinal"; - - /// - /// CLDR plural type attribute for ordered number ruleset. - /// - internal const string OrdinalType = "ordinal"; - /// /// ICU MessageFormat function name for "default" pluralization, based on cardinal numbers. /// @@ -36,14 +26,9 @@ public class PluralFormatter : BaseFormatter, IFormatter internal const string OrdinalFunction = "selectordinal"; /// - /// Maps supported parser names to CLDR plural types. - /// The plural language rule schema is identical between these types and we just need to pick the correct set. + /// Delegate type to try to look up a specific plural rule for a given locale. /// - private static readonly Dictionary CldrTypeForFunction = new() - { - { PluralFunction, CardinalType }, - { OrdinalFunction, OrdinalType } - }; + internal delegate bool TryGetRuleForLocale(string locale, [NotNullWhen(true)] out ContextPluralizer? contextPluralizer); #region Constructors and Destructors @@ -52,8 +37,8 @@ public class PluralFormatter : BaseFormatter, IFormatter /// public PluralFormatter() { - this.Pluralizers = new Dictionary(); - this.AddStandardPluralizers(); + this.Pluralizers = new Dictionary(); + this.OrdinalPluralizers = new Dictionary(); } #endregion @@ -66,12 +51,20 @@ public PluralFormatter() public bool VariableMustExist => true; /// - /// Gets the pluralizers dictionary. Key is the locale and plural type. + /// Gets the pluralizers dictionary to use for cardinal numbers. Key is the locale. /// /// /// The pluralizers. /// - public IDictionary Pluralizers { get; private set; } + public IDictionary Pluralizers { get; private set; } + + /// + /// Gets the pluralizers dictionary to use for ordinal numbers. Key is the locale. + /// + /// + /// The ordinal pluralizers. + /// + public IDictionary OrdinalPluralizers { get; private set; } #endregion @@ -93,7 +86,7 @@ public bool CanFormat(FormatterRequest request) return false; } - return CldrTypeForFunction.ContainsKey(request.FormatterName); + return request.FormatterName == PluralFunction || request.FormatterName == OrdinalFunction; } /// @@ -137,14 +130,28 @@ public string Format(string locale, // Get CLDR plural ruleset from request. // CanFormat() should have guaranteed this is valid, but we'll be defensive just in case. - if (!CldrTypeForFunction.TryGetValue(request.FormatterName ?? string.Empty, out var pluralType)) + TryGetRuleForLocale cldrPluralLookup; + IDictionary customLookup; + if (request.FormatterName == PluralFunction) + { + cldrPluralLookup = PluralRulesMetadata.TryGetCardinalRuleByLocale; + customLookup = this.Pluralizers; + } + else if (request.FormatterName == OrdinalFunction) + { + cldrPluralLookup = PluralRulesMetadata.TryGetOrdinalRuleByLocale; + customLookup = this.OrdinalPluralizers; + } + else { throw new MessageFormatterException($"Unsupported plural formatter name: {request.FormatterName}"); } var ctx = CreatePluralContext(value, offset); var pluralized = this.Pluralize( - new PluralRuleKey(PluralType: pluralType, Locale: locale), + locale, + cldrPluralLookup, + customLookup, arguments, ctx, offset); @@ -160,8 +167,15 @@ public string Format(string locale, /// /// Returns the correct plural block. /// - /// - /// The locale and pluralType. + /// + /// The locale. + /// + /// + /// Delegate to retrieve a for a given locale. + /// + /// + /// Dictionary to retrieve a for a given locale, to be evaluated + /// before resolving against . /// /// /// The parsed arguments string. @@ -176,27 +190,38 @@ public string Format(string locale, /// The . /// /// - /// The 'other' option was not found in pattern. + /// The 'other' option was not found in pattern, or is missing + /// both the provided locale and the CLDR root locale. /// [SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1126:PrefixCallsCorrectly", Justification = "Reviewed. Suppression is OK here.")] - internal string Pluralize(PluralRuleKey ruleKey, ParsedArguments arguments, PluralContext context, double offset) + internal string Pluralize( + string locale, + TryGetRuleForLocale cldrPluralLookup, + IDictionary customLookup, + ParsedArguments arguments, + PluralContext context, + double offset) { string pluralForm; - if (this.Pluralizers.TryGetValue(ruleKey, out var pluralizer)) + if (customLookup.TryGetValue(locale, out var pluralizer)) { pluralForm = pluralizer(context.Number); } - else if (PluralRulesMetadata.TryGetRuleByLocale(ruleKey, out var contextPluralizer)) + else if (cldrPluralLookup(locale, out var contextPluralizer)) { pluralForm = contextPluralizer(context); } + else if (cldrPluralLookup(PluralRulesMetadata.RootLocale, out var rootPluralizer)) + { + pluralForm = rootPluralizer(context); + } else { - pluralForm = this.Pluralizers[new PluralRuleKey(Locale: "en", PluralType: ruleKey.PluralType)](context.Number); + throw new MessageFormatterException($"Could not find either locale {locale} or root locale {PluralRulesMetadata.RootLocale} in specified plural rule lookup"); } - - KeyedBlock? other = null; + + KeyedBlock? other = null; foreach (var keyedBlock in arguments.KeyedBlocks) { if (keyedBlock.Key == OtherKey) @@ -332,58 +357,6 @@ internal string ReplaceNumberLiterals(string pluralized, double n) } } - /// - /// Adds the standard pluralizers. - /// - private void AddStandardPluralizers() - { - this.Pluralizers.Add( - PluralRuleKey.Cardinal("en"), - n => - { - // ReSharper disable CompareOfFloatsByEqualityOperator - if (n == 0) - { - return "zero"; - } - - if (n == 1) - { - return "one"; - } - - // ReSharper restore CompareOfFloatsByEqualityOperator - return "other"; - } - ); - this.Pluralizers.Add( - PluralRuleKey.Ordinal("en"), - n => - { - // e.g., 1st - if (n % 10 == 1 && n % 100 != 11) - { - return "one"; - } - - // e.g., 2nd - if (n % 10 == 2 && n % 100 != 12) - { - return "two"; - } - - // e.g., 3rd - if (n % 10 == 3 && n % 100 != 13) - { - return "few"; - } - - // e.g., 4th, 11th, etc - return "other"; - } - ); - } - /// /// Creates a for the specified value. /// diff --git a/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralRuleKey.cs b/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralRuleKey.cs deleted file mode 100644 index 60a5fbb..0000000 --- a/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralRuleKey.cs +++ /dev/null @@ -1,19 +0,0 @@ -namespace Jeffijoe.MessageFormat.Formatting.Formatters; - -/// -/// Used to retrieve a specific . -/// -/// e.g., 'cardinal' or 'ordinal'. -/// Two-letter language tag. -public readonly record struct PluralRuleKey(string PluralType, string Locale) -{ - /// - /// Helper to generate a cardinal rule look up for a locale, suitable for the 'plural' MessageFormat function. - /// - public static PluralRuleKey Cardinal(string locale) => new(PluralType: PluralFormatter.CardinalType, Locale: locale); - - /// - /// Helper to generate an ordinal rule look up for a locale, suitable for the 'selectordinal' MessageFormat function. - /// - public static PluralRuleKey Ordinal(string locale) => new(PluralType: PluralFormatter.OrdinalType, Locale: locale); -} \ No newline at end of file diff --git a/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralRulesMetadata.cs b/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralRulesMetadata.cs index ccf5bb2..3c18382 100644 --- a/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralRulesMetadata.cs +++ b/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralRulesMetadata.cs @@ -5,5 +5,7 @@ namespace Jeffijoe.MessageFormat.Formatting.Formatters; [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] internal static partial class PluralRulesMetadata { - public static partial bool TryGetRuleByLocale(PluralRuleKey key, out ContextPluralizer contextPluralizer); + public static partial bool TryGetCardinalRuleByLocale(string locale, out ContextPluralizer? contextPluralizer); + + public static partial bool TryGetOrdinalRuleByLocale(string locale, out ContextPluralizer? contextPluralizer); } \ No newline at end of file diff --git a/src/Jeffijoe.MessageFormat/MessageFormatter.cs b/src/Jeffijoe.MessageFormat/MessageFormatter.cs index 4c37b78..da147a2 100644 --- a/src/Jeffijoe.MessageFormat/MessageFormatter.cs +++ b/src/Jeffijoe.MessageFormat/MessageFormatter.cs @@ -145,12 +145,12 @@ public IFormatterLibrary Formatters public string Locale { get; set; } /// - /// Gets the pluralizers dictionary from the , if set. Key is the locale, then the plural type. + /// Gets the pluralizers dictionary from the , if set. Key is the locale. /// /// /// The pluralizers, or null if the plural formatter has not been added. /// - public IDictionary? Pluralizers + public IDictionary? Pluralizers { get { @@ -159,6 +159,21 @@ public IDictionary? Pluralizers } } + /// + /// Gets the ordinal number pluralizers dictionary from the , if set. Key is the locale. + /// + /// + /// The pluralizers, or null if the plural formatter has not been added. + /// + public IDictionary? OrdinalPluralizers + { + get + { + var pluralFormatter = this.Formatters.OfType().FirstOrDefault(); + return pluralFormatter?.OrdinalPluralizers; + } + } + #endregion #region Public Methods and Operators From e9cea37c6ab561588a07a71ddf24683a66d874bf Mon Sep 17 00:00:00 2001 From: Steven Fuqua Date: Mon, 16 Feb 2026 22:45:21 -0800 Subject: [PATCH 88/98] Locale fallback --- .../Helpers/LocaleHelperTests.cs | 61 +++++++++++++++++++ .../GeneratedPluralRulesTests.cs | 34 +++++++++-- .../Formatting/Formatters/PluralFormatter.cs | 26 +++++--- .../Helpers/LocaleHelper.cs | 57 +++++++++++++++++ 4 files changed, 164 insertions(+), 14 deletions(-) create mode 100644 src/Jeffijoe.MessageFormat.Tests/Helpers/LocaleHelperTests.cs create mode 100644 src/Jeffijoe.MessageFormat/Helpers/LocaleHelper.cs diff --git a/src/Jeffijoe.MessageFormat.Tests/Helpers/LocaleHelperTests.cs b/src/Jeffijoe.MessageFormat.Tests/Helpers/LocaleHelperTests.cs new file mode 100644 index 0000000..e785d5f --- /dev/null +++ b/src/Jeffijoe.MessageFormat.Tests/Helpers/LocaleHelperTests.cs @@ -0,0 +1,61 @@ +using Jeffijoe.MessageFormat.Formatting.Formatters; +using Jeffijoe.MessageFormat.Helpers; +using System.Linq; +using Xunit; + +namespace Jeffijoe.MessageFormat.Tests.Helpers; + +/// +/// The locale helper tests. +/// +public class LocaleHelperTests +{ + /// + /// Tests that both '-' and '_' are supported when extracting the base language. + /// + [Fact] + public void GetInheritanceChain_HandlesBothSeparators() + { + Assert.Equal( + ["en-US", "en", PluralRulesMetadata.RootLocale], + LocaleHelper.GetInheritanceChain("en-US").ToList() + ); + + Assert.Equal( + ["en_US", "en", PluralRulesMetadata.RootLocale], + LocaleHelper.GetInheritanceChain("en_US").ToList() + ); + } + + /// + /// Confirms that our implementation only returns the original locale, + /// the language, and the root. + /// + /// + /// This is a perf optimization given the CLDR data set we're using. + /// + [Fact] + public void GetInheritanceChain_SkipsIntermediateTags() + { + Assert.Equal( + ["th-TH-u-nu-thai", "th", PluralRulesMetadata.RootLocale], + LocaleHelper.GetInheritanceChain("th-TH-u-nu-thai").ToList() + ); + } + + [Theory] + [InlineData("")] + [InlineData("-")] + [InlineData("_")] + [InlineData("x")] + [InlineData("x-")] + [InlineData("x-test")] + [InlineData("i-test")] + public void GetInheritanceChain_HandlesBadInput(string input) + { + Assert.Equal( + [PluralRulesMetadata.RootLocale], + LocaleHelper.GetInheritanceChain(input).ToList() + ); + } +} \ No newline at end of file diff --git a/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/GeneratedPluralRulesTests.cs b/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/GeneratedPluralRulesTests.cs index 7852024..6f154b6 100644 --- a/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/GeneratedPluralRulesTests.cs +++ b/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/GeneratedPluralRulesTests.cs @@ -1,7 +1,9 @@ using System; using System.Collections.Generic; +using System.Linq; using Jeffijoe.MessageFormat.Formatting; using Jeffijoe.MessageFormat.Formatting.Formatters; +using Jeffijoe.MessageFormat.Helpers; using Jeffijoe.MessageFormat.Parsing; using Xunit; @@ -69,7 +71,7 @@ public void Ru_PluralizerTests(double n, string expected) [InlineData(101, "days")] [InlineData(102, "days")] [InlineData(105, "days")] - public void En_Cardinal_PluralizerTests(double n, string expected) + public void EnUS_Cardinal_PluralizerTests(double n, string expected) { var subject = new PluralFormatter(); subject.Pluralizers.Clear(); @@ -87,7 +89,7 @@ public void En_Cardinal_PluralizerTests(double n, string expected) }, new FormatterExtension[0]); var request = new FormatterRequest(new Literal(1, 1, 1, 1, ""), "test", PluralFormatter.PluralFunction, null); - var actual = subject.Pluralize("en", PluralRulesMetadata.TryGetCardinalRuleByLocale, subject.Pluralizers, arguments, new PluralContext(Convert.ToDecimal(args[request.Variable])), 0); + var actual = subject.Pluralize("en_US", PluralRulesMetadata.TryGetCardinalRuleByLocale, subject.Pluralizers, arguments, new PluralContext(Convert.ToDecimal(args[request.Variable])), 0); Assert.Equal(expected, actual); } @@ -100,7 +102,7 @@ public void En_Cardinal_PluralizerTests(double n, string expected) [InlineData(9, "9th")] [InlineData(11, "11th")] [InlineData(21, "21st")] - public void En_Ordinal_PluralizerTests(double n, string expected) + public void EnUS_Ordinal_PluralizerTests(double n, string expected) { var subject = new PluralFormatter(); subject.Pluralizers.Clear(); @@ -117,8 +119,32 @@ public void En_Ordinal_PluralizerTests(double n, string expected) }, new FormatterExtension[0]); var request = new FormatterRequest(new Literal(1, 1, 1, 1, ""), "test", PluralFormatter.OrdinalFunction, null); - var pluralized = subject.Pluralize("en", PluralRulesMetadata.TryGetOrdinalRuleByLocale, subject.OrdinalPluralizers, arguments, new PluralContext(Convert.ToDecimal(args[request.Variable])), 0); + var pluralized = subject.Pluralize("en-US", PluralRulesMetadata.TryGetOrdinalRuleByLocale, subject.OrdinalPluralizers, arguments, new PluralContext(Convert.ToDecimal(args[request.Variable])), 0); var actual = subject.ReplaceNumberLiterals(pluralized, n); Assert.Equal(expected, actual); } + + [Fact] + public void RootLocale_MatchesRules() + { + Assert.True(PluralRulesMetadata.TryGetCardinalRuleByLocale(PluralRulesMetadata.RootLocale, out _)); + Assert.True(PluralRulesMetadata.TryGetOrdinalRuleByLocale(PluralRulesMetadata.RootLocale, out _)); + } + + /// + /// Tests to confirm that separators normalize properly in the data, + /// and that language lookups are case insensitive. + /// + [Fact] + public void Fallback_PluralizerTests() + { + Assert.True(PluralRulesMetadata.TryGetCardinalRuleByLocale("kok_Latn", out _)); + Assert.True(PluralRulesMetadata.TryGetCardinalRuleByLocale("pt-PT", out _)); + Assert.True(PluralRulesMetadata.TryGetCardinalRuleByLocale("pt-pt", out _)); + Assert.True(PluralRulesMetadata.TryGetCardinalRuleByLocale("PT_PT", out _)); + Assert.True(PluralRulesMetadata.TryGetCardinalRuleByLocale("pT", out _)); + + Assert.True(PluralRulesMetadata.TryGetOrdinalRuleByLocale("kok_Latn", out _)); + Assert.False(PluralRulesMetadata.TryGetOrdinalRuleByLocale("pt-PT", out _)); + } } \ No newline at end of file diff --git a/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralFormatter.cs b/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralFormatter.cs index f61319d..93f3c88 100644 --- a/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralFormatter.cs +++ b/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralFormatter.cs @@ -3,6 +3,7 @@ // Author: Jeff Hansen // Copyright (C) Jeff Hansen 2014. All rights reserved. +using Jeffijoe.MessageFormat.Helpers; using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; @@ -203,25 +204,30 @@ internal string Pluralize( PluralContext context, double offset) { - string pluralForm; + string? pluralForm = null; if (customLookup.TryGetValue(locale, out var pluralizer)) { pluralForm = pluralizer(context.Number); } - else if (cldrPluralLookup(locale, out var contextPluralizer)) - { - pluralForm = contextPluralizer(context); - } - else if (cldrPluralLookup(PluralRulesMetadata.RootLocale, out var rootPluralizer)) + else { - pluralForm = rootPluralizer(context); + foreach (var candidate in LocaleHelper.GetInheritanceChain(locale)) + { + if (cldrPluralLookup(candidate, out var contextPluralizer)) + { + pluralForm = contextPluralizer(context); + break; + } + } } - else + + if (pluralForm is null) { - throw new MessageFormatterException($"Could not find either locale {locale} or root locale {PluralRulesMetadata.RootLocale} in specified plural rule lookup"); + // GetInheritanceChain should resolve the root CLDR locale as a last attempt, so this should never happen... + throw new MessageFormatterException($"Could not find locale {locale} in specified plural rule lookup"); } - KeyedBlock? other = null; + KeyedBlock? other = null; foreach (var keyedBlock in arguments.KeyedBlocks) { if (keyedBlock.Key == OtherKey) diff --git a/src/Jeffijoe.MessageFormat/Helpers/LocaleHelper.cs b/src/Jeffijoe.MessageFormat/Helpers/LocaleHelper.cs new file mode 100644 index 0000000..a5a31cc --- /dev/null +++ b/src/Jeffijoe.MessageFormat/Helpers/LocaleHelper.cs @@ -0,0 +1,57 @@ +using Jeffijoe.MessageFormat.Formatting.Formatters; +using System.Collections.Generic; + +namespace Jeffijoe.MessageFormat.Helpers; + +/// +/// Helpers for working with locale strings. +/// +internal class LocaleHelper +{ + /// + /// Partial implementation of locale inheritance + /// from the LDML spec. + /// + /// Given an input locale in BCP 47 format, yields back various strings to use as lookups in CLDR data. + /// + /// + /// This function doesn't perform any canonicalization of input or fully implement the LDML spec. + /// It first yields the input as-is, then the base language tag, then the CLDR "root" value. + /// + /// This is because at the time of authorship, the only lookups needed by this library are for CLDR plurals, + /// which almost exclusively use languages without subtags. + /// + /// + /// Given "language-Script-REGION", yields: + /// - language-Script-REGION + /// - language + /// - root + /// + /// A BCP 47 locale tag + public static IEnumerable GetInheritanceChain(string locale) + { + // 0 or 1 characters do not form a valid language ID, so we can skip those + // Also skip x- and i- as those BCP 47 tags will never match CLDR and should + // only resolve to 'root'. + if (locale.Length >= 2 && locale[1] != '-') + { + yield return locale; + } + + // If the length is 2, we don't have any subtags for valid input + if (locale.Length >= 3 && locale[1] != '-') + { + // Find the first separator character, Substring to that, and break + for (int i = 2; i < locale.Length; i++) + { + if (locale[i] == '_' || locale[i] == '-') + { + yield return locale.Substring(0, i); + break; + } + } + } + + yield return PluralRulesMetadata.RootLocale; + } +} From e464a9a452a29278c80f611620e27612c33dc828 Mon Sep 17 00:00:00 2001 From: Steven Fuqua Date: Mon, 16 Feb 2026 22:51:42 -0800 Subject: [PATCH 89/98] Cleanup formatting --- .../Plural/Parsing/PluralParser.cs | 3 +-- .../MetadataGenerator/GeneratedPluralRulesTests.cs | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/PluralParser.cs b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/PluralParser.cs index 793e79d..1656357 100644 --- a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/PluralParser.cs +++ b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/Parsing/PluralParser.cs @@ -29,7 +29,7 @@ public PluralRuleSet Parse() } /// - /// Parses the represented XML document and merges the rules into the given . + /// Parses the represented XML document and merges the rules into the given . /// /// /// If the CLDR XML is missing expected attributes. @@ -76,7 +76,6 @@ public void ParseInto(PluralRuleSet ruleIndex) } var conditions = new List(); - foreach (XmlNode condition in rule.ChildNodes) { if (condition.Name == "pluralRule") diff --git a/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/GeneratedPluralRulesTests.cs b/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/GeneratedPluralRulesTests.cs index 6f154b6..87182c1 100644 --- a/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/GeneratedPluralRulesTests.cs +++ b/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/GeneratedPluralRulesTests.cs @@ -81,8 +81,7 @@ public void EnUS_Cardinal_PluralizerTests(double n, string expected) new ParsedArguments( new[] { - // 'zero' is a red herring to confirm CLDR rules are used instead of built-in; - // CLDR does not specify an English 'zero' form, so 0 should fallthrough to 'other'. + // Regression test to ensure 0 does not match 'zero' for English new KeyedBlock("zero", "FAIL"), new KeyedBlock("one", "day"), new KeyedBlock("other", "days") From 13c6ae2a24816a123eb0b0b0801636d76cb79d32 Mon Sep 17 00:00:00 2001 From: Steven Fuqua Date: Tue, 17 Feb 2026 10:03:47 -0800 Subject: [PATCH 90/98] Rename properties --- README.md | 8 ++++---- .../Formatters/PluralFormatterTests.cs | 6 +++--- .../MessageFormatterFullIntegrationTests.cs | 4 ++-- .../GeneratedPluralRulesTests.cs | 14 +++++++------- .../Formatting/Formatters/PluralFormatter.cs | 8 ++++---- src/Jeffijoe.MessageFormat/MessageFormatter.cs | 18 ++++++++++++++---- 6 files changed, 34 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index c5ece6f..52ad3dd 100644 --- a/README.md +++ b/README.md @@ -137,10 +137,10 @@ var message = formatter.FormatMessage("{value, number, $0.0}", new { value = 23 ## Adding your own pluralizer functions > Since MessageFormat 5.0, pluralizers based on the [official CLDR data][plural-cldr] ship -> with the package, so this is no longer needed. +> with the package, so this is no longer needed except when overriding specific custom locales. Same thing as with [MessageFormat.js][0], you can add your own pluralizer function. -The `Pluralizers` property is a `IDictionary` that starts empty, along +The `CardinalPluralizers` property is a `IDictionary` that starts empty, along with `OrdinalPluralizers` for ordinal numbers. Adding to these Dictionaries will take precedence over the CLDR data for exact matches on @@ -148,7 +148,7 @@ the input locales. ````csharp var mf = new MessageFormatter(); -mf.Pluralizers.Add("", n => { +mf.CardinalPluralizers.Add("", n => { // ´n´ is the number being pluralized. if(n == 0) return "zero"; @@ -163,7 +163,7 @@ you may use in your pluralization block. ````csharp var mf = new MessageFormatter(true, "en"); // true = use cache -mf.Pluralizers["en"] = n => +mf.CardinalPluralizers["en"] = n => { // ´n´ is the number being pluralized. if (n == 1) diff --git a/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/PluralFormatterTests.cs b/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/PluralFormatterTests.cs index 01d6d0b..9ad7cc7 100644 --- a/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/PluralFormatterTests.cs +++ b/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/PluralFormatterTests.cs @@ -48,7 +48,7 @@ public void Pluralize(double n, string expected) }, Array.Empty()); var request = new FormatterRequest(new Literal(1, 1, 1, 1, ""), "test", "plural", null); - var actual = subject.Pluralize("en", PluralRulesMetadata.TryGetCardinalRuleByLocale, subject.Pluralizers, arguments, new PluralContext(Convert.ToDecimal(Convert.ToDouble(args[request.Variable]))), 0); + var actual = subject.Pluralize("en", PluralRulesMetadata.TryGetCardinalRuleByLocale, subject.CardinalPluralizers, arguments, new PluralContext(Convert.ToDecimal(Convert.ToDouble(args[request.Variable]))), 0); Assert.Equal(expected, actual); } @@ -70,7 +70,7 @@ public void Pluralize_defaults_to_root_locale_when_specified_locale_is_not_found }, Array.Empty()); var request = new FormatterRequest(new Literal(1, 1, 1, 1, ""), "test", "plural", null); - var actual = subject.Pluralize("unknown", PluralRulesMetadata.TryGetCardinalRuleByLocale, subject.Pluralizers, arguments, new PluralContext(Convert.ToDecimal(Convert.ToDouble(args[request.Variable]))), 0); + var actual = subject.Pluralize("unknown", PluralRulesMetadata.TryGetCardinalRuleByLocale, subject.CardinalPluralizers, arguments, new PluralContext(Convert.ToDecimal(Convert.ToDouble(args[request.Variable]))), 0); Assert.Equal("wow", actual); } @@ -91,7 +91,7 @@ public void Pluralize_throws_when_missing_other_block() }, Array.Empty()); var request = new FormatterRequest(new Literal(1, 1, 1, 1, ""), "test", "plural", null); - Assert.Throws(() => subject.Pluralize(PluralRulesMetadata.RootLocale, PluralRulesMetadata.TryGetCardinalRuleByLocale, subject.Pluralizers, arguments, new PluralContext(Convert.ToDecimal(Convert.ToDouble(args[request.Variable]))), 0)); + Assert.Throws(() => subject.Pluralize(PluralRulesMetadata.RootLocale, PluralRulesMetadata.TryGetCardinalRuleByLocale, subject.CardinalPluralizers, arguments, new PluralContext(Convert.ToDecimal(Convert.ToDouble(args[request.Variable]))), 0)); } /// diff --git a/src/Jeffijoe.MessageFormat.Tests/MessageFormatterFullIntegrationTests.cs b/src/Jeffijoe.MessageFormat.Tests/MessageFormatterFullIntegrationTests.cs index 2c296b8..ddb6497 100644 --- a/src/Jeffijoe.MessageFormat.Tests/MessageFormatterFullIntegrationTests.cs +++ b/src/Jeffijoe.MessageFormat.Tests/MessageFormatterFullIntegrationTests.cs @@ -406,7 +406,7 @@ public void FormatMessage(string source, Dictionary args, strin // Historically these tests relied on a default English pluralizer that mapped // 0 to "zero"; adding that back in manually to ensure we maintain test coverage // for multiple forms. - subject.Pluralizers!.Add("en", (number) => + subject.CardinalPluralizers!.Add("en", (number) => { if (number == 0) { @@ -654,7 +654,7 @@ public void ReadMe_test_to_make_sure_I_dont_look_like_a_fool() { var mf = new MessageFormatter(useCache: true, locale: "en"); - mf.Pluralizers!["en"] = n => + mf.CardinalPluralizers!["en"] = n => { // ´n´ is the number being pluralized. // ReSharper disable once CompareOfFloatsByEqualityOperator diff --git a/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/GeneratedPluralRulesTests.cs b/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/GeneratedPluralRulesTests.cs index 87182c1..75fbc0d 100644 --- a/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/GeneratedPluralRulesTests.cs +++ b/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/GeneratedPluralRulesTests.cs @@ -20,7 +20,7 @@ public class GeneratedPluralRulesTests public void Uk_PluralizerTests(double n, string expected) { var subject = new PluralFormatter(); - subject.Pluralizers.Clear(); + subject.CardinalPluralizers.Clear(); var args = new Dictionary { { "test", n } }; var arguments = @@ -34,7 +34,7 @@ public void Uk_PluralizerTests(double n, string expected) }, new FormatterExtension[0]); var request = new FormatterRequest(new Literal(1, 1, 1, 1, ""), "test", PluralFormatter.PluralFunction, null); - var actual = subject.Pluralize("uk", PluralRulesMetadata.TryGetCardinalRuleByLocale, subject.Pluralizers, arguments, new PluralContext(Convert.ToDecimal(Convert.ToDouble(args[request.Variable]))), 0); + var actual = subject.Pluralize("uk", PluralRulesMetadata.TryGetCardinalRuleByLocale, subject.CardinalPluralizers, arguments, new PluralContext(Convert.ToDecimal(Convert.ToDouble(args[request.Variable]))), 0); Assert.Equal(expected, actual); } @@ -47,7 +47,7 @@ public void Uk_PluralizerTests(double n, string expected) public void Ru_PluralizerTests(double n, string expected) { var subject = new PluralFormatter(); - subject.Pluralizers.Clear(); + subject.CardinalPluralizers.Clear(); var args = new Dictionary { { "test", n } }; var arguments = @@ -61,7 +61,7 @@ public void Ru_PluralizerTests(double n, string expected) }, new FormatterExtension[0]); var request = new FormatterRequest(new Literal(1, 1, 1, 1, ""), "test", PluralFormatter.PluralFunction, null); - var actual = subject.Pluralize("ru", PluralRulesMetadata.TryGetCardinalRuleByLocale, subject.Pluralizers, arguments, new PluralContext(Convert.ToDecimal(args[request.Variable])), 0); + var actual = subject.Pluralize("ru", PluralRulesMetadata.TryGetCardinalRuleByLocale, subject.CardinalPluralizers, arguments, new PluralContext(Convert.ToDecimal(args[request.Variable])), 0); Assert.Equal(expected, actual); } @@ -74,7 +74,7 @@ public void Ru_PluralizerTests(double n, string expected) public void EnUS_Cardinal_PluralizerTests(double n, string expected) { var subject = new PluralFormatter(); - subject.Pluralizers.Clear(); + subject.CardinalPluralizers.Clear(); var args = new Dictionary { { "test", n } }; var arguments = @@ -88,7 +88,7 @@ public void EnUS_Cardinal_PluralizerTests(double n, string expected) }, new FormatterExtension[0]); var request = new FormatterRequest(new Literal(1, 1, 1, 1, ""), "test", PluralFormatter.PluralFunction, null); - var actual = subject.Pluralize("en_US", PluralRulesMetadata.TryGetCardinalRuleByLocale, subject.Pluralizers, arguments, new PluralContext(Convert.ToDecimal(args[request.Variable])), 0); + var actual = subject.Pluralize("en_US", PluralRulesMetadata.TryGetCardinalRuleByLocale, subject.CardinalPluralizers, arguments, new PluralContext(Convert.ToDecimal(args[request.Variable])), 0); Assert.Equal(expected, actual); } @@ -104,7 +104,7 @@ public void EnUS_Cardinal_PluralizerTests(double n, string expected) public void EnUS_Ordinal_PluralizerTests(double n, string expected) { var subject = new PluralFormatter(); - subject.Pluralizers.Clear(); + subject.CardinalPluralizers.Clear(); var args = new Dictionary { { "test", n } }; var arguments = diff --git a/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralFormatter.cs b/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralFormatter.cs index 93f3c88..3d005ff 100644 --- a/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralFormatter.cs +++ b/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralFormatter.cs @@ -38,7 +38,7 @@ public class PluralFormatter : BaseFormatter, IFormatter /// public PluralFormatter() { - this.Pluralizers = new Dictionary(); + this.CardinalPluralizers = new Dictionary(); this.OrdinalPluralizers = new Dictionary(); } @@ -57,7 +57,7 @@ public PluralFormatter() /// /// The pluralizers. /// - public IDictionary Pluralizers { get; private set; } + public IDictionary CardinalPluralizers { get; } /// /// Gets the pluralizers dictionary to use for ordinal numbers. Key is the locale. @@ -65,7 +65,7 @@ public PluralFormatter() /// /// The ordinal pluralizers. /// - public IDictionary OrdinalPluralizers { get; private set; } + public IDictionary OrdinalPluralizers { get; } #endregion @@ -136,7 +136,7 @@ public string Format(string locale, if (request.FormatterName == PluralFunction) { cldrPluralLookup = PluralRulesMetadata.TryGetCardinalRuleByLocale; - customLookup = this.Pluralizers; + customLookup = this.CardinalPluralizers; } else if (request.FormatterName == OrdinalFunction) { diff --git a/src/Jeffijoe.MessageFormat/MessageFormatter.cs b/src/Jeffijoe.MessageFormat/MessageFormatter.cs index da147a2..3e90160 100644 --- a/src/Jeffijoe.MessageFormat/MessageFormatter.cs +++ b/src/Jeffijoe.MessageFormat/MessageFormatter.cs @@ -145,23 +145,33 @@ public IFormatterLibrary Formatters public string Locale { get; set; } /// - /// Gets the pluralizers dictionary from the , if set. Key is the locale. + /// Gets the custom cardinal pluralizers dictionary from the , if set. Key is the locale. + /// These are the pluralizers used to translate e.g., {count, plural, one {1 book} other {# books}} /// + /// + /// The library relies on Unicode CLDR rules for locales by default, and any values in this dictionary override those behaviors + /// for the specified locales. + /// /// /// The pluralizers, or null if the plural formatter has not been added. /// - public IDictionary? Pluralizers + public IDictionary? CardinalPluralizers { get { var pluralFormatter = this.Formatters.OfType().FirstOrDefault(); - return pluralFormatter?.Pluralizers; + return pluralFormatter?.CardinalPluralizers; } } /// - /// Gets the ordinal number pluralizers dictionary from the , if set. Key is the locale. + /// Gets the custom ordinal number pluralizers dictionary from the , if set. Key is the locale. + /// These are the pluralizers used to translate e.g., {count, selectordinal, one {#st} two {#nd} few {#rd} other {#th}} /// + /// + /// The library relies on Unicode CLDR rules for locales by default, and any values in this dictionary override those behaviors + /// for the specified locales. + /// /// /// The pluralizers, or null if the plural formatter has not been added. /// From f587a11a6899d57b8e2962b5a9f91adcd4f9a757 Mon Sep 17 00:00:00 2001 From: Steven Fuqua Date: Tue, 17 Feb 2026 13:27:24 -0800 Subject: [PATCH 91/98] Revert redundant Clear() calls --- .../MetadataGenerator/GeneratedPluralRulesTests.cs | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/GeneratedPluralRulesTests.cs b/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/GeneratedPluralRulesTests.cs index 75fbc0d..09f56bc 100644 --- a/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/GeneratedPluralRulesTests.cs +++ b/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/GeneratedPluralRulesTests.cs @@ -1,9 +1,7 @@ using System; using System.Collections.Generic; -using System.Linq; using Jeffijoe.MessageFormat.Formatting; using Jeffijoe.MessageFormat.Formatting.Formatters; -using Jeffijoe.MessageFormat.Helpers; using Jeffijoe.MessageFormat.Parsing; using Xunit; @@ -20,8 +18,6 @@ public class GeneratedPluralRulesTests public void Uk_PluralizerTests(double n, string expected) { var subject = new PluralFormatter(); - subject.CardinalPluralizers.Clear(); - var args = new Dictionary { { "test", n } }; var arguments = new ParsedArguments( @@ -47,8 +43,6 @@ public void Uk_PluralizerTests(double n, string expected) public void Ru_PluralizerTests(double n, string expected) { var subject = new PluralFormatter(); - subject.CardinalPluralizers.Clear(); - var args = new Dictionary { { "test", n } }; var arguments = new ParsedArguments( @@ -74,8 +68,6 @@ public void Ru_PluralizerTests(double n, string expected) public void EnUS_Cardinal_PluralizerTests(double n, string expected) { var subject = new PluralFormatter(); - subject.CardinalPluralizers.Clear(); - var args = new Dictionary { { "test", n } }; var arguments = new ParsedArguments( @@ -104,8 +96,6 @@ public void EnUS_Cardinal_PluralizerTests(double n, string expected) public void EnUS_Ordinal_PluralizerTests(double n, string expected) { var subject = new PluralFormatter(); - subject.CardinalPluralizers.Clear(); - var args = new Dictionary { { "test", n } }; var arguments = new ParsedArguments( From 3197fe2bb710ea1a63a3a5e45825ecd8d6fc6bc2 Mon Sep 17 00:00:00 2001 From: Jeff Hansen Date: Sat, 21 Feb 2026 10:29:45 -0500 Subject: [PATCH 92/98] Remove .NET 6 target, add .NET 10 target --- .../Jeffijoe.MessageFormat.Tests.csproj | 4 ++-- src/Jeffijoe.MessageFormat/Jeffijoe.MessageFormat.csproj | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Jeffijoe.MessageFormat.Tests/Jeffijoe.MessageFormat.Tests.csproj b/src/Jeffijoe.MessageFormat.Tests/Jeffijoe.MessageFormat.Tests.csproj index a24f302..c0407ff 100644 --- a/src/Jeffijoe.MessageFormat.Tests/Jeffijoe.MessageFormat.Tests.csproj +++ b/src/Jeffijoe.MessageFormat.Tests/Jeffijoe.MessageFormat.Tests.csproj @@ -4,9 +4,9 @@ True MessageFormat.snk False - 12 + default enable - net8.0 + net10.0 diff --git a/src/Jeffijoe.MessageFormat/Jeffijoe.MessageFormat.csproj b/src/Jeffijoe.MessageFormat/Jeffijoe.MessageFormat.csproj index fd00743..2eebfd9 100644 --- a/src/Jeffijoe.MessageFormat/Jeffijoe.MessageFormat.csproj +++ b/src/Jeffijoe.MessageFormat/Jeffijoe.MessageFormat.csproj @@ -10,7 +10,7 @@ https://github.com/jeffijoe/messageformat.net latest enable - net6.0;net8.0;netstandard2.0;netstandard2.1 + net8.0;netstandard2.0;netstandard2.1;net10.0 true true From 12bf8344e5aa0cdfe542047ca6ccae230f24aac1 Mon Sep 17 00:00:00 2001 From: Jeff Hansen Date: Sat, 21 Feb 2026 11:26:27 -0500 Subject: [PATCH 93/98] Use `CultureInfo` in the API instead of a string locale + default to `CurrentCulture` --- README.md | 4 +- .../Formatters/DateFormatterTests.cs | 6 +-- .../Formatters/NumberFormatterTests.cs | 14 +++--- .../Formatters/SelectFormatterTests.cs | 5 ++- .../Formatters/TimeFormatterTests.cs | 6 +-- .../Formatters/VariableFormatterTests.cs | 5 ++- .../MessageFormatterFullIntegrationTests.cs | 5 ++- .../MessageFormatterIssues.cs | 7 +-- .../MessageFormatterTests.cs | 3 +- .../TestHelpers/FakeFormatter.cs | 3 +- .../Formatters/BaseValueFormatter.cs | 3 +- .../Formatting/Formatters/PluralFormatter.cs | 8 ++-- .../Formatting/Formatters/SelectFormatter.cs | 5 ++- .../Formatters/VariableFormatter.cs | 45 +++++-------------- .../Formatting/IFormatter.cs | 5 ++- .../MessageFormatter.cs | 25 ++++++----- 16 files changed, 68 insertions(+), 81 deletions(-) diff --git a/README.md b/README.md index 52ad3dd..34ee08f 100644 --- a/README.md +++ b/README.md @@ -127,7 +127,7 @@ var custom = new CustomValueFormatters }; // Create a MessageFormatter with the custom value formatter. -var formatter = new MessageFormatter(locale: "en-US", customValueFormatter: custom); +var formatter = new MessageFormatter(culture: CultureInfo.GetCultureInfo("en-US"), customValueFormatter: custom); // Format a message. var message = formatter.FormatMessage("{value, number, $0.0}", new { value = 23 }); @@ -162,7 +162,7 @@ There's no restrictions on what strings you may return, nor what strings you may use in your pluralization block. ````csharp -var mf = new MessageFormatter(true, "en"); // true = use cache +var mf = new MessageFormatter(true, CultureInfo.GetCultureInfo("en")); // true = use cache mf.CardinalPluralizers["en"] = n => { // ´n´ is the number being pluralized. diff --git a/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/DateFormatterTests.cs b/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/DateFormatterTests.cs index c36168f..381721d 100644 --- a/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/DateFormatterTests.cs +++ b/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/DateFormatterTests.cs @@ -12,7 +12,7 @@ public class DateFormatterTests [InlineData("da-DK", "1994-09-06T15:00:00Z", "06.09.1994")] public void DateFormatter_Short(string locale, string dateStr, string expected) { - var mf = new MessageFormatter(locale: locale); + var mf = new MessageFormatter(culture: CultureInfo.GetCultureInfo(locale)); var actual = mf.FormatMessage("{value, date}", new { value = DateTimeOffset.Parse(dateStr) @@ -26,7 +26,7 @@ public void DateFormatter_Short(string locale, string dateStr, string expected) [InlineData("da-DK", "1994-09-06T15:00:00Z", "tirsdag den 6. september 1994")] public void DateFormatter_Full(string locale, string dateStr, string expected) { - var mf = new MessageFormatter(locale: locale); + var mf = new MessageFormatter(culture: CultureInfo.GetCultureInfo(locale)); var actual = mf.FormatMessage("{value, date, full}", new { value = DateTimeOffset.Parse(dateStr) @@ -58,7 +58,7 @@ public void DateFormatter_Custom() return true; } }; - var mf = new MessageFormatter(locale: "en-US", customValueFormatter: formatter); + var mf = new MessageFormatter(culture: CultureInfo.GetCultureInfo("en-US"), customValueFormatter: formatter); var actual = mf.FormatMessage("{value, date, long}", new { value = DateTimeOffset.Parse("1994-09-06T15:00:00Z") diff --git a/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/NumberFormatterTests.cs b/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/NumberFormatterTests.cs index 648cfab..740c4d3 100644 --- a/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/NumberFormatterTests.cs +++ b/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/NumberFormatterTests.cs @@ -13,7 +13,7 @@ public class NumberFormatterTests [InlineData(1234567.1234567, "1,234,567.123")] public void NumberFormatter_Default(decimal number, string expected) { - var mf = new MessageFormatter(locale: "en-US"); + var mf = new MessageFormatter(culture: CultureInfo.GetCultureInfo("en-US")); // NOTE: The whitespace at the end is on purpose to cover whitespace tolerance in parsing. var actual = mf.FormatMessage("{value, number }", new { @@ -36,7 +36,7 @@ public void NumberFormatter_Decimal_CustomFormat(decimal number, string expected return true; } }; - var mf = new MessageFormatter(locale: "en-US", customValueFormatter: formatters); + var mf = new MessageFormatter(culture: CultureInfo.GetCultureInfo("en-US"), customValueFormatter: formatters); var actual = mf.FormatMessage("{value, number, 0.0000}", new { @@ -52,7 +52,7 @@ public void NumberFormatter_Decimal_CustomFormat(decimal number, string expected [InlineData(1234567.1234567, "123,456,712%")] public void NumberFormatter_Percent(decimal number, string expected) { - var mf = new MessageFormatter(locale: "en-US"); + var mf = new MessageFormatter(culture: CultureInfo.GetCultureInfo("en-US")); // NOTE: The inconsistent whitespace in the pattern is to cover whitespace tolerance in parsing. var actual = mf.FormatMessage("{value, number,percent}", new @@ -72,7 +72,7 @@ public void NumberFormatter_Percent(decimal number, string expected) [InlineData(true, "True")] public void NumberFormatter_Integer(object? value, string expected) { - var mf = new MessageFormatter(locale: "en-US"); + var mf = new MessageFormatter(culture: CultureInfo.GetCultureInfo("en-US")); var actual = mf.FormatMessage("{value, number, integer}", new { value @@ -87,7 +87,7 @@ public void NumberFormatter_Integer(object? value, string expected) [InlineData("da-DK", 99.99, "99,99 kr.")] public void NumberFormatter_Currency(string locale, decimal number, string expected) { - var mf = new MessageFormatter(locale: locale); + var mf = new MessageFormatter(culture: CultureInfo.GetCultureInfo(locale)); // NOTE: The inconsistent whitespace in the pattern is to cover whitespace tolerance in parsing. var actual = mf.FormatMessage("{value, number, currency }", new @@ -102,7 +102,7 @@ public void NumberFormatter_Currency(string locale, decimal number, string expec public void NumberFormatter_ThrowsIfStyleIsNotSupported() { const decimal Number = 12.34m; - var mf = new MessageFormatter(locale: "en-US"); + var mf = new MessageFormatter(culture: CultureInfo.GetCultureInfo("en-US")); var ex = Assert.Throws(() => mf.FormatMessage($"{{value, number, wow}}", new @@ -117,7 +117,7 @@ public void NumberFormatter_ThrowsIfStyleIsNotSupported() [Fact] public void NumberFormatter_BadInput_FallsBackToRegularFormat() { - var mf = new MessageFormatter(locale: "en-US"); + var mf = new MessageFormatter(culture: CultureInfo.GetCultureInfo("en-US")); { var actual = mf.FormatMessage($"{{value, number, currency}}", new diff --git a/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/SelectFormatterTests.cs b/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/SelectFormatterTests.cs index 4e2ce37..b5ef8fb 100644 --- a/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/SelectFormatterTests.cs +++ b/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/SelectFormatterTests.cs @@ -5,6 +5,7 @@ // Copyright (C) Jeff Hansen 2015. All rights reserved. using System.Collections.Generic; +using System.Globalization; using Jeffijoe.MessageFormat.Formatting; using Jeffijoe.MessageFormat.Formatting.Formatters; using Jeffijoe.MessageFormat.Parsing; @@ -62,7 +63,7 @@ public void Format(string formatterArgs, string gender, string expectedBlock) "select", formatterArgs); var args = new Dictionary { { "gender", gender } }; - var result = subject.Format("en", req, args, gender, messageFormatter); + var result = subject.Format(CultureInfo.GetCultureInfo("en"), req, args, gender, messageFormatter); Assert.Equal(expectedBlock, result); } @@ -83,7 +84,7 @@ public void VerifyFormatThrowsWhenNoOtherOptionIsGiven() Assert.Throws(() => { - subject.Format("en", req, args, "non-binary", messageFormatter); + subject.Format(CultureInfo.GetCultureInfo("en"), req, args, "non-binary", messageFormatter); }); } diff --git a/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/TimeFormatterTests.cs b/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/TimeFormatterTests.cs index ca22c7f..31ad3c9 100644 --- a/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/TimeFormatterTests.cs +++ b/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/TimeFormatterTests.cs @@ -14,7 +14,7 @@ public partial class TimeFormatterTests [InlineData("da-DK", "1994-09-06T15:01:23Z", "15.01")] public void TimeFormatter_Short(string locale, string dateStr, string expected) { - var mf = new MessageFormatter(locale: locale); + var mf = new MessageFormatter(culture: CultureInfo.GetCultureInfo(locale)); var actual = mf.FormatMessage("{value, time, short}", new { value = DateTimeOffset.Parse(dateStr) @@ -31,7 +31,7 @@ public void TimeFormatter_Short(string locale, string dateStr, string expected) [InlineData("da-DK", "1994-09-06T15:01:23Z", "15.01.23")] public void TimeFormatter_Default(string locale, string dateStr, string expected) { - var mf = new MessageFormatter(locale: locale); + var mf = new MessageFormatter(culture: CultureInfo.GetCultureInfo(locale)); var actual = mf.FormatMessage("{value, time}", new { value = DateTimeOffset.Parse(dateStr) @@ -65,7 +65,7 @@ public void TimeFormatter_Custom() return true; } }; - var mf = new MessageFormatter(locale: "en-US", customValueFormatter: formatter); + var mf = new MessageFormatter(culture: CultureInfo.GetCultureInfo("en-US"), customValueFormatter: formatter); var actual = mf.FormatMessage("{value, time, long}", new { value = DateTimeOffset.Parse("1994-09-06T16:20:09Z") diff --git a/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/VariableFormatterTests.cs b/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/VariableFormatterTests.cs index 9cd26c7..fbd8f44 100644 --- a/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/VariableFormatterTests.cs +++ b/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/VariableFormatterTests.cs @@ -5,6 +5,7 @@ // Copyright (C) Jeff Hansen 2015. All rights reserved. using System.Collections.Generic; +using System.Globalization; using Jeffijoe.MessageFormat.Formatting; using Jeffijoe.MessageFormat.Formatting.Formatters; using Jeffijoe.MessageFormat.Parsing; @@ -57,7 +58,7 @@ public void VerifyAnEmptyStringIsReturnedWhenTheArgumentIsNull() var req = CreateRequest(); var args = new Dictionary(); - Assert.Equal(string.Empty, this.subject.Format("en", req, args, null, this.formatter)); + Assert.Equal(string.Empty, this.subject.Format(CultureInfo.GetCultureInfo("en"), req, args, null, this.formatter)); } /// @@ -69,7 +70,7 @@ public void VerifyTheValueFromTheGivenArgumentsIsReturnedAsAString() var req = CreateRequest(); var args = new Dictionary(); - Assert.Equal("is good", this.subject.Format("en", req, args, "is good", this.formatter)); + Assert.Equal("is good", this.subject.Format(CultureInfo.GetCultureInfo("en"), req, args, "is good", this.formatter)); } #endregion diff --git a/src/Jeffijoe.MessageFormat.Tests/MessageFormatterFullIntegrationTests.cs b/src/Jeffijoe.MessageFormat.Tests/MessageFormatterFullIntegrationTests.cs index ddb6497..2afff8c 100644 --- a/src/Jeffijoe.MessageFormat.Tests/MessageFormatterFullIntegrationTests.cs +++ b/src/Jeffijoe.MessageFormat.Tests/MessageFormatterFullIntegrationTests.cs @@ -5,6 +5,7 @@ // Copyright (C) Jeff Hansen 2015. All rights reserved. using System.Collections.Generic; +using System.Globalization; using Jeffijoe.MessageFormat.Formatting; using Jeffijoe.MessageFormat.Formatting.Formatters; using Jeffijoe.MessageFormat.Tests.TestHelpers; @@ -401,7 +402,7 @@ public static IEnumerable Tests [MemberData(nameof(Tests))] public void FormatMessage(string source, Dictionary args, string expected) { - var subject = new MessageFormatter(false); + var subject = new MessageFormatter(useCache: false, culture: CultureInfo.GetCultureInfo("en")); // Historically these tests relied on a default English pluralizer that mapped // 0 to "zero"; adding that back in manually to ensure we maintain test coverage @@ -653,7 +654,7 @@ public void ReadMe_test_to_make_sure_I_dont_look_like_a_fool() } { - var mf = new MessageFormatter(useCache: true, locale: "en"); + var mf = new MessageFormatter(useCache: true, culture: CultureInfo.GetCultureInfo("en")); mf.CardinalPluralizers!["en"] = n => { // ´n´ is the number being pluralized. diff --git a/src/Jeffijoe.MessageFormat.Tests/MessageFormatterIssues.cs b/src/Jeffijoe.MessageFormat.Tests/MessageFormatterIssues.cs index 87aeca9..92c115a 100644 --- a/src/Jeffijoe.MessageFormat.Tests/MessageFormatterIssues.cs +++ b/src/Jeffijoe.MessageFormat.Tests/MessageFormatterIssues.cs @@ -5,6 +5,7 @@ // Copyright (C) Jeff Hansen 2015. All rights reserved. using System.Collections.Generic; +using System.Globalization; using Xunit; namespace Jeffijoe.MessageFormat.Tests; @@ -41,7 +42,7 @@ public void Issue27_WhiteSpace_in_identifiers_is_ignored() [Fact] public void Issue31_IDictionary_interface_support() { - var subject = new MessageFormatter(locale: "en-US"); + var subject = new MessageFormatter(culture: CultureInfo.GetCultureInfo("en-US")); IDictionary idict = new Dictionary { @@ -60,7 +61,7 @@ public void Issue31_IDictionary_interface_support() [Fact] public void Issue34_Newlines_are_stripped() { - var subject = new MessageFormatter(locale: "en-US"); + var subject = new MessageFormatter(culture: CultureInfo.GetCultureInfo("en-US")); const string Expected = "Single text which will not change.\nSummary:\nAccepted\nData:\n-X\n-Y\n-Z"; @@ -76,7 +77,7 @@ public void Issue34_Newlines_are_stripped() [Fact] public void Issue45_Url_should_not_be_parsed_as_extension() { - var subject = new MessageFormatter(locale: "en-US"); + var subject = new MessageFormatter(culture: CultureInfo.GetCultureInfo("en-US")); IDictionary dict = new Dictionary { diff --git a/src/Jeffijoe.MessageFormat.Tests/MessageFormatterTests.cs b/src/Jeffijoe.MessageFormat.Tests/MessageFormatterTests.cs index 62677c0..270fa97 100644 --- a/src/Jeffijoe.MessageFormat.Tests/MessageFormatterTests.cs +++ b/src/Jeffijoe.MessageFormat.Tests/MessageFormatterTests.cs @@ -5,6 +5,7 @@ // Copyright (C) Jeff Hansen 2015. All rights reserved. using System.Collections.Generic; +using System.Globalization; using System.Text; using Jeffijoe.MessageFormat.Formatting; @@ -110,7 +111,7 @@ public TestFormatter(bool variableMustExist, string formatterName) public bool CanFormat(FormatterRequest request) => request.FormatterName == this.formatterName; - public string Format(string locale, FormatterRequest request, IReadOnlyDictionary args, object? value, + public string Format(CultureInfo culture, FormatterRequest request, IReadOnlyDictionary args, object? value, IMessageFormatter messageFormatter) { return "formatted"; diff --git a/src/Jeffijoe.MessageFormat.Tests/TestHelpers/FakeFormatter.cs b/src/Jeffijoe.MessageFormat.Tests/TestHelpers/FakeFormatter.cs index 0b97189..ef6fae7 100644 --- a/src/Jeffijoe.MessageFormat.Tests/TestHelpers/FakeFormatter.cs +++ b/src/Jeffijoe.MessageFormat.Tests/TestHelpers/FakeFormatter.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Globalization; using Jeffijoe.MessageFormat.Formatting; namespace Jeffijoe.MessageFormat.Tests.TestHelpers; @@ -43,7 +44,7 @@ public FakeFormatter(bool canFormat = false, string formatResult = "formatted") /// public string Format( - string locale, + CultureInfo culture, FormatterRequest request, IReadOnlyDictionary args, object? value, diff --git a/src/Jeffijoe.MessageFormat/Formatting/Formatters/BaseValueFormatter.cs b/src/Jeffijoe.MessageFormat/Formatting/Formatters/BaseValueFormatter.cs index 221e223..950202b 100644 --- a/src/Jeffijoe.MessageFormat/Formatting/Formatters/BaseValueFormatter.cs +++ b/src/Jeffijoe.MessageFormat/Formatting/Formatters/BaseValueFormatter.cs @@ -37,14 +37,13 @@ protected abstract string FormatValue(CultureInfo culture, CustomValueFormatter? /// public string Format( - string locale, + CultureInfo culture, FormatterRequest request, IReadOnlyDictionary args, object? value, IMessageFormatter messageFormatter) { var formatterArgs = request.FormatterArguments!; - var culture = CultureInfo.GetCultureInfo(locale); return FormatValue( culture: culture, customValueFormatter: messageFormatter.CustomValueFormatter, diff --git a/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralFormatter.cs b/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralFormatter.cs index 3d005ff..9e18e20 100644 --- a/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralFormatter.cs +++ b/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralFormatter.cs @@ -7,6 +7,7 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Globalization; using System.Linq; namespace Jeffijoe.MessageFormat.Formatting.Formatters; @@ -96,8 +97,8 @@ public bool CanFormat(FormatterRequest request) /// nested formatting. This is only called if returns true. /// The args will always contain the . /// - /// - /// The locale being used. It is up to the formatter what they do with this information. + /// + /// The culture being used. It is up to the formatter what they do with this information. /// /// /// The parameters. @@ -115,7 +116,7 @@ public bool CanFormat(FormatterRequest request) /// /// If does not specify a formatter name supported by . /// - public string Format(string locale, + public string Format(CultureInfo culture, FormatterRequest request, IReadOnlyDictionary args, object? value, @@ -148,6 +149,7 @@ public string Format(string locale, throw new MessageFormatterException($"Unsupported plural formatter name: {request.FormatterName}"); } + var locale = culture.Name; var ctx = CreatePluralContext(value, offset); var pluralized = this.Pluralize( locale, diff --git a/src/Jeffijoe.MessageFormat/Formatting/Formatters/SelectFormatter.cs b/src/Jeffijoe.MessageFormat/Formatting/Formatters/SelectFormatter.cs index f90cace..c254721 100644 --- a/src/Jeffijoe.MessageFormat/Formatting/Formatters/SelectFormatter.cs +++ b/src/Jeffijoe.MessageFormat/Formatting/Formatters/SelectFormatter.cs @@ -6,6 +6,7 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Globalization; namespace Jeffijoe.MessageFormat.Formatting.Formatters; @@ -48,7 +49,7 @@ public bool CanFormat(FormatterRequest request) /// nested formatting. This is only called if returns true. /// The args will always contain the . /// - /// The locale being used. It is up to the formatter what they do with this information. + /// The culture being used. It is up to the formatter what they do with this information. /// The parameters. /// The arguments. /// The value of from the given args dictionary. Can be null. @@ -59,7 +60,7 @@ public bool CanFormat(FormatterRequest request) /// 'other' option not found in pattern, and variable was not present in collection. [SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1126:PrefixCallsCorrectly", Justification = "Reviewed. Suppression is OK here.")] - public string Format(string locale, + public string Format(CultureInfo culture, FormatterRequest request, IReadOnlyDictionary args, object? value, diff --git a/src/Jeffijoe.MessageFormat/Formatting/Formatters/VariableFormatter.cs b/src/Jeffijoe.MessageFormat/Formatting/Formatters/VariableFormatter.cs index ea58b26..fbf2d21 100644 --- a/src/Jeffijoe.MessageFormat/Formatting/Formatters/VariableFormatter.cs +++ b/src/Jeffijoe.MessageFormat/Formatting/Formatters/VariableFormatter.cs @@ -1,10 +1,9 @@ -// MessageFormat for .NET +// MessageFormat for .NET // - VariableFormatter.cs // Author: Jeff Hansen // Copyright (C) Jeff Hansen 2014. All rights reserved. using System; -using System.Collections.Concurrent; using System.Collections.Generic; using System.Globalization; @@ -15,21 +14,15 @@ namespace Jeffijoe.MessageFormat.Formatting.Formatters; /// public class VariableFormatter : IFormatter { - #region Fields - - private readonly ConcurrentDictionary cultures = new ConcurrentDictionary(); - - #endregion - #region Public Properties /// /// This formatter requires the input variable to exist. /// public bool VariableMustExist => true; - + #endregion - + #region Public Methods and Operators /// @@ -47,12 +40,12 @@ public bool CanFormat(FormatterRequest request) } /// - /// Using the specified parameters and arguments, a formatted string shall be returned. - /// The is being provided as well, to enable - /// nested formatting. This is only called if returns true. - /// The args will always contain the . + /// Using the specified parameters and arguments, a formatted string shall be returned. + /// The is being provided as well, to enable + /// nested formatting. This is only called if returns true. + /// The args will always contain the . /// - /// The locale being used. It is up to the formatter what they do with this information. + /// The culture being used. It is up to the formatter what they do with this information. /// The parameters. /// The arguments. /// The value of from the given args dictionary. Can be null. @@ -60,7 +53,7 @@ public bool CanFormat(FormatterRequest request) /// /// The . /// - public string Format(string locale, + public string Format(CultureInfo culture, FormatterRequest request, IReadOnlyDictionary args, object? value, @@ -69,27 +62,11 @@ public string Format(string locale, switch (value) { case IFormattable formattable: - return formattable.ToString(null, GetCultureInfo(locale)); + return formattable.ToString(null, culture); default: return value?.ToString() ?? string.Empty; } } - /// - /// Get and cache the culture for a locale. - /// - /// Locale for which to get the culture. - /// - /// Culture of locale. - /// - private CultureInfo GetCultureInfo(string locale) - { - if (!this.cultures.ContainsKey(locale)) - { - this.cultures[locale] = new CultureInfo(locale); - } - return this.cultures[locale]; - } - #endregion -} \ No newline at end of file +} diff --git a/src/Jeffijoe.MessageFormat/Formatting/IFormatter.cs b/src/Jeffijoe.MessageFormat/Formatting/IFormatter.cs index abc8f2b..88e0327 100644 --- a/src/Jeffijoe.MessageFormat/Formatting/IFormatter.cs +++ b/src/Jeffijoe.MessageFormat/Formatting/IFormatter.cs @@ -4,6 +4,7 @@ // Copyright (C) Jeff Hansen 2014. All rights reserved. using System.Collections.Generic; +using System.Globalization; namespace Jeffijoe.MessageFormat.Formatting; @@ -41,7 +42,7 @@ public interface IFormatter /// nested formatting. This is only called if returns true. /// The args will always contain the . /// - /// The locale being used. It is up to the formatter what they do with this information. + /// The culture being used. It is up to the formatter what they do with this information. /// The parameters. /// The arguments. /// The value of from the given args dictionary. Can be null. @@ -50,7 +51,7 @@ public interface IFormatter /// The . /// string Format( - string locale, + CultureInfo culture, FormatterRequest request, IReadOnlyDictionary args, object? value, diff --git a/src/Jeffijoe.MessageFormat/MessageFormatter.cs b/src/Jeffijoe.MessageFormat/MessageFormatter.cs index 3e90160..dbce2e8 100644 --- a/src/Jeffijoe.MessageFormat/MessageFormatter.cs +++ b/src/Jeffijoe.MessageFormat/MessageFormatter.cs @@ -7,6 +7,7 @@ using System.Collections.Generic; using System.Collections.Concurrent; using System.Diagnostics.CodeAnalysis; +using System.Globalization; using System.Linq; using System.Runtime.CompilerServices; using System.Text; @@ -64,19 +65,19 @@ public class MessageFormatter : IMessageFormatter /// /// The use Cache. /// - /// - /// The locale. + /// + /// The culture to use. Defaults to if not specified. /// /// /// The custom value formatter to use. Can be null. /// - public MessageFormatter(bool useCache = true, string locale = "en", + public MessageFormatter(bool useCache = true, CultureInfo? culture = null, CustomValueFormatter? customValueFormatter = null) : this( patternParser: new PatternParser(new LiteralParser()), library: new FormatterLibrary(), useCache: useCache, - locale: locale, + culture: culture, customValueFormatter: customValueFormatter) { } @@ -93,8 +94,8 @@ public MessageFormatter(bool useCache = true, string locale = "en", /// /// if set to true uses the cache. /// - /// - /// The locale to use. Formatters may need this. + /// + /// The culture to use. Formatters may need this. Defaults to if not specified. /// /// /// The custom value formatter to use. Can be null. @@ -103,13 +104,13 @@ internal MessageFormatter( IPatternParser patternParser, IFormatterLibrary library, bool useCache, - string locale = "en", + CultureInfo? culture = null, CustomValueFormatter? customValueFormatter = null) { this.patternParser = patternParser ?? throw new ArgumentNullException("patternParser"); this.library = library ?? throw new ArgumentNullException("library"); this.CustomValueFormatter = customValueFormatter; - this.Locale = locale; + this.Culture = culture ?? CultureInfo.CurrentCulture; if (useCache) { this.cache = new ConcurrentDictionary(); @@ -137,12 +138,12 @@ public IFormatterLibrary Formatters } /// - /// Gets or sets the locale. + /// Gets or sets the culture. /// /// - /// The locale. + /// The culture. /// - public string Locale { get; set; } + public CultureInfo Culture { get; set; } /// /// Gets the custom cardinal pluralizers dictionary from the , if set. Key is the locale. @@ -276,7 +277,7 @@ public string FormatMessage(string pattern, IReadOnlyDictionary } // Double dispatch, yeah! - var result = formatter.Format(this.Locale, request, args, value, this); + var result = formatter.Format(this.Culture, request, args, value, this); // First, we remove the literal from the source. var sourceLiteral = request.SourceLiteral; From 5e70b7bc5cb017273069a844c66f6ba22e229764 Mon Sep 17 00:00:00 2001 From: Jeff Hansen Date: Sat, 21 Feb 2026 11:49:52 -0500 Subject: [PATCH 94/98] Add `culture` override to `FormatMessage` --- README.md | 13 ++- .../Formatters/DateFormatterTests.cs | 20 ++-- .../Formatters/NumberFormatterTests.cs | 40 ++++--- .../Formatters/TimeFormatterTests.cs | 19 +-- .../MessageFormatterFullIntegrationTests.cs | 110 ++++++++++++------ .../MessageFormatterIssues.cs | 22 ++-- .../MessageFormatterStringExtensionTests.cs | 11 +- .../MetadataGenerator/ParserTests.cs | 1 - .../Parsing/LiteralParserTests.cs | 1 - .../TestHelpers/FakeMessageFormatter.cs | 3 +- .../TestHelpers/UseCultureAttribute.cs | 73 ++++++++++++ .../Formatting/Formatters/PluralFormatter.cs | 2 +- .../Formatters/PluralRulesMetadata.cs | 4 +- .../Formatting/Formatters/SelectFormatter.cs | 4 +- .../IMessageFormatter.cs | 6 +- .../MessageFormatter.cs | 48 ++++---- .../MessageFormatterExtensions.cs | 16 ++- 17 files changed, 265 insertions(+), 128 deletions(-) create mode 100644 src/Jeffijoe.MessageFormat.Tests/TestHelpers/UseCultureAttribute.cs diff --git a/README.md b/README.md index 34ee08f..d9ce36f 100644 --- a/README.md +++ b/README.md @@ -127,10 +127,11 @@ var custom = new CustomValueFormatters }; // Create a MessageFormatter with the custom value formatter. -var formatter = new MessageFormatter(culture: CultureInfo.GetCultureInfo("en-US"), customValueFormatter: custom); +var formatter = new MessageFormatter(customValueFormatter: custom); -// Format a message. -var message = formatter.FormatMessage("{value, number, $0.0}", new { value = 23 }); +// Format a message, passing the culture to FormatMessage. +var message = formatter.FormatMessage("{value, number, $0.0}", new { value = 23 }, + CultureInfo.GetCultureInfo("en-US")); // "$23.0" ``` @@ -158,11 +159,11 @@ mf.CardinalPluralizers.Add("", n => { }); ```` -There's no restrictions on what strings you may return, nor what strings +There are no restrictions on what strings you may return, nor what strings you may use in your pluralization block. ````csharp -var mf = new MessageFormatter(true, CultureInfo.GetCultureInfo("en")); // true = use cache +var mf = new MessageFormatter(); // uses cache by default mf.CardinalPluralizers["en"] = n => { // ´n´ is the number being pluralized. @@ -175,7 +176,7 @@ mf.CardinalPluralizers["en"] = n => mf.FormatMessage("You have {number, plural, thatsalot {a shitload of notifications} other {# notifications}}", new Dictionary{ {"number", 1001} -}); +}, CultureInfo.GetCultureInfo("en")); ```` ## Escaping literals diff --git a/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/DateFormatterTests.cs b/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/DateFormatterTests.cs index 381721d..5c33a00 100644 --- a/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/DateFormatterTests.cs +++ b/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/DateFormatterTests.cs @@ -7,16 +7,18 @@ namespace Jeffijoe.MessageFormat.Tests.Formatting.Formatters; public class DateFormatterTests { + private static readonly CultureInfo En = CultureInfo.GetCultureInfo("en"); + [Theory] [InlineData("en-US", "1994-09-06T15:00:00Z", "9/6/1994")] [InlineData("da-DK", "1994-09-06T15:00:00Z", "06.09.1994")] public void DateFormatter_Short(string locale, string dateStr, string expected) { - var mf = new MessageFormatter(culture: CultureInfo.GetCultureInfo(locale)); + var mf = new MessageFormatter(); var actual = mf.FormatMessage("{value, date}", new { value = DateTimeOffset.Parse(dateStr) - }); + }, CultureInfo.GetCultureInfo(locale)); Assert.Equal(expected, actual); } @@ -26,11 +28,11 @@ public void DateFormatter_Short(string locale, string dateStr, string expected) [InlineData("da-DK", "1994-09-06T15:00:00Z", "tirsdag den 6. september 1994")] public void DateFormatter_Full(string locale, string dateStr, string expected) { - var mf = new MessageFormatter(culture: CultureInfo.GetCultureInfo(locale)); + var mf = new MessageFormatter(); var actual = mf.FormatMessage("{value, date, full}", new { value = DateTimeOffset.Parse(dateStr) - }); + }, CultureInfo.GetCultureInfo(locale)); Assert.Equal(expected, actual); } @@ -45,25 +47,25 @@ public void DateFormatter_UnsupportedStyle() value = DateTimeOffset.UtcNow })); } - + [Fact] public void DateFormatter_Custom() { var formatter = new CustomValueFormatters { - Date = (CultureInfo culture, object? value, string? _, out string? formatted) => + Date = (culture, value, _, out formatted) => { // This is just a test, you probably shouldn't be doing this in real workloads. formatted = ((FormattableString)$"{value:MMMM d 'in the year' yyyy}").ToString(culture); return true; } }; - var mf = new MessageFormatter(culture: CultureInfo.GetCultureInfo("en-US"), customValueFormatter: formatter); + var mf = new MessageFormatter(customValueFormatter: formatter); var actual = mf.FormatMessage("{value, date, long}", new { value = DateTimeOffset.Parse("1994-09-06T15:00:00Z") - }); + }, En); Assert.Equal("September 6 in the year 1994", actual); } -} \ No newline at end of file +} diff --git a/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/NumberFormatterTests.cs b/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/NumberFormatterTests.cs index 740c4d3..d52ccff 100644 --- a/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/NumberFormatterTests.cs +++ b/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/NumberFormatterTests.cs @@ -6,6 +6,8 @@ namespace Jeffijoe.MessageFormat.Tests.Formatting.Formatters; public class NumberFormatterTests { + private static readonly CultureInfo En = CultureInfo.GetCultureInfo("en"); + [Theory] [InlineData(69, "69")] [InlineData(69.420, "69.42")] @@ -13,12 +15,12 @@ public class NumberFormatterTests [InlineData(1234567.1234567, "1,234,567.123")] public void NumberFormatter_Default(decimal number, string expected) { - var mf = new MessageFormatter(culture: CultureInfo.GetCultureInfo("en-US")); + var mf = new MessageFormatter(); // NOTE: The whitespace at the end is on purpose to cover whitespace tolerance in parsing. var actual = mf.FormatMessage("{value, number }", new { value = number - }); + }, En); Assert.Equal(expected, actual); } @@ -30,18 +32,18 @@ public void NumberFormatter_Decimal_CustomFormat(decimal number, string expected { var formatters = new CustomValueFormatters { - Number = (CultureInfo culture, object? value, string? style, out string? formatted) => + Number = (culture, value, style, out formatted) => { formatted = string.Format(culture, $"{{0:{style}}}", value); return true; } }; - var mf = new MessageFormatter(culture: CultureInfo.GetCultureInfo("en-US"), customValueFormatter: formatters); + var mf = new MessageFormatter(customValueFormatter: formatters); var actual = mf.FormatMessage("{value, number, 0.0000}", new { value = number - }); + }, En); Assert.Equal(expected, actual); } @@ -52,13 +54,13 @@ public void NumberFormatter_Decimal_CustomFormat(decimal number, string expected [InlineData(1234567.1234567, "123,456,712%")] public void NumberFormatter_Percent(decimal number, string expected) { - var mf = new MessageFormatter(culture: CultureInfo.GetCultureInfo("en-US")); - + var mf = new MessageFormatter(); + // NOTE: The inconsistent whitespace in the pattern is to cover whitespace tolerance in parsing. var actual = mf.FormatMessage("{value, number,percent}", new { value = number - }); + }, En); Assert.Equal(expected, actual); } @@ -72,11 +74,11 @@ public void NumberFormatter_Percent(decimal number, string expected) [InlineData(true, "True")] public void NumberFormatter_Integer(object? value, string expected) { - var mf = new MessageFormatter(culture: CultureInfo.GetCultureInfo("en-US")); + var mf = new MessageFormatter(); var actual = mf.FormatMessage("{value, number, integer}", new { value - }); + }, En); Assert.Equal(expected, actual); } @@ -87,13 +89,13 @@ public void NumberFormatter_Integer(object? value, string expected) [InlineData("da-DK", 99.99, "99,99 kr.")] public void NumberFormatter_Currency(string locale, decimal number, string expected) { - var mf = new MessageFormatter(culture: CultureInfo.GetCultureInfo(locale)); + var mf = new MessageFormatter(); // NOTE: The inconsistent whitespace in the pattern is to cover whitespace tolerance in parsing. var actual = mf.FormatMessage("{value, number, currency }", new { value = number - }); + }, CultureInfo.GetCultureInfo(locale)); Assert.Equal(expected, actual); } @@ -102,13 +104,13 @@ public void NumberFormatter_Currency(string locale, decimal number, string expec public void NumberFormatter_ThrowsIfStyleIsNotSupported() { const decimal Number = 12.34m; - var mf = new MessageFormatter(culture: CultureInfo.GetCultureInfo("en-US")); + var mf = new MessageFormatter(); var ex = Assert.Throws(() => mf.FormatMessage($"{{value, number, wow}}", new { value = Number - })); + }, En)); Assert.Equal("value", ex.Variable); Assert.Equal("number", ex.Format); Assert.Equal("wow", ex.Style); @@ -117,13 +119,13 @@ public void NumberFormatter_ThrowsIfStyleIsNotSupported() [Fact] public void NumberFormatter_BadInput_FallsBackToRegularFormat() { - var mf = new MessageFormatter(culture: CultureInfo.GetCultureInfo("en-US")); + var mf = new MessageFormatter(); { var actual = mf.FormatMessage($"{{value, number, currency}}", new { value = "a lot of money" - }); + }, En); Assert.Equal("a lot of money", actual); } @@ -132,7 +134,7 @@ public void NumberFormatter_BadInput_FallsBackToRegularFormat() var actual = mf.FormatMessage($"{{value, number, integer}}", new { value = "a lot of money" - }); + }, En); Assert.Equal("a lot of money", actual); } @@ -141,9 +143,9 @@ public void NumberFormatter_BadInput_FallsBackToRegularFormat() var actual = mf.FormatMessage($"{{value, number, integer}}", new { value = true - }); + }, En); Assert.Equal("True", actual); } } -} \ No newline at end of file +} diff --git a/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/TimeFormatterTests.cs b/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/TimeFormatterTests.cs index 31ad3c9..04f5715 100644 --- a/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/TimeFormatterTests.cs +++ b/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/TimeFormatterTests.cs @@ -8,17 +8,18 @@ namespace Jeffijoe.MessageFormat.Tests.Formatting.Formatters; public partial class TimeFormatterTests { + private static readonly CultureInfo En = CultureInfo.GetCultureInfo("en"); [Theory] [InlineData("en-US", "1994-09-06T15:01:23Z", "3:01 PM")] [InlineData("da-DK", "1994-09-06T15:01:23Z", "15.01")] public void TimeFormatter_Short(string locale, string dateStr, string expected) { - var mf = new MessageFormatter(culture: CultureInfo.GetCultureInfo(locale)); + var mf = new MessageFormatter(); var actual = mf.FormatMessage("{value, time, short}", new { value = DateTimeOffset.Parse(dateStr) - }); + }, CultureInfo.GetCultureInfo(locale)); // Replacing all whitespace due to a difference in formatting on macOS vs Linux. expected = Normalize(expected); @@ -31,11 +32,11 @@ public void TimeFormatter_Short(string locale, string dateStr, string expected) [InlineData("da-DK", "1994-09-06T15:01:23Z", "15.01.23")] public void TimeFormatter_Default(string locale, string dateStr, string expected) { - var mf = new MessageFormatter(culture: CultureInfo.GetCultureInfo(locale)); + var mf = new MessageFormatter(); var actual = mf.FormatMessage("{value, time}", new { value = DateTimeOffset.Parse(dateStr) - }); + }, CultureInfo.GetCultureInfo(locale)); // Replacing all whitespace due to a difference in formatting on macOS vs Linux. expected = Normalize(expected); @@ -53,23 +54,23 @@ public void TimeFormatter_UnsupportedStyle() value = DateTimeOffset.UtcNow })); } - + [Fact] public void TimeFormatter_Custom() { var formatter = new CustomValueFormatters { - Time = (CultureInfo _, object? value, string? _, out string? formatted) => + Time = (_, value, _, out formatted) => { formatted = $"{value:hmm} nice"; return true; } }; - var mf = new MessageFormatter(culture: CultureInfo.GetCultureInfo("en-US"), customValueFormatter: formatter); + var mf = new MessageFormatter(customValueFormatter: formatter); var actual = mf.FormatMessage("{value, time, long}", new { value = DateTimeOffset.Parse("1994-09-06T16:20:09Z") - }); + }, En); Assert.Equal("420 nice", actual); } @@ -81,4 +82,4 @@ private static string Normalize(string input) { return WhitespaceRegex().Replace(input, string.Empty); } -} \ No newline at end of file +} diff --git a/src/Jeffijoe.MessageFormat.Tests/MessageFormatterFullIntegrationTests.cs b/src/Jeffijoe.MessageFormat.Tests/MessageFormatterFullIntegrationTests.cs index 2afff8c..c0b28ff 100644 --- a/src/Jeffijoe.MessageFormat.Tests/MessageFormatterFullIntegrationTests.cs +++ b/src/Jeffijoe.MessageFormat.Tests/MessageFormatterFullIntegrationTests.cs @@ -1,4 +1,4 @@ -// MessageFormat for .NET +// MessageFormat for .NET // - MessageFormatter_full_integration_tests.cs // // Author: Jeff Hansen @@ -7,7 +7,6 @@ using System.Collections.Generic; using System.Globalization; using Jeffijoe.MessageFormat.Formatting; -using Jeffijoe.MessageFormat.Formatting.Formatters; using Jeffijoe.MessageFormat.Tests.TestHelpers; using Xunit; using Xunit.Abstractions; @@ -21,6 +20,10 @@ public class MessageFormatterFullIntegrationTests { #region Fields + private static readonly CultureInfo En = CultureInfo.GetCultureInfo("en"); + private static readonly CultureInfo EnUs = CultureInfo.GetCultureInfo("en-US"); + private static readonly CultureInfo DaDk = CultureInfo.GetCultureInfo("da-DK"); + /// /// The output helper. /// @@ -143,32 +146,32 @@ public static IEnumerable Tests { get { - const string Case1 = @"{gender, select, + const string Case1 = @"{gender, select, male {He - '{'{name}'}' -} female {She - '{'{name}'}' -} other {They} } said: You're pretty cool!"; - const string Case2 = @"{gender, select, + const string Case2 = @"{gender, select, male {He - '{'{name}'}' -} female {She - '{'{name}'}' -} other {They} - } said: You have {count, plural, + } said: You have {count, plural, zero {no notifications} one {just one notification} =42 {a universal amount of notifications} other {# notifications} }. Have a nice day!"; - const string Case3 = @"You have {count, plural, + const string Case3 = @"You have {count, plural, zero {no notifications} one {just one notification} =42 {a universal amount of notifications} other {# notifications} }. Have a nice day!"; - const string Case4 = @"{gender, select, + const string Case4 = @"{gender, select, male {He} female {She} other {They} - } said: You have {count, plural, + } said: You have {count, plural, zero {no notifications} one {just one notification} =42 {a universal amount of notifications} @@ -176,21 +179,21 @@ public static IEnumerable Tests }. Have a nice day!"; // Please take the following sample in the spirit it was intended. :) - const string Case5 = @"{gender, select, - male {He (who has {genitals, plural, + const string Case5 = @"{gender, select, + male {He (who has {genitals, plural, zero {no testicles} one {just one testicle} =2 {a normal amount of testicles} other {the insane amount of # testicles} })} - female {She (who has {genitals, plural, + female {She (who has {genitals, plural, zero {no boobies} one {just one boob} =2 {a pair of lovelies} other {the freakish amount of # boobies} })} other {They} - } said: You have {count, plural, + } said: You have {count, plural, zero {no notifications} one {just one notification} =42 {a universal amount of notifications} @@ -402,30 +405,22 @@ public static IEnumerable Tests [MemberData(nameof(Tests))] public void FormatMessage(string source, Dictionary args, string expected) { - var subject = new MessageFormatter(useCache: false, culture: CultureInfo.GetCultureInfo("en")); + var subject = new MessageFormatter(useCache: false); // Historically these tests relied on a default English pluralizer that mapped // 0 to "zero"; adding that back in manually to ensure we maintain test coverage // for multiple forms. - subject.CardinalPluralizers!.Add("en", (number) => + subject.CardinalPluralizers!.Add("en", number => number switch { - if (number == 0) - { - return "zero"; - } else if (number == 1) - { - return "one"; - } - else - { - return "other"; - } + 0 => "zero", + 1 => "one", + _ => "other" }); // Warmup - subject.FormatMessage(source, args); + subject.FormatMessage(source, args, En); Benchmark.Start("Formatting", this.outputHelper); - string result = subject.FormatMessage(source, args); + string result = subject.FormatMessage(source, args, En); Benchmark.End(this.outputHelper); Assert.Equal(expected, result); this.outputHelper.WriteLine(result); @@ -450,11 +445,11 @@ public void FormatMessage_escaping(string source, Dictionary ar [Fact] public void FormatMessage_debug() { - const string Source = @"{gender, select, + const string Source = @"{gender, select, male {He} female {She} other {They} - } said: You have {count, plural, + } said: You have {count, plural, zero {no notifications} one {just one notification} =42 {a universal amount of notifications} @@ -487,8 +482,8 @@ public void FormatMessage_lets_non_ascii_characters_right_through() public void FormatMessage_nesting_with_brace_escaping() { var subject = new MessageFormatter(false); - const string Pattern = @"{s1, select, - 1 {{s2, select, + const string Pattern = @"{s1, select, + 1 {{s2, select, 2 {'{'} }} }"; @@ -503,7 +498,7 @@ public void FormatMessage_nesting_with_brace_escaping() [Fact] public void FormatMessage_with_reflection_overload() { - var subject = new MessageFormatter(false); + var subject = new MessageFormatter(false, culture: EnUs); const string Pattern = "You have {UnreadCount, plural, " + "=0 {no unread messages}" + "one {just one unread message}" + "other {# unread messages}" + "} today."; @@ -526,7 +521,7 @@ public void FormatMessage_with_reflection_overload() /// /// The read me_test_to_make_sure_ i_dont_look_like_a_fool. /// - [Fact] + [Fact, UseCulture("en")] public void ReadMe_test_to_make_sure_I_dont_look_like_a_fool() { { @@ -546,7 +541,7 @@ public void ReadMe_test_to_make_sure_I_dont_look_like_a_fool() { var mf = new MessageFormatter(false); const string Str = @"You {NUM_ADDS, plural, offset:1 - =0{didnt add this to your profile} + =0{didnt add this to your profile} =1{added this to your profile} one{and one other person added this to their profile} other{and # others added this to their profiles} @@ -654,7 +649,7 @@ public void ReadMe_test_to_make_sure_I_dont_look_like_a_fool() } { - var mf = new MessageFormatter(useCache: true, culture: CultureInfo.GetCultureInfo("en")); + var mf = new MessageFormatter(useCache: true); mf.CardinalPluralizers!["en"] = n => { // ´n´ is the number being pluralized. @@ -681,10 +676,51 @@ public void ReadMe_test_to_make_sure_I_dont_look_like_a_fool() var actual = mf.FormatMessage( "You have {number, plural, thatsalot {a shitload of notifications} other {# notifications}}", - new Dictionary { { "number", 1001 } }); + new Dictionary { { "number", 1001 } }, + En); Assert.Equal("You have a shitload of notifications", actual); } } + [Fact] + public void FormatMessage_uses_constructor_culture_as_default() + { + var mf = new MessageFormatter(culture: DaDk); + + // Should use da-DK formatting without specifying culture on FormatMessage. + var result = mf.FormatMessage("{value, number}", new Dictionary { { "value", 1234.5m } }); + Assert.Equal("1.234,5", result); + } + + [Fact] + public void FormatMessage_with_culture_override() + { + var mf = new MessageFormatter(culture: EnUs); + + // Without override, uses the constructor culture (en-US). + var resultUs = mf.FormatMessage("{value, number}", new Dictionary { { "value", 1234.5m } }); + Assert.Equal("1,234.5", resultUs); + + // With override, uses da-DK formatting (period as thousands separator, comma as decimal). + var resultDk = mf.FormatMessage( + "{value, number}", + new Dictionary { { "value", 1234.5m } }, + DaDk); + Assert.Equal("1.234,5", resultDk); + } + + [Fact] + public void FormatMessage_culture_override_propagates_to_nested_formatting() + { + var mf = new MessageFormatter(); + + // The culture override should propagate through nested formatting (e.g. select -> number). + var result = mf.FormatMessage( + "{gender, select, male {He earned {amount, number}} other {They earned {amount, number}}}", + new Dictionary { { "gender", "male" }, { "amount", 1234.5m } }, + DaDk); + Assert.Equal("He earned 1.234,5", result); + } + #endregion -} \ No newline at end of file +} diff --git a/src/Jeffijoe.MessageFormat.Tests/MessageFormatterIssues.cs b/src/Jeffijoe.MessageFormat.Tests/MessageFormatterIssues.cs index 92c115a..acb2cd1 100644 --- a/src/Jeffijoe.MessageFormat.Tests/MessageFormatterIssues.cs +++ b/src/Jeffijoe.MessageFormat.Tests/MessageFormatterIssues.cs @@ -1,6 +1,6 @@ -// MessageFormat for .NET +// MessageFormat for .NET // - MessageFormatter_full_integration_tests.cs -// +// // Author: Jeff Hansen // Copyright (C) Jeff Hansen 2015. All rights reserved. @@ -15,6 +15,8 @@ namespace Jeffijoe.MessageFormat.Tests; /// public class MessageFormatterIssues { + private static readonly CultureInfo En = CultureInfo.GetCultureInfo("en"); + [Fact] public void Issue13_Bad_escaping_on_pound_symbol() { @@ -42,7 +44,7 @@ public void Issue27_WhiteSpace_in_identifiers_is_ignored() [Fact] public void Issue31_IDictionary_interface_support() { - var subject = new MessageFormatter(culture: CultureInfo.GetCultureInfo("en-US")); + var subject = new MessageFormatter(); IDictionary idict = new Dictionary { @@ -54,14 +56,14 @@ public void Issue31_IDictionary_interface_support() ["string"] = "value" }; - Assert.Equal("value", subject.FormatMessage("{string}", idict)); - Assert.Equal("value", subject.FormatMessage("{string}", idictNullable!)); + Assert.Equal("value", subject.FormatMessage("{string}", idict, En)); + Assert.Equal("value", subject.FormatMessage("{string}", idictNullable!, En)); } [Fact] public void Issue34_Newlines_are_stripped() { - var subject = new MessageFormatter(culture: CultureInfo.GetCultureInfo("en-US")); + var subject = new MessageFormatter(); const string Expected = "Single text which will not change.\nSummary:\nAccepted\nData:\n-X\n-Y\n-Z"; @@ -70,14 +72,14 @@ public void Issue34_Newlines_are_stripped() new { acceptedData = "\n-X\n-Y\n-Z" - }); + }, En); Assert.Equal(Expected, result); } [Fact] public void Issue45_Url_should_not_be_parsed_as_extension() { - var subject = new MessageFormatter(culture: CultureInfo.GetCultureInfo("en-US")); + var subject = new MessageFormatter(); IDictionary dict = new Dictionary { @@ -86,7 +88,7 @@ public void Issue45_Url_should_not_be_parsed_as_extension() var result = subject.FormatMessage( "{cond, select, foo{https://www.google.com/} other{https://www.bing.com/}}", - dict); + dict, En); Assert.Equal("https://www.google.com/", result); } -} \ No newline at end of file +} diff --git a/src/Jeffijoe.MessageFormat.Tests/MessageFormatterStringExtensionTests.cs b/src/Jeffijoe.MessageFormat.Tests/MessageFormatterStringExtensionTests.cs index 1f55141..39b910c 100644 --- a/src/Jeffijoe.MessageFormat.Tests/MessageFormatterStringExtensionTests.cs +++ b/src/Jeffijoe.MessageFormat.Tests/MessageFormatterStringExtensionTests.cs @@ -4,6 +4,7 @@ // Author: Jeff Hansen // Copyright (C) Jeff Hansen 2015. All rights reserved. +using System.Globalization; using System.Threading.Tasks; using Xunit; @@ -26,12 +27,14 @@ public class MessageFormatterStringExtensionTests [Fact] public async Task FormatMessage_with_multiple_tasks() { - var pattern = "Copying {fileCount, plural, one {one file} other{# files}}."; + const string Pattern = "Copying {fileCount, plural, one {one file} other{# files}}."; + + var en = CultureInfo.GetCultureInfo("en"); // 2 with the same message to test there are no issues with caching with multiple threads. - var t1 = Task.Run(() => MessageFormatter.Format(pattern, new { fileCount = 1 })); - var t2 = Task.Run(() => MessageFormatter.Format(pattern, new { fileCount = 1 })); - var t3 = Task.Run(() => MessageFormatter.Format(pattern, new { fileCount = 5 })); + var t1 = Task.Run(() => MessageFormatter.Format(Pattern, new { fileCount = 1 }, en)); + var t2 = Task.Run(() => MessageFormatter.Format(Pattern, new { fileCount = 1 }, en)); + var t3 = Task.Run(() => MessageFormatter.Format(Pattern, new { fileCount = 5 }, en)); await Task.WhenAll(t1, t2, t3); Assert.Equal("Copying one file.", t1.Result); diff --git a/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/ParserTests.cs b/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/ParserTests.cs index a17f8a5..08bc95a 100644 --- a/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/ParserTests.cs +++ b/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/ParserTests.cs @@ -2,7 +2,6 @@ using Jeffijoe.MessageFormat.MetadataGenerator.Plural.Parsing.AST; using System; -using System.Collections.Generic; using System.Xml; using Xunit; diff --git a/src/Jeffijoe.MessageFormat.Tests/Parsing/LiteralParserTests.cs b/src/Jeffijoe.MessageFormat.Tests/Parsing/LiteralParserTests.cs index de7237a..1b04c6a 100644 --- a/src/Jeffijoe.MessageFormat.Tests/Parsing/LiteralParserTests.cs +++ b/src/Jeffijoe.MessageFormat.Tests/Parsing/LiteralParserTests.cs @@ -4,7 +4,6 @@ // Author: Jeff Hansen // Copyright (C) Jeff Hansen 2015. All rights reserved. -using System; using System.Linq; using System.Text; diff --git a/src/Jeffijoe.MessageFormat.Tests/TestHelpers/FakeMessageFormatter.cs b/src/Jeffijoe.MessageFormat.Tests/TestHelpers/FakeMessageFormatter.cs index 59264a3..bc3819d 100644 --- a/src/Jeffijoe.MessageFormat.Tests/TestHelpers/FakeMessageFormatter.cs +++ b/src/Jeffijoe.MessageFormat.Tests/TestHelpers/FakeMessageFormatter.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Globalization; namespace Jeffijoe.MessageFormat.Tests.TestHelpers; @@ -9,7 +10,7 @@ internal class FakeMessageFormatter : IMessageFormatter { public CustomValueFormatter? CustomValueFormatter { get; set; } - public string FormatMessage(string pattern, IReadOnlyDictionary argsMap) => pattern; + public string FormatMessage(string pattern, IReadOnlyDictionary argsMap, CultureInfo? culture = null) => pattern; public string FormatMessage(string pattern, object args) => pattern; } \ No newline at end of file diff --git a/src/Jeffijoe.MessageFormat.Tests/TestHelpers/UseCultureAttribute.cs b/src/Jeffijoe.MessageFormat.Tests/TestHelpers/UseCultureAttribute.cs new file mode 100644 index 0000000..f3bdf33 --- /dev/null +++ b/src/Jeffijoe.MessageFormat.Tests/TestHelpers/UseCultureAttribute.cs @@ -0,0 +1,73 @@ +using System; +using System.Globalization; +using System.Reflection; +using System.Threading; +using Xunit.Sdk; + +namespace Jeffijoe.MessageFormat.Tests.TestHelpers; + +/// +/// Apply this attribute to your test method to replace the +/// and +/// with another culture. +/// +/// +/// Replaces the culture and UI culture of the current thread with +/// and +/// +/// The name of the culture. +/// The name of the UI culture. +[AttributeUsage(AttributeTargets.Assembly | AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = false, Inherited = true)] +public class UseCultureAttribute(string culture, string uiCulture) : BeforeAfterTestAttribute +{ + private readonly Lazy culture = new(() => new CultureInfo(culture, false)); + private readonly Lazy uiCulture = new(() => new CultureInfo(uiCulture, false)); + + private CultureInfo? originalCulture; + private CultureInfo? originalUiCulture; + + /// + /// Replaces the culture and UI culture of the current thread with + /// + /// + /// The name of the culture. + /// + /// This constructor overload uses for both Culture and UICulture. + /// + public UseCultureAttribute(string culture) + : this(culture, culture) { } + + /// + /// Stores the current + /// and + /// and replaces them with the new cultures defined in the constructor. + /// + /// The method under test + public override void Before(MethodInfo methodUnderTest) + { + originalCulture = Thread.CurrentThread.CurrentCulture; + originalUiCulture = Thread.CurrentThread.CurrentUICulture; + + Thread.CurrentThread.CurrentCulture = culture.Value; + Thread.CurrentThread.CurrentUICulture = uiCulture.Value; + + CultureInfo.CurrentCulture.ClearCachedData(); + CultureInfo.CurrentUICulture.ClearCachedData(); + } + + /// + /// Restores the original and + /// to + /// + /// The method under test + public override void After(MethodInfo methodUnderTest) + { + if (originalCulture is not null) + Thread.CurrentThread.CurrentCulture = originalCulture; + if (originalUiCulture is not null) + Thread.CurrentThread.CurrentUICulture = originalUiCulture; + + CultureInfo.CurrentCulture.ClearCachedData(); + CultureInfo.CurrentUICulture.ClearCachedData(); + } +} \ No newline at end of file diff --git a/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralFormatter.cs b/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralFormatter.cs index 9e18e20..a2bf1b5 100644 --- a/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralFormatter.cs +++ b/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralFormatter.cs @@ -159,7 +159,7 @@ public string Format(CultureInfo culture, ctx, offset); var result = this.ReplaceNumberLiterals(pluralized, ctx.Number); - var formatted = messageFormatter.FormatMessage(result, args); + var formatted = messageFormatter.FormatMessage(result, args, culture); return formatted; } diff --git a/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralRulesMetadata.cs b/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralRulesMetadata.cs index 3c18382..ee9cab0 100644 --- a/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralRulesMetadata.cs +++ b/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralRulesMetadata.cs @@ -1,6 +1,4 @@ -using System.Diagnostics.CodeAnalysis; - -namespace Jeffijoe.MessageFormat.Formatting.Formatters; +namespace Jeffijoe.MessageFormat.Formatting.Formatters; [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] internal static partial class PluralRulesMetadata diff --git a/src/Jeffijoe.MessageFormat/Formatting/Formatters/SelectFormatter.cs b/src/Jeffijoe.MessageFormat/Formatting/Formatters/SelectFormatter.cs index c254721..0a5492c 100644 --- a/src/Jeffijoe.MessageFormat/Formatting/Formatters/SelectFormatter.cs +++ b/src/Jeffijoe.MessageFormat/Formatting/Formatters/SelectFormatter.cs @@ -73,7 +73,7 @@ public string Format(CultureInfo culture, { if (str == keyedBlock.Key) { - return messageFormatter.FormatMessage(keyedBlock.BlockText, args); + return messageFormatter.FormatMessage(keyedBlock.BlockText, args, culture); } if (keyedBlock.Key == OtherKey) @@ -88,7 +88,7 @@ public string Format(CultureInfo culture, "'other' option not found in pattern, and variable was not present in collection."); } - return messageFormatter.FormatMessage(other.BlockText, args); + return messageFormatter.FormatMessage(other.BlockText, args, culture); } #endregion diff --git a/src/Jeffijoe.MessageFormat/IMessageFormatter.cs b/src/Jeffijoe.MessageFormat/IMessageFormatter.cs index 41d6bbc..2029b4a 100644 --- a/src/Jeffijoe.MessageFormat/IMessageFormatter.cs +++ b/src/Jeffijoe.MessageFormat/IMessageFormatter.cs @@ -4,6 +4,7 @@ // Copyright (C) Jeff Hansen 2014. All rights reserved. using System.Collections.Generic; +using System.Globalization; namespace Jeffijoe.MessageFormat; @@ -32,10 +33,13 @@ public interface IMessageFormatter /// /// The arguments. /// + /// + /// The culture to use, or null to use . + /// /// /// The . /// - string FormatMessage(string pattern, IReadOnlyDictionary argsMap); + string FormatMessage(string pattern, IReadOnlyDictionary argsMap, CultureInfo? culture = null); #endregion } \ No newline at end of file diff --git a/src/Jeffijoe.MessageFormat/MessageFormatter.cs b/src/Jeffijoe.MessageFormat/MessageFormatter.cs index dbce2e8..b92fdc2 100644 --- a/src/Jeffijoe.MessageFormat/MessageFormatter.cs +++ b/src/Jeffijoe.MessageFormat/MessageFormatter.cs @@ -66,12 +66,13 @@ public class MessageFormatter : IMessageFormatter /// The use Cache. /// /// - /// The culture to use. Defaults to if not specified. + /// The default culture to use, or null to use . /// /// /// The custom value formatter to use. Can be null. /// - public MessageFormatter(bool useCache = true, CultureInfo? culture = null, + public MessageFormatter(bool useCache = true, + CultureInfo? culture = null, CustomValueFormatter? customValueFormatter = null) : this( patternParser: new PatternParser(new LiteralParser()), @@ -95,7 +96,7 @@ public MessageFormatter(bool useCache = true, CultureInfo? culture = null, /// if set to true uses the cache. /// /// - /// The culture to use. Formatters may need this. Defaults to if not specified. + /// The default culture to use, or null to use . /// /// /// The custom value formatter to use. Can be null. @@ -109,8 +110,8 @@ internal MessageFormatter( { this.patternParser = patternParser ?? throw new ArgumentNullException("patternParser"); this.library = library ?? throw new ArgumentNullException("library"); + this.Culture = culture; this.CustomValueFormatter = customValueFormatter; - this.Culture = culture ?? CultureInfo.CurrentCulture; if (useCache) { this.cache = new ConcurrentDictionary(); @@ -121,6 +122,11 @@ internal MessageFormatter( #region Public Properties + /// + /// The default culture to use for formatting, or null to use . + /// + public CultureInfo? Culture { get; } + /// /// The custom value formatter to use for formats like `number`, `date`, `time` etc. /// @@ -137,14 +143,6 @@ public IFormatterLibrary Formatters get { return this.library; } } - /// - /// Gets or sets the culture. - /// - /// - /// The culture. - /// - public CultureInfo Culture { get; set; } - /// /// Gets the custom cardinal pluralizers dictionary from the , if set. Key is the locale. /// These are the pluralizers used to translate e.g., {count, plural, one {1 book} other {# books}} @@ -193,7 +191,7 @@ public IDictionary? OrdinalPluralizers /// Formats the specified pattern with the specified data. /// /// - /// This method calls + /// This method calls /// on a singleton instance using a lock. /// Do not use in a tight loop, as a lock is being used to ensure thread safety. /// @@ -203,14 +201,17 @@ public IDictionary? OrdinalPluralizers /// /// The data. /// + /// + /// The culture to use, or null to use . + /// /// /// The formatted message. /// - public static string Format(string pattern, IReadOnlyDictionary data) + public static string Format(string pattern, IReadOnlyDictionary data, CultureInfo? culture = null) { lock (Lock) { - return Instance.FormatMessage(pattern, data); + return Instance.FormatMessage(pattern, data, culture); } } @@ -218,7 +219,7 @@ public static string Format(string pattern, IReadOnlyDictionary /// Formats the specified pattern with the specified data. /// /// This method calls - /// + /// /// on a singleton instance using a lock. /// Do not use in a tight loop, as a lock is being used to ensure thread safety. /// @@ -227,16 +228,19 @@ public static string Format(string pattern, IReadOnlyDictionary /// /// The data. /// + /// + /// The culture to use, or null to use . + /// /// /// The formatted message. /// [OverloadResolutionPriority(-1)] [RequiresUnreferencedCode("This method uses the FormatMessage extension which uses reflection to convert object into dictionary")] - public static string Format(string pattern, object data) + public static string Format(string pattern, object data, CultureInfo? culture = null) { lock (Lock) { - return Instance.FormatMessage(pattern, data); + return Instance.FormatMessage(pattern, data, culture); } } @@ -249,15 +253,19 @@ public static string Format(string pattern, object data) /// /// The arguments. /// + /// + /// The culture to use, or null to use . + /// /// /// The . /// - public string FormatMessage(string pattern, IReadOnlyDictionary args) + public string FormatMessage(string pattern, IReadOnlyDictionary args, CultureInfo? culture = null) { /* * We are assuming the formatters are ordered correctly * - that is, from left to right, string-wise. */ + var activeCulture = culture ?? this.Culture ?? CultureInfo.CurrentCulture; var sourceBuilder = StringBuilderPool.Get(); try @@ -277,7 +285,7 @@ public string FormatMessage(string pattern, IReadOnlyDictionary } // Double dispatch, yeah! - var result = formatter.Format(this.Culture, request, args, value, this); + var result = formatter.Format(activeCulture, request, args, value, this); // First, we remove the literal from the source. var sourceLiteral = request.SourceLiteral; diff --git a/src/Jeffijoe.MessageFormat/MessageFormatterExtensions.cs b/src/Jeffijoe.MessageFormat/MessageFormatterExtensions.cs index 52787e8..8ec9d7c 100644 --- a/src/Jeffijoe.MessageFormat/MessageFormatterExtensions.cs +++ b/src/Jeffijoe.MessageFormat/MessageFormatterExtensions.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Globalization; using System.Runtime.CompilerServices; using Jeffijoe.MessageFormat.Helpers; @@ -22,15 +23,19 @@ public static class MessageFormatterExtensions /// /// The arguments. /// + /// + /// The culture to use, or null to use . + /// /// /// The . /// public static string FormatMessage( this IMessageFormatter formatter, string pattern, - IDictionary args) + IDictionary args, + CultureInfo? culture = null) { - return formatter.FormatMessage(pattern, (IReadOnlyDictionary)args); + return formatter.FormatMessage(pattern, (IReadOnlyDictionary)args, culture); } /// @@ -45,13 +50,16 @@ public static string FormatMessage( /// /// The arguments. /// + /// + /// The culture to use, or null to use . + /// /// /// The . /// [OverloadResolutionPriority(-1)] [RequiresUnreferencedCode("This method uses the ToDictionary extension which uses reflection to convert object into dictionary")] - public static string FormatMessage(this IMessageFormatter formatter, string pattern, object args) + public static string FormatMessage(this IMessageFormatter formatter, string pattern, object args, CultureInfo? culture = null) { - return formatter.FormatMessage(pattern, args.ToDictionary()); + return formatter.FormatMessage(pattern, args.ToDictionary(), culture); } } From bf4999931b3af4921f4ce21913bed81189707444 Mon Sep 17 00:00:00 2001 From: Jeff Hansen Date: Sat, 21 Feb 2026 12:36:41 -0500 Subject: [PATCH 95/98] Upgrade packages and migrate to incremental generator --- ...joe.MessageFormat.MetadataGenerator.csproj | 4 +- .../Plural/PluralLanguagesGenerator.cs | 44 +++++++------------ .../PluralRulesMetadataGenerator.cs | 1 - .../Jeffijoe.MessageFormat.Tests.csproj | 13 +++--- .../MessageFormatterStringExtensionTests.cs | 6 +-- .../Jeffijoe.MessageFormat.csproj | 4 +- 6 files changed, 28 insertions(+), 44 deletions(-) diff --git a/src/Jeffijoe.MessageFormat.MetadataGenerator/Jeffijoe.MessageFormat.MetadataGenerator.csproj b/src/Jeffijoe.MessageFormat.MetadataGenerator/Jeffijoe.MessageFormat.MetadataGenerator.csproj index 4a55968..241670f 100644 --- a/src/Jeffijoe.MessageFormat.MetadataGenerator/Jeffijoe.MessageFormat.MetadataGenerator.csproj +++ b/src/Jeffijoe.MessageFormat.MetadataGenerator/Jeffijoe.MessageFormat.MetadataGenerator.csproj @@ -15,11 +15,11 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/PluralLanguagesGenerator.cs b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/PluralLanguagesGenerator.cs index d47015c..f12b098 100644 --- a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/PluralLanguagesGenerator.cs +++ b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/PluralLanguagesGenerator.cs @@ -1,39 +1,30 @@ -using Jeffijoe.MessageFormat.MetadataGenerator.Plural.Parsing; +using Jeffijoe.MessageFormat.MetadataGenerator.Plural.Parsing; using Jeffijoe.MessageFormat.MetadataGenerator.Plural.SourceGeneration; using Microsoft.CodeAnalysis; -using System; using System.IO; using System.Xml; namespace Jeffijoe.MessageFormat.MetadataGenerator.Plural; [Generator] -public class PluralLanguagesGenerator : ISourceGenerator +public class PluralLanguagesGenerator : IIncrementalGenerator { - public void Execute(GeneratorExecutionContext context) + public void Initialize(IncrementalGeneratorInitializationContext context) { - var excludeLocales = ReadExcludeLocales(context); - var rules = GetRules(excludeLocales); - var generator = new PluralRulesMetadataGenerator(rules); - var sourceCode = generator.GenerateClass(); - - context.AddSource("PluralRulesMetadata.Generated.cs", sourceCode); - } - - private string[] ReadExcludeLocales(GeneratorExecutionContext context) - { - if(context.AnalyzerConfigOptions.GlobalOptions.TryGetValue($"build_property.PluralLanguagesMetadataExcludeLocales", out var value)) + context.RegisterPostInitializationOutput(static spc => { - var locales = value.Trim().Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); - return locales; - } + // Not currently excluding any locales. + var rules = GetRules(excludedLocales: []); + var generator = new PluralRulesMetadataGenerator(rules); + var sourceCode = generator.GenerateClass(); - return Array.Empty(); + spc.AddSource("PluralRulesMetadata.Generated.cs", sourceCode); + }); } - private PluralRuleSet GetRules(string[] excludedLocales) + private static PluralRuleSet GetRules(string[] excludedLocales) { PluralRuleSet ruleIndex = new(); foreach (var ruleset in new[] { "plurals.xml", "ordinals.xml" }) @@ -49,14 +40,9 @@ private PluralRuleSet GetRules(string[] excludedLocales) return ruleIndex; } - - private Stream GetRulesContentStream(string cldrFileName) - { - return typeof(PluralLanguagesGenerator).Assembly.GetManifestResourceStream($"Jeffijoe.MessageFormat.MetadataGenerator.data.{cldrFileName}")!; - } - - public void Initialize(GeneratorInitializationContext context) + private static Stream GetRulesContentStream(string cldrFileName) { - + return typeof(PluralLanguagesGenerator).Assembly + .GetManifestResourceStream($"Jeffijoe.MessageFormat.MetadataGenerator.data.{cldrFileName}")!; } -} \ No newline at end of file +} diff --git a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/SourceGeneration/PluralRulesMetadataGenerator.cs b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/SourceGeneration/PluralRulesMetadataGenerator.cs index caf0dec..bc3a132 100644 --- a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/SourceGeneration/PluralRulesMetadataGenerator.cs +++ b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/SourceGeneration/PluralRulesMetadataGenerator.cs @@ -33,7 +33,6 @@ public string GenerateClass() // Export a constant for the normalized root locale to match the logic we're using internally. // This way the rest of the lib's locale chaining can continue to work if we swap out // normalization internally. - var rootRules = _rules.RuleIndicesByLocale[PluralRuleSet.RootLocale]; WriteLine($"public static readonly string RootLocale = \"{PluralRuleSet.RootLocale}\";"); // Generate a method for each unique rule, by index, that chooses the plural form diff --git a/src/Jeffijoe.MessageFormat.Tests/Jeffijoe.MessageFormat.Tests.csproj b/src/Jeffijoe.MessageFormat.Tests/Jeffijoe.MessageFormat.Tests.csproj index c0407ff..31baa32 100644 --- a/src/Jeffijoe.MessageFormat.Tests/Jeffijoe.MessageFormat.Tests.csproj +++ b/src/Jeffijoe.MessageFormat.Tests/Jeffijoe.MessageFormat.Tests.csproj @@ -10,14 +10,13 @@ - - - + + - - - - + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Jeffijoe.MessageFormat.Tests/MessageFormatterStringExtensionTests.cs b/src/Jeffijoe.MessageFormat.Tests/MessageFormatterStringExtensionTests.cs index 39b910c..9aa57c3 100644 --- a/src/Jeffijoe.MessageFormat.Tests/MessageFormatterStringExtensionTests.cs +++ b/src/Jeffijoe.MessageFormat.Tests/MessageFormatterStringExtensionTests.cs @@ -37,9 +37,9 @@ public async Task FormatMessage_with_multiple_tasks() var t3 = Task.Run(() => MessageFormatter.Format(Pattern, new { fileCount = 5 }, en)); await Task.WhenAll(t1, t2, t3); - Assert.Equal("Copying one file.", t1.Result); - Assert.Equal("Copying one file.", t2.Result); - Assert.Equal("Copying 5 files.", t3.Result); + Assert.Equal("Copying one file.", await t1); + Assert.Equal("Copying one file.", await t2); + Assert.Equal("Copying 5 files.", await t3); } #endregion diff --git a/src/Jeffijoe.MessageFormat/Jeffijoe.MessageFormat.csproj b/src/Jeffijoe.MessageFormat/Jeffijoe.MessageFormat.csproj index 2eebfd9..dc2bdc2 100644 --- a/src/Jeffijoe.MessageFormat/Jeffijoe.MessageFormat.csproj +++ b/src/Jeffijoe.MessageFormat/Jeffijoe.MessageFormat.csproj @@ -20,8 +20,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive From 0a9f9ffa3c484f9ac43c21a5f5e0c3e3f39db2ec Mon Sep 17 00:00:00 2001 From: Jeff Hansen Date: Sat, 21 Feb 2026 12:41:24 -0500 Subject: [PATCH 96/98] Remove unused build property --- src/Jeffijoe.MessageFormat/Jeffijoe.MessageFormat.csproj | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/Jeffijoe.MessageFormat/Jeffijoe.MessageFormat.csproj b/src/Jeffijoe.MessageFormat/Jeffijoe.MessageFormat.csproj index dc2bdc2..e17ea51 100644 --- a/src/Jeffijoe.MessageFormat/Jeffijoe.MessageFormat.csproj +++ b/src/Jeffijoe.MessageFormat/Jeffijoe.MessageFormat.csproj @@ -35,8 +35,5 @@ - - - From eac9acd7dac17c3bdd3b0116d048eb59f34b63ca Mon Sep 17 00:00:00 2001 From: Jeff Hansen Date: Sat, 21 Feb 2026 12:55:19 -0500 Subject: [PATCH 97/98] Update CI to use .NET 10 --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8c07a19..0099f60 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,9 +23,9 @@ jobs: fetch-depth: 100 - name: Setup .NET - uses: actions/setup-dotnet@v4 + uses: actions/setup-dotnet@v5.1.0 with: - dotnet-version: 8.0.x + dotnet-version: 10.0.x - name: Install dependencies working-directory: ./src From 6798a5b354dd7ea5a94289e550fe4f673a88b14d Mon Sep 17 00:00:00 2001 From: Jeff Hansen Date: Sat, 21 Feb 2026 16:52:22 -0500 Subject: [PATCH 98/98] Replace `Microsoft.Extensions.ObjectPool` with built-in Roslyn-based pool This may allocate more during heavy contention, but is ~50% faster overall --- .gitignore | 1 + .../Jeffijoe.MessageFormat.Benchmarks.csproj | 21 ++++ .../MessageFormat.snk | Bin 0 -> 596 bytes .../MessageFormatterBenchmarks.cs | 104 ++++++++++++++++++ .../PoolBenchmarks.cs | 45 ++++++++ .../Program.cs | 3 + .../ObjectPoolTests.cs | 79 +++++++++++++ .../Jeffijoe.MessageFormat.csproj | 5 +- src/Jeffijoe.MessageFormat/ObjectPool.cs | 96 ++++++++++++++++ .../Properties/AssemblyInfo.cs | 1 + .../StringBuilderPool.cs | 25 +++-- src/MessageFormat.sln | 42 +++++++ 12 files changed, 410 insertions(+), 12 deletions(-) create mode 100644 src/Jeffijoe.MessageFormat.Benchmarks/Jeffijoe.MessageFormat.Benchmarks.csproj create mode 100644 src/Jeffijoe.MessageFormat.Benchmarks/MessageFormat.snk create mode 100644 src/Jeffijoe.MessageFormat.Benchmarks/MessageFormatterBenchmarks.cs create mode 100644 src/Jeffijoe.MessageFormat.Benchmarks/PoolBenchmarks.cs create mode 100644 src/Jeffijoe.MessageFormat.Benchmarks/Program.cs create mode 100644 src/Jeffijoe.MessageFormat.Tests/ObjectPoolTests.cs create mode 100644 src/Jeffijoe.MessageFormat/ObjectPool.cs diff --git a/.gitignore b/.gitignore index 2e5faf7..45158ca 100644 --- a/.gitignore +++ b/.gitignore @@ -29,3 +29,4 @@ imagecache /src/.vs src/.idea/.idea.MessageFormat/.idea/workspace.xml .DS_Store +BenchmarkDotNet.Artifacts/ diff --git a/src/Jeffijoe.MessageFormat.Benchmarks/Jeffijoe.MessageFormat.Benchmarks.csproj b/src/Jeffijoe.MessageFormat.Benchmarks/Jeffijoe.MessageFormat.Benchmarks.csproj new file mode 100644 index 0000000..6315f90 --- /dev/null +++ b/src/Jeffijoe.MessageFormat.Benchmarks/Jeffijoe.MessageFormat.Benchmarks.csproj @@ -0,0 +1,21 @@ + + + + Exe + net10.0 + latest + enable + enable + True + MessageFormat.snk + + + + + + + + + + + diff --git a/src/Jeffijoe.MessageFormat.Benchmarks/MessageFormat.snk b/src/Jeffijoe.MessageFormat.Benchmarks/MessageFormat.snk new file mode 100644 index 0000000000000000000000000000000000000000..ba4de3e31dacaa7eb2ddabadc98d074accc7d7cc GIT binary patch literal 596 zcmV-a0;~N80ssI2Bme+XQ$aES1ONa50096`4Z#Z7hc{9AhCyn5F*M|Jtu$1p6OJa& zmtgPnL2#eN4r$qiwBYJdD^@56kVB&!;p77J>g)ftcP{Oo^QWofpAsfB@s%l|>6m~8 zD4x)o9w|z`6BR1{yjg=>FYhRM*kl`4O{#xU5}52Siawh%C>h{fy6Ox3bN&zp6~D(R z2K8Qu)4ULXeB6@Eo;>OAD1!rwnPhq+3jN=#`>@szEkB3dbzg^ofOX&Xo=fOAMYIl4 ziKlJlbg@?#YV^prHihp9*0vbvf=Q?eXCnn zDN>{%hm_P|CUWYx*6$aj(9(vH?{)sU@Zl4s0PsVrF;>qfv&cvg_l#&(*Mw^0?C_5E zq@z$PvjME|HbsIZ^>ckxhax!~E62ZWF*7?C_II?0MM zgDq^8RmE0-na2lq)0PzU+N`X?;Qx}W$ISt+yQOgZV96rtbrikTlHQ$e^LbKY=vKR! zW>VmgM^K(0A!=b}p>UEj4=E@=-X390=@hCUhv;XS1&TukEwQ@RDCKMYOzGjNeBx30 zTh=>(-k7l2Dxw4{1x=FmyW$xq=;CmQ#=?^03!)&e^8W1uf~B1!_Hmdpyja>wq=okC z4XyvaUNWo@5{N_C>LU-12X$rBso_TXBVJU$0V)v=u123+H@_fnCAqj!Rgdet5urTd iS?#=Ksj{+Dm-ig3!o++c!PiDd%I>d>yHI|ZSd1ELG%EW5 literal 0 HcmV?d00001 diff --git a/src/Jeffijoe.MessageFormat.Benchmarks/MessageFormatterBenchmarks.cs b/src/Jeffijoe.MessageFormat.Benchmarks/MessageFormatterBenchmarks.cs new file mode 100644 index 0000000..e7cdf05 --- /dev/null +++ b/src/Jeffijoe.MessageFormat.Benchmarks/MessageFormatterBenchmarks.cs @@ -0,0 +1,104 @@ +using System.Collections.Generic; +using BenchmarkDotNet.Attributes; +using Jeffijoe.MessageFormat; + +namespace Jeffijoe.MessageFormat.Benchmarks; + +[MemoryDiagnoser] +public class MessageFormatterBenchmarks +{ + private MessageFormatter _formatter = null!; + + private readonly Dictionary _simpleArgs = new() { ["name"] = "World" }; + + private readonly Dictionary _pluralSimpleArgs = new() { ["count"] = 5 }; + + private readonly Dictionary _selectSimpleArgs = new() { ["gender"] = "male" }; + + private readonly Dictionary _pluralOffsetArgs = new() { ["count"] = 3 }; + + private readonly Dictionary _nested2Args = new() { ["gender"] = "female", ["count"] = 1 }; + + private readonly Dictionary _nested3Args = new() + { + ["gender"] = "male", + ["count"] = 2, + ["total"] = 10 + }; + + [GlobalSetup] + public void Setup() + { + _formatter = new MessageFormatter(); + } + + [Benchmark] + public string SimpleSubstitution() + { + return _formatter.FormatMessage("{name}", _simpleArgs); + } + + [Benchmark] + public string PluralSimple() + { + return _formatter.FormatMessage( + "{count, plural, one {1 thing} other {# things}}", + _pluralSimpleArgs); + } + + [Benchmark] + public string SelectSimple() + { + return _formatter.FormatMessage( + "{gender, select, male {He} female {She} other {They}}", + _selectSimpleArgs); + } + + [Benchmark] + public string PluralWithOffset() + { + return _formatter.FormatMessage( + "{count, plural, offset:1 =0 {Nobody} one {You and one other} other {You and # others}}", + _pluralOffsetArgs); + } + + [Benchmark] + public string Nested2Levels() + { + return _formatter.FormatMessage( + "{gender, select, male {{count, plural, one {He has 1 item} other {He has # items}}} female {{count, plural, one {She has 1 item} other {She has # items}}} other {{count, plural, one {They have 1 item} other {They have # items}}}}", + _nested2Args); + } + + [Benchmark] + public string Nested3Levels() + { + return _formatter.FormatMessage( + "{gender, select, male {{count, plural, one {He has 1 of {total} items} other {He has # of {total} items}}} female {{count, plural, one {She has 1 of {total} items} other {She has # of {total} items}}} other {{count, plural, one {They have 1 of {total} items} other {They have # of {total} items}}}}", + _nested3Args); + } + + [Params(1, 2, 4, 8)] + public int ThreadCount { get; set; } + + [Benchmark] + public void MultiThreadFormatMessage() + { + var args = new Dictionary { ["count"] = 5 }; + var pattern = "{count, plural, one {1 thing} other {# things}}"; + + var tasks = new Task[ThreadCount]; + for (var t = 0; t < ThreadCount; t++) + { + tasks[t] = Task.Run(() => + { + for (var i = 0; i < 1000; i++) + { + _formatter.FormatMessage(pattern, args); + } + }); + } + + Task.WaitAll(tasks); + } +} diff --git a/src/Jeffijoe.MessageFormat.Benchmarks/PoolBenchmarks.cs b/src/Jeffijoe.MessageFormat.Benchmarks/PoolBenchmarks.cs new file mode 100644 index 0000000..9948aa8 --- /dev/null +++ b/src/Jeffijoe.MessageFormat.Benchmarks/PoolBenchmarks.cs @@ -0,0 +1,45 @@ +using System.Text; +using BenchmarkDotNet.Attributes; +using Jeffijoe.MessageFormat; + +namespace Jeffijoe.MessageFormat.Benchmarks; + +[MemoryDiagnoser] +public class PoolBenchmarks +{ + private const int OperationsPerThread = 1000; + + [Benchmark] + public void SingleThreadGetReturn() + { + for (var i = 0; i < OperationsPerThread; i++) + { + var sb = StringBuilderPool.Get(); + sb.Append("test"); + StringBuilderPool.Return(sb); + } + } + + [Params(1, 2, 4, 8)] + public int ThreadCount { get; set; } + + [Benchmark] + public void MultiThreadGetReturn() + { + var tasks = new Task[ThreadCount]; + for (var t = 0; t < ThreadCount; t++) + { + tasks[t] = Task.Run(() => + { + for (var i = 0; i < OperationsPerThread; i++) + { + var sb = StringBuilderPool.Get(); + sb.Append("test"); + StringBuilderPool.Return(sb); + } + }); + } + + Task.WaitAll(tasks); + } +} diff --git a/src/Jeffijoe.MessageFormat.Benchmarks/Program.cs b/src/Jeffijoe.MessageFormat.Benchmarks/Program.cs new file mode 100644 index 0000000..c9a0467 --- /dev/null +++ b/src/Jeffijoe.MessageFormat.Benchmarks/Program.cs @@ -0,0 +1,3 @@ +using BenchmarkDotNet.Running; + +BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args); diff --git a/src/Jeffijoe.MessageFormat.Tests/ObjectPoolTests.cs b/src/Jeffijoe.MessageFormat.Tests/ObjectPoolTests.cs new file mode 100644 index 0000000..c530fd3 --- /dev/null +++ b/src/Jeffijoe.MessageFormat.Tests/ObjectPoolTests.cs @@ -0,0 +1,79 @@ +using System.Text; +using System.Threading.Tasks; + +using Xunit; + +namespace Jeffijoe.MessageFormat.Tests; + +public class ObjectPoolTests +{ + [Fact] + public void Allocate_WhenPoolEmpty_ReturnsNewObject() + { + var pool = new ObjectPool(() => new StringBuilder()); + var sb = pool.Allocate(); + Assert.NotNull(sb); + } + + [Fact] + public void Free_ThenAllocate_ReturnsSameInstance() + { + var pool = new ObjectPool(() => new StringBuilder()); + var sb = pool.Allocate(); + pool.Free(sb); + var sb2 = pool.Allocate(); + Assert.Same(sb, sb2); + } + + [Fact] + public void Allocate_BeyondPoolSize_CreatesNewObjects() + { + var pool = new ObjectPool(() => new StringBuilder(), size: 2); + var a = pool.Allocate(); + var b = pool.Allocate(); + var c = pool.Allocate(); + Assert.NotSame(a, b); + Assert.NotSame(b, c); + Assert.NotSame(a, c); + } + + [Fact] + public void Free_BeyondPoolSize_DoesNotThrow() + { + var pool = new ObjectPool(() => new StringBuilder(), size: 2); + var a = pool.Allocate(); + var b = pool.Allocate(); + var c = pool.Allocate(); + pool.Free(a); + pool.Free(b); + pool.Free(c); // exceeds pool size, should silently discard + } + + [Fact] + public async Task ConcurrentAllocateAndFree_DoesNotThrow() + { + var pool = new ObjectPool(() => new StringBuilder()); + const int ThreadCount = 8; + const int Iterations = 1000; + + var tasks = new Task[ThreadCount]; + for (var t = 0; t < ThreadCount; t++) + { + tasks[t] = Task.Run(() => + { + for (var i = 0; i < Iterations; i++) + { + var sb = pool.Allocate(); + sb.Append("test"); + var output = sb.ToString(); + // Assert we didn't get a dirty builder with data still left in it. + Assert.Equal("test", output); + sb.Clear(); + pool.Free(sb); + } + }); + } + + await Task.WhenAll(tasks); + } +} diff --git a/src/Jeffijoe.MessageFormat/Jeffijoe.MessageFormat.csproj b/src/Jeffijoe.MessageFormat/Jeffijoe.MessageFormat.csproj index e17ea51..793e4f7 100644 --- a/src/Jeffijoe.MessageFormat/Jeffijoe.MessageFormat.csproj +++ b/src/Jeffijoe.MessageFormat/Jeffijoe.MessageFormat.csproj @@ -20,7 +20,6 @@ - all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -35,5 +34,9 @@ + + + + diff --git a/src/Jeffijoe.MessageFormat/ObjectPool.cs b/src/Jeffijoe.MessageFormat/ObjectPool.cs new file mode 100644 index 0000000..6b26c26 --- /dev/null +++ b/src/Jeffijoe.MessageFormat/ObjectPool.cs @@ -0,0 +1,96 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +// Ported from Roslyn, see: https://github.com/dotnet/roslyn/blob/main/src/Dependencies/PooledObjects/ObjectPool%601.cs. + +using System; +using System.Runtime.CompilerServices; +using System.Threading; + +namespace Jeffijoe.MessageFormat; + +/// +/// Generic implementation of object pooling pattern with predefined pool size limit. The main purpose +/// is that limited number of frequently used objects can be kept in the pool for further recycling. +/// +/// The type of objects to pool. +internal sealed class ObjectPool(Func factory, int size) + where T : class +{ + private readonly Element[] _items = new Element[size - 1]; + private T? _firstItem; + + public ObjectPool(Func factory) + : this(factory, Environment.ProcessorCount * 2) + { + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public T Allocate() + { + var item = _firstItem; + + if (item is null || item != Interlocked.CompareExchange(ref _firstItem, null, item)) + { + item = AllocateSlow(); + } + + return item; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Free(T obj) + { + if (_firstItem is null) + { + _firstItem = obj; + } + else + { + FreeSlow(obj); + } + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private T AllocateSlow() + { + foreach (ref var element in _items.AsSpan()) + { + var instance = element.Value; + + if (instance is null) + { + continue; + } + + if (instance == Interlocked.CompareExchange(ref element.Value, null, instance)) + { + return instance; + } + } + + + return factory(); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private void FreeSlow(T obj) + { + foreach (ref var element in _items.AsSpan()) + { + if (element.Value is not null) + { + continue; + } + + element.Value = obj; + break; + } + } + + private struct Element + { + internal T? Value; + } +} diff --git a/src/Jeffijoe.MessageFormat/Properties/AssemblyInfo.cs b/src/Jeffijoe.MessageFormat/Properties/AssemblyInfo.cs index f9ea5d5..8df6583 100644 --- a/src/Jeffijoe.MessageFormat/Properties/AssemblyInfo.cs +++ b/src/Jeffijoe.MessageFormat/Properties/AssemblyInfo.cs @@ -6,3 +6,4 @@ using System.Runtime.CompilerServices; [assembly: InternalsVisibleTo("Jeffijoe.MessageFormat.Tests, PublicKey=00240000048000009400000006020000002400005253413100040000010001004f0dc10ad8873751f986416a7d3134e473ad3454a7138e26cf9760eff341709fc50e69d985b4e0ea512b5628079043a31ce1e402f4eaebffb5772eed9ef3a7a9e39f122633f19529a1e9988005289ed09a1e294abe13152afebc59835c2fef2879d8641b564daa7f511298ec2f8a3e9b322819e05cbaea0bfc73fe100615bfc7")] +[assembly: InternalsVisibleTo("Jeffijoe.MessageFormat.Benchmarks, PublicKey=00240000048000009400000006020000002400005253413100040000010001004f0dc10ad8873751f986416a7d3134e473ad3454a7138e26cf9760eff341709fc50e69d985b4e0ea512b5628079043a31ce1e402f4eaebffb5772eed9ef3a7a9e39f122633f19529a1e9988005289ed09a1e294abe13152afebc59835c2fef2879d8641b564daa7f511298ec2f8a3e9b322819e05cbaea0bfc73fe100615bfc7")] diff --git a/src/Jeffijoe.MessageFormat/StringBuilderPool.cs b/src/Jeffijoe.MessageFormat/StringBuilderPool.cs index f91fc26..7d0a1cb 100644 --- a/src/Jeffijoe.MessageFormat/StringBuilderPool.cs +++ b/src/Jeffijoe.MessageFormat/StringBuilderPool.cs @@ -1,25 +1,28 @@ -using System.Text; -using Microsoft.Extensions.ObjectPool; +using System.Text; namespace Jeffijoe.MessageFormat; internal static class StringBuilderPool { - private static readonly ObjectPool SbPool; + private const int MaxBuilderCapacity = 4096; - static StringBuilderPool() - { - var shared = new DefaultObjectPoolProvider(); - SbPool = shared.CreateStringBuilderPool(); - } + private static readonly ObjectPool SbPool = new(static () => new StringBuilder()); public static StringBuilder Get() { - return SbPool.Get(); + return SbPool.Allocate(); } public static void Return(StringBuilder sb) { - SbPool.Return(sb); + // If the builder grew too large, just let it go + // rather than returning it so it can get garbage-collected. + if (sb.Capacity > MaxBuilderCapacity) + { + return; + } + + sb.Clear(); + SbPool.Free(sb); } -} \ No newline at end of file +} diff --git a/src/MessageFormat.sln b/src/MessageFormat.sln index 3a62c07..d682f2e 100644 --- a/src/MessageFormat.sln +++ b/src/MessageFormat.sln @@ -9,24 +9,66 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jeffijoe.MessageFormat.Test EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jeffijoe.MessageFormat.MetadataGenerator", "Jeffijoe.MessageFormat.MetadataGenerator\Jeffijoe.MessageFormat.MetadataGenerator.csproj", "{5431C848-23D1-4752-A9B0-5159E5B2F92E}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jeffijoe.MessageFormat.Benchmarks", "Jeffijoe.MessageFormat.Benchmarks\Jeffijoe.MessageFormat.Benchmarks.csproj", "{D63A7E6E-D302-44E2-A355-F72DD005AB57}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {7D16B114-A482-4FC4-A055-8E96573BE2A3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {7D16B114-A482-4FC4-A055-8E96573BE2A3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7D16B114-A482-4FC4-A055-8E96573BE2A3}.Debug|x64.ActiveCfg = Debug|Any CPU + {7D16B114-A482-4FC4-A055-8E96573BE2A3}.Debug|x64.Build.0 = Debug|Any CPU + {7D16B114-A482-4FC4-A055-8E96573BE2A3}.Debug|x86.ActiveCfg = Debug|Any CPU + {7D16B114-A482-4FC4-A055-8E96573BE2A3}.Debug|x86.Build.0 = Debug|Any CPU {7D16B114-A482-4FC4-A055-8E96573BE2A3}.Release|Any CPU.ActiveCfg = Release|Any CPU {7D16B114-A482-4FC4-A055-8E96573BE2A3}.Release|Any CPU.Build.0 = Release|Any CPU + {7D16B114-A482-4FC4-A055-8E96573BE2A3}.Release|x64.ActiveCfg = Release|Any CPU + {7D16B114-A482-4FC4-A055-8E96573BE2A3}.Release|x64.Build.0 = Release|Any CPU + {7D16B114-A482-4FC4-A055-8E96573BE2A3}.Release|x86.ActiveCfg = Release|Any CPU + {7D16B114-A482-4FC4-A055-8E96573BE2A3}.Release|x86.Build.0 = Release|Any CPU {F1AC744E-9031-468E-A397-6F44AA19EBA1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {F1AC744E-9031-468E-A397-6F44AA19EBA1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F1AC744E-9031-468E-A397-6F44AA19EBA1}.Debug|x64.ActiveCfg = Debug|Any CPU + {F1AC744E-9031-468E-A397-6F44AA19EBA1}.Debug|x64.Build.0 = Debug|Any CPU + {F1AC744E-9031-468E-A397-6F44AA19EBA1}.Debug|x86.ActiveCfg = Debug|Any CPU + {F1AC744E-9031-468E-A397-6F44AA19EBA1}.Debug|x86.Build.0 = Debug|Any CPU {F1AC744E-9031-468E-A397-6F44AA19EBA1}.Release|Any CPU.ActiveCfg = Release|Any CPU {F1AC744E-9031-468E-A397-6F44AA19EBA1}.Release|Any CPU.Build.0 = Release|Any CPU + {F1AC744E-9031-468E-A397-6F44AA19EBA1}.Release|x64.ActiveCfg = Release|Any CPU + {F1AC744E-9031-468E-A397-6F44AA19EBA1}.Release|x64.Build.0 = Release|Any CPU + {F1AC744E-9031-468E-A397-6F44AA19EBA1}.Release|x86.ActiveCfg = Release|Any CPU + {F1AC744E-9031-468E-A397-6F44AA19EBA1}.Release|x86.Build.0 = Release|Any CPU {5431C848-23D1-4752-A9B0-5159E5B2F92E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {5431C848-23D1-4752-A9B0-5159E5B2F92E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5431C848-23D1-4752-A9B0-5159E5B2F92E}.Debug|x64.ActiveCfg = Debug|Any CPU + {5431C848-23D1-4752-A9B0-5159E5B2F92E}.Debug|x64.Build.0 = Debug|Any CPU + {5431C848-23D1-4752-A9B0-5159E5B2F92E}.Debug|x86.ActiveCfg = Debug|Any CPU + {5431C848-23D1-4752-A9B0-5159E5B2F92E}.Debug|x86.Build.0 = Debug|Any CPU {5431C848-23D1-4752-A9B0-5159E5B2F92E}.Release|Any CPU.ActiveCfg = Release|Any CPU {5431C848-23D1-4752-A9B0-5159E5B2F92E}.Release|Any CPU.Build.0 = Release|Any CPU + {5431C848-23D1-4752-A9B0-5159E5B2F92E}.Release|x64.ActiveCfg = Release|Any CPU + {5431C848-23D1-4752-A9B0-5159E5B2F92E}.Release|x64.Build.0 = Release|Any CPU + {5431C848-23D1-4752-A9B0-5159E5B2F92E}.Release|x86.ActiveCfg = Release|Any CPU + {5431C848-23D1-4752-A9B0-5159E5B2F92E}.Release|x86.Build.0 = Release|Any CPU + {D63A7E6E-D302-44E2-A355-F72DD005AB57}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D63A7E6E-D302-44E2-A355-F72DD005AB57}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D63A7E6E-D302-44E2-A355-F72DD005AB57}.Debug|x64.ActiveCfg = Debug|Any CPU + {D63A7E6E-D302-44E2-A355-F72DD005AB57}.Debug|x64.Build.0 = Debug|Any CPU + {D63A7E6E-D302-44E2-A355-F72DD005AB57}.Debug|x86.ActiveCfg = Debug|Any CPU + {D63A7E6E-D302-44E2-A355-F72DD005AB57}.Debug|x86.Build.0 = Debug|Any CPU + {D63A7E6E-D302-44E2-A355-F72DD005AB57}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D63A7E6E-D302-44E2-A355-F72DD005AB57}.Release|Any CPU.Build.0 = Release|Any CPU + {D63A7E6E-D302-44E2-A355-F72DD005AB57}.Release|x64.ActiveCfg = Release|Any CPU + {D63A7E6E-D302-44E2-A355-F72DD005AB57}.Release|x64.Build.0 = Release|Any CPU + {D63A7E6E-D302-44E2-A355-F72DD005AB57}.Release|x86.ActiveCfg = Release|Any CPU + {D63A7E6E-D302-44E2-A355-F72DD005AB57}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE