Skip to content

Commit e98917a

Browse files
authored
Resolves #345 (#346)
* Resolves #345 * PluralLocalizationFormatter does treat numeric string as valid argument * Restore behavior of v3.1.0 and before (don't convert numeric string to decimal for arg values) * Bump version to v3.2.2 * Update appveyor and github CI scripts
1 parent c8a99c0 commit e98917a

5 files changed

Lines changed: 58 additions & 62 deletions

File tree

.github/workflows/SonarCloud.yml

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,25 +13,26 @@ jobs:
1313
# (PRs from forks can't access secrets other than secrets.GITHUB_TOKEN for security reasons)
1414
if: ${{ !github.event.pull_request.head.repo.fork }}
1515
env:
16-
version: '3.2.1'
17-
versionFile: '3.2.1'
16+
version: '3.2.2'
17+
versionFile: '3.2.2'
1818
steps:
19-
- name: Set up JDK 11
20-
uses: actions/setup-java@v1
19+
- name: Set up JDK 17
20+
uses: actions/setup-java@v3
2121
with:
22-
java-version: 1.11
22+
distribution: 'microsoft'
23+
java-version: '17'
2324
- uses: actions/checkout@v3
2425
with:
2526
fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
2627
- name: Cache SonarCloud packages
27-
uses: actions/cache@v1
28+
uses: actions/cache@v3
2829
with:
2930
path: ~\sonar\cache
3031
key: ${{ runner.os }}-sonar
3132
restore-keys: ${{ runner.os }}-sonar
3233
- name: Cache SonarCloud scanner
3334
id: cache-sonar-scanner
34-
uses: actions/cache@v1
35+
uses: actions/cache@v3
3536
with:
3637
path: .\.sonar\scanner
3738
key: ${{ runner.os }}-sonar-scanner
@@ -48,13 +49,13 @@ jobs:
4849
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
4950
shell: powershell
5051
run: |
51-
.\.sonar\scanner\dotnet-sonarscanner begin /k:"${{ github.event.repository.owner.login }}_SmartFormat" /o:"${{ github.event.repository.owner.login }}" /d:sonar.login="${{ secrets.SONAR_TOKEN }}" /d:sonar.host.url="https://sonarcloud.io" /d:sonar.exclusions="**/SmartFormat.ZString/**/*" /d:sonar.cs.opencover.reportsPaths="./src/SmartFormat.Tests/**/coverage*.xml"
52+
.\.sonar\scanner\dotnet-sonarscanner begin /k:"${{ github.event.repository.owner.login }}_SmartFormat" /o:"${{ github.event.repository.owner.login }}" /d:sonar.token="${{ secrets.SONAR_TOKEN }}" /d:sonar.host.url="https://sonarcloud.io" /d:sonar.exclusions="**/SmartFormat.ZString/**/*" /d:sonar.cs.opencover.reportsPaths="./src/SmartFormat.Tests/**/coverage*.xml"
5253
dotnet sln ./src/SmartFormat.sln remove ./src/Demo/Demo.csproj ./src/Demo.NetFramework/Demo.NetFramework.csproj ./src/Performance/Performance.csproj ./src/Performance_v27/Performance_v27.csproj
5354
dotnet add ./src/SmartFormat.Tests/SmartFormat.Tests.csproj package AltCover
5455
dotnet restore ./src/SmartFormat.sln
5556
dotnet build ./src/SmartFormat.sln --no-restore /verbosity:minimal /t:rebuild /p:configuration=release /nowarn:CS1591,CS0618 /p:IncludeSymbols=true /p:SymbolPackageFormat=snupkg /p:ContinuousIntegrationBuild=true /p:Version=${{ env.version }} /p:FileVersion=${{ env.versionFile }}
5657
dotnet test ./src/SmartFormat.sln --no-build --verbosity normal /p:configuration=release /p:AltCover=true /p:AltCoverXmlReport="coverage.xml" /p:AltCoverStrongNameKey="../SmartFormat/SmartFormat.snk" /p:AltCoverAssemblyExcludeFilter="SmartFormat.Tests|SmartFormat.ZString|NUnit3.TestAdapter"
57-
.\.sonar\scanner\dotnet-sonarscanner end /d:sonar.login="${{ secrets.SONAR_TOKEN }}"
58+
.\.sonar\scanner\dotnet-sonarscanner end /d:sonar.token="${{ secrets.SONAR_TOKEN }}"
5859
- name: Pack
5960
run: |
6061
echo "Packing Version: ${{ env.version }}, File Version: ${{ env.versionFile }}"

appveyor.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ for:
2323
- ps: dotnet restore SmartFormat.sln --verbosity quiet
2424
- ps: dotnet add .\SmartFormat.Tests\SmartFormat.Tests.csproj package AltCover
2525
- ps: |
26-
$version = "3.2.1"
26+
$version = "3.2.2"
2727
$versionFile = $version + "." + ${env:APPVEYOR_BUILD_NUMBER}
2828
2929
if ($env:APPVEYOR_PULL_REQUEST_NUMBER) {

src/Directory.Build.props

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@
88
<Copyright>Copyright 2011-$(CurrentYear) SmartFormat Project</Copyright>
99
<RepositoryUrl>https://github.com/axuno/SmartFormat.git</RepositoryUrl>
1010
<PublishRepositoryUrl>true</PublishRepositoryUrl>
11-
<Version>3.2.1</Version>
12-
<FileVersion>3.2.1</FileVersion>
11+
<Version>3.2.2</Version>
12+
<FileVersion>3.2.2</FileVersion>
1313
<AssemblyVersion>3.0.0</AssemblyVersion> <!--only update AssemblyVersion with major releases -->
1414
<LangVersion>latest</LangVersion>
1515
<EnableNETAnalyzers>true</EnableNETAnalyzers>

src/SmartFormat.Tests/Extensions/PluralLocalizationFormatterTests.cs

Lines changed: 26 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,32 @@ public void Explicit_Formatter_Without_IEnumerable_Arg_Should_Throw()
4949
Assert.That(() => smart.Format("{0:plural:One|Two}", new object()), Throws.Exception.TypeOf<FormattingException>());
5050
}
5151

52+
[TestCase("")] // no string
53+
[TestCase("1234")] // don't convert numeric string to decimal, see https://github.com/axuno/SmartFormat/issues/345
54+
[TestCase(false)] // no boolean
55+
[TestCase(3.40282347E+38f)] // float.MaxValue exceeds decimal.MaxValue
56+
public void Explicit_Formatter_Without_Valid_Argument_Should_Throw(object arg)
57+
{
58+
var smart = Smart.CreateDefaultSmartFormat();
59+
Assert.That(() => smart.Format("{0:plural:One|Two}", arg), Throws.Exception.TypeOf<FormattingException>(), "Invalid argument type or value");
60+
}
61+
62+
[TestCase("String", "String")]
63+
[TestCase(false, "other")]
64+
[TestCase(default(string?), "other")]
65+
public void AutoDetect_Formatter_Should_Not_Handle_bool_string_null(object arg, string expected)
66+
{
67+
var smart = new SmartFormatter()
68+
.AddExtensions(new DefaultSource())
69+
.AddExtensions(new PluralLocalizationFormatter { CanAutoDetect = true },
70+
new ConditionalFormatter { CanAutoDetect = true },
71+
new DefaultFormatter());
72+
73+
// Result comes from ConditionalFormatter!
74+
var result = smart.Format("{0:{}|other}", arg);
75+
Assert.That(result, Is.EqualTo(expected));
76+
}
77+
5278
[TestCase(0)]
5379
[TestCase(1)]
5480
[TestCase(100)]
@@ -337,34 +363,4 @@ public void Pluralization_With_Changed_SplitChar(int numOfPeople, string format,
337363
var result = smart.Format(format, data);
338364
Assert.That(result, Is.EqualTo(expected));
339365
}
340-
341-
[Test]
342-
public void DoesNotHandle_Bool_WhenCanAutoDetect_IsTrue()
343-
{
344-
var smart = new SmartFormatter()
345-
.AddExtensions(new DefaultSource())
346-
.AddExtensions(new PluralLocalizationFormatter { CanAutoDetect = true }, // Should not handle the bool
347-
new ConditionalFormatter { CanAutoDetect = true }, // Should handle the bool
348-
new DefaultFormatter());
349-
350-
var result = smart.Format(new CultureInfo("ar"), "{0:yes|no}", true);
351-
Assert.That(result, Is.EqualTo("yes"));
352-
}
353-
354-
[TestCase("A", "[A]")]
355-
[TestCase(default(string?), "null")]
356-
public void DoesNotHandle_NonDecimalValues_WhenCanAutoDetect_IsTrue(string? category, string expected)
357-
{
358-
var smart = new SmartFormatter()
359-
.AddExtensions(new DefaultSource())
360-
.AddExtensions(new ReflectionSource())
361-
// Should not handle because "Category" value cannot convert to decimal
362-
.AddExtensions(new PluralLocalizationFormatter { CanAutoDetect = true },
363-
// Should detect and handle the format
364-
new ConditionalFormatter { CanAutoDetect = true },
365-
new DefaultFormatter());
366-
367-
var result = smart.Format(new CultureInfo("en"), "{Category:[{}]|null}", new { Category = category });
368-
Assert.That(result, Is.EqualTo(expected));
369-
}
370366
}

src/SmartFormat/Extensions/PluralLocalizationFormatter.cs

Lines changed: 19 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ namespace SmartFormat.Extensions;
1414

1515
/// <summary>
1616
/// A class to format following culture specific pluralization rules.
17+
/// The range of values the formatter can process is from <see cref="decimal.MinValue"/> to <see cref="decimal.MaxValue"/>.
1718
/// </summary>
1819
public class PluralLocalizationFormatter : IFormatter
1920
{
@@ -79,7 +80,7 @@ public PluralLocalizationFormatter(string defaultTwoLetterIsoLanguageName)
7980
/// called by its name in the input format string.
8081
/// <para/>
8182
/// <b>Auto detection only works with more than 1 format argument.
82-
/// Is recommended to set <see cref="CanAutoDetect"/> to <see langword="false"/>. This will be the default in a future version.
83+
/// It is recommended to set <see cref="CanAutoDetect"/> to <see langword="false"/>. This will be the default in a future version.
8384
/// </b>
8485
/// </summary>
8586
/// <remarks>
@@ -102,44 +103,42 @@ public bool TryEvaluateFormat(IFormattingInfo formattingInfo)
102103
{
103104
var format = formattingInfo.Format;
104105
var current = formattingInfo.CurrentValue;
105-
106-
if (format == null) return false;
107106

108-
// Extract the plural words from the format string:
107+
if (format == null) return false;
108+
109+
// Extract the plural words from the format string
109110
var pluralWords = format.Split(SplitChar);
110111

112+
var useAutoDetection = string.IsNullOrEmpty(formattingInfo.Placeholder?.FormatterName);
113+
111114
// This extension requires at least two plural words for auto detection
112-
// For locales
113-
if (pluralWords.Count <= 1 && string.IsNullOrEmpty(formattingInfo.Placeholder?.FormatterName))
114-
{
115-
// Auto detection calls just return a failure to evaluate
116-
return false;
117-
}
115+
// Valid types for auto detection are checked later
116+
if (useAutoDetection && pluralWords.Count <= 1) return false;
118117

119118
decimal value;
120119

121-
// Check whether arguments can be handled by this formatter
122-
123-
// We can format numbers, and IEnumerables. For IEnumerables we look at the number of items
124-
// in the collection: this means the user can e.g. use the same parameter for both plural and list, for example
125-
// 'Smart.Format("The following {0:plural:person is|people are} impressed: {0:list:{}|, |, and}", new[] { "bob", "alice" });'
120+
/*
121+
Check whether arguments can be handled by this formatter:
126122
123+
We can format numbers, and IEnumerables. For IEnumerables we look at the number of items
124+
in the collection: this means the user can e.g. use the same parameter for both plural and list, for example
125+
'Smart.Format("The following {0:plural:person is|people are} impressed: {0:list:{}|, |, and}", new[] { "bob", "alice" });'
126+
*/
127127
switch (current)
128128
{
129-
case IConvertible convertible when current is not bool && TryGetDecimalValue(convertible, null, out value):
129+
case IConvertible convertible when convertible is not (bool or string) && TryGetDecimalValue(convertible, null, out value):
130130
break;
131131
case IEnumerable<object> objects:
132132
value = objects.Count();
133133
break;
134134
default:
135135
{
136136
// Auto detection calls just return a failure to evaluate
137-
if (string.IsNullOrEmpty(formattingInfo.Placeholder?.FormatterName))
138-
return false;
137+
if (useAutoDetection) return false;
139138

140139
// throw, if the formatter has been called explicitly
141-
throw new FormatException(
142-
$"Formatter named '{formattingInfo.Placeholder?.FormatterName}' can format numbers and IEnumerables, but the argument was of type '{current?.GetType().ToString() ?? "null"}'");
140+
throw new FormattingException(format,
141+
$"Formatter named '{formattingInfo.Placeholder?.FormatterName}' can format numbers and IEnumerables, but the argument was of type '{current?.GetType().ToString() ?? "null"}'", 0);
143142
}
144143
}
145144

0 commit comments

Comments
 (0)