diff --git a/LanguageTags/LanguageTag.cs b/LanguageTags/LanguageTag.cs
index 55921b8..cc60ac2 100644
--- a/LanguageTags/LanguageTag.cs
+++ b/LanguageTags/LanguageTag.cs
@@ -320,16 +320,41 @@ public ExtensionTag()
public override string ToString() =>
Tags.IsEmpty ? string.Empty : $"{Prefix}-{string.Join('-', Tags)}";
- ///
- /// Creates a new extension tag with sorted and lowercased tags.
- ///
- /// A normalized copy of this extension tag.
internal ExtensionTag Normalize() =>
this with
{
Prefix = char.ToLowerInvariant(Prefix),
Tags = [.. Tags.Select(t => t.ToLowerInvariant()).OrderBy(t => t)],
};
+
+ ///
+ /// Determines whether this instance is equal to another .
+ ///
+ /// The to compare with.
+ /// true if the extension tags are equal; otherwise, false.
+ public bool Equals(ExtensionTag? other) =>
+ ReferenceEquals(this, other)
+ || (
+ other is not null
+ && char.ToLowerInvariant(Prefix) == char.ToLowerInvariant(other.Prefix)
+ && Tags.SequenceEqual(other.Tags, StringComparer.OrdinalIgnoreCase)
+ );
+
+ ///
+ /// Returns the hash code for this extension tag.
+ ///
+ /// A hash code for the current extension tag.
+ public override int GetHashCode()
+ {
+ HashCode hashCode = new();
+ hashCode.Add(char.ToLowerInvariant(Prefix));
+ foreach (string tag in Tags)
+ {
+ hashCode.Add(tag, StringComparer.OrdinalIgnoreCase);
+ }
+
+ return hashCode.ToHashCode();
+ }
}
///
@@ -363,13 +388,33 @@ public PrivateUseTag()
public override string ToString() =>
Tags.IsEmpty ? string.Empty : $"{Prefix}-{string.Join('-', Tags)}";
- ///
- /// Creates a new private use tag with sorted and lowercased tags.
- ///
- /// A normalized copy of this private use tag.
internal PrivateUseTag Normalize() =>
this with
{
Tags = [.. Tags.Select(t => t.ToLowerInvariant()).OrderBy(t => t)],
};
+
+ ///
+ /// Determines whether this instance is equal to another .
+ ///
+ /// The to compare with.
+ /// true if the private use tags are equal; otherwise, false.
+ public bool Equals(PrivateUseTag? other) =>
+ ReferenceEquals(this, other)
+ || (other is not null && Tags.SequenceEqual(other.Tags, StringComparer.OrdinalIgnoreCase));
+
+ ///
+ /// Returns the hash code for this private use tag.
+ ///
+ /// A hash code for the current private use tag.
+ public override int GetHashCode()
+ {
+ HashCode hashCode = new();
+ foreach (string tag in Tags)
+ {
+ hashCode.Add(tag, StringComparer.OrdinalIgnoreCase);
+ }
+
+ return hashCode.ToHashCode();
+ }
}
diff --git a/LanguageTags/LanguageTagBuilder.cs b/LanguageTags/LanguageTagBuilder.cs
index f45dc44..8e9fd78 100644
--- a/LanguageTags/LanguageTagBuilder.cs
+++ b/LanguageTags/LanguageTagBuilder.cs
@@ -82,10 +82,17 @@ public LanguageTagBuilder VariantAddRange(IEnumerable values)
/// The extension values.
/// The builder instance for method chaining.
/// Thrown when is null.
+ /// Thrown when is empty.
public LanguageTagBuilder ExtensionAdd(char prefix, IEnumerable values)
{
ArgumentNullException.ThrowIfNull(values);
- _languageTag._extensions.Add(new ExtensionTag(prefix, values));
+ ImmutableArray tags = [.. values];
+ if (tags.IsEmpty)
+ {
+ throw new ArgumentException("Extension tags cannot be empty.", nameof(values));
+ }
+
+ _languageTag._extensions.Add(new ExtensionTag(prefix, tags));
return this;
}
diff --git a/LanguageTags/LanguageTagParser.cs b/LanguageTags/LanguageTagParser.cs
index 931c4c2..e489098 100644
--- a/LanguageTags/LanguageTagParser.cs
+++ b/LanguageTags/LanguageTagParser.cs
@@ -290,7 +290,9 @@ private static bool ValidateExtensionPrefix(string tag) =>
private static bool ValidateExtension(string tag) =>
// 2 - 8 chars
- !string.IsNullOrEmpty(tag) && tag.Length is >= 2 and <= 8;
+ !string.IsNullOrWhiteSpace(tag)
+ && tag.Length is >= 2 and <= 8
+ && !tag.Any(char.IsWhiteSpace);
private bool ParseExtension()
{
@@ -788,7 +790,8 @@ internal static bool Validate(LanguageTag languageTag)
}
if (
languageTag._extensions.Any(extension =>
- !ValidateExtensionPrefix(extension.Prefix.ToString())
+ extension.Tags.IsEmpty
+ || !ValidateExtensionPrefix(extension.Prefix.ToString())
|| extension.Tags.Any(tag => !ValidateExtension(tag))
)
)
diff --git a/LanguageTagsCreate/HttpClientFactory.cs b/LanguageTagsCreate/HttpClientFactory.cs
index c97c65e..db37c50 100644
--- a/LanguageTagsCreate/HttpClientFactory.cs
+++ b/LanguageTagsCreate/HttpClientFactory.cs
@@ -28,8 +28,11 @@ private static ResilienceHandler CreateResilienceHandler() =>
MaxDelay = TimeSpan.FromSeconds(30),
ShouldHandle = args =>
ValueTask.FromResult(
- args.Outcome.Result != null
- && !args.Outcome.Result.IsSuccessStatusCode
+ args.Outcome.Exception != null
+ || (
+ args.Outcome.Result != null
+ && !args.Outcome.Result.IsSuccessStatusCode
+ )
),
}
)
@@ -42,8 +45,11 @@ private static ResilienceHandler CreateResilienceHandler() =>
BreakDuration = TimeSpan.FromSeconds(30),
ShouldHandle = args =>
ValueTask.FromResult(
- args.Outcome.Result != null
- && !args.Outcome.Result.IsSuccessStatusCode
+ args.Outcome.Exception != null
+ || (
+ args.Outcome.Result != null
+ && !args.Outcome.Result.IsSuccessStatusCode
+ )
),
}
)
diff --git a/LanguageTagsCreate/Program.cs b/LanguageTagsCreate/Program.cs
index afd8145..c904236 100644
--- a/LanguageTagsCreate/Program.cs
+++ b/LanguageTagsCreate/Program.cs
@@ -8,10 +8,6 @@ CancellationToken cancellationToken
private const string DataDirectory = "LanguageData";
private const string CodeDirectory = "LanguageTags";
- internal CommandLine.Options GetCommandLineOptions() => commandLineOptions;
-
- internal CancellationToken GetCancellationToken() => cancellationToken;
-
internal static async Task Main(string[] args)
{
// Parse commandline
diff --git a/LanguageTagsTests/LanguageTagBuilderTests.cs b/LanguageTagsTests/LanguageTagBuilderTests.cs
index e7e800e..da91798 100644
--- a/LanguageTagsTests/LanguageTagBuilderTests.cs
+++ b/LanguageTagsTests/LanguageTagBuilderTests.cs
@@ -125,6 +125,10 @@ public void Build_Fail()
// Extension prefix 1 char, not x
languageTag = new LanguageTagBuilder().Language("en").ExtensionAdd('x', ["abcd"]).Build();
_ = languageTag.Validate().Should().BeFalse();
+
+ // Extension tags must not be whitespace
+ languageTag = new LanguageTagBuilder().Language("en").ExtensionAdd('a', [" "]).Build();
+ _ = languageTag.Validate().Should().BeFalse();
}
[Fact]
@@ -161,6 +165,16 @@ public void ExtensionAdd_ThrowsOnNull()
.NotBeNull();
}
+ [Fact]
+ public void ExtensionAdd_ThrowsOnEmpty()
+ {
+ LanguageTagBuilder builder = new();
+ _ = Assert
+ .Throws(() => builder.ExtensionAdd('u', []))
+ .Should()
+ .NotBeNull();
+ }
+
[Fact]
public void PrivateUseAddRange_ThrowsOnNull()
{
diff --git a/LanguageTagsTests/LanguageTagParserTests.cs b/LanguageTagsTests/LanguageTagParserTests.cs
index 227f3ba..eb51c1c 100644
--- a/LanguageTagsTests/LanguageTagParserTests.cs
+++ b/LanguageTagsTests/LanguageTagParserTests.cs
@@ -95,6 +95,7 @@ public void Normalize_Sort_Pass(string tag, string parsed)
[InlineData("en-gb-abcde-abcde")] // Variant repeats
[InlineData("en-gb-a-abcd-a-abcde")] // Extension prefix repeats
[InlineData("en-gb-a-abcd-abcd")] // Extension tag repeats
+ [InlineData("en-a- ")] // Extension tag whitespace
[InlineData("en-gb-x-abcd-x-abcd")] // Private prefix repeats
[InlineData("en-gb-x-abcd-abcd")] // Private tag repeats
public void Parse_Fail(string tag) => _ = new LanguageTagParser().Parse(tag).Should().BeNull();
diff --git a/LanguageTagsTests/LanguageTagTests.cs b/LanguageTagsTests/LanguageTagTests.cs
index 0228f87..4d59d25 100644
--- a/LanguageTagsTests/LanguageTagTests.cs
+++ b/LanguageTagsTests/LanguageTagTests.cs
@@ -1,5 +1,4 @@
using System.Collections.Immutable;
-using System.Linq;
namespace ptr727.LanguageTags.Tests;
@@ -564,16 +563,15 @@ public void ExtensionTag_EnumerableConstructor_CreatesTag()
public void ExtensionTag_RecordEquality_WorksCorrectly()
{
ExtensionTag ext1 = new('u', ["ca", "buddhist"]);
- ExtensionTag ext2 = new('u', ["ca", "buddhist"]);
+ ExtensionTag ext2 = new('U', ["CA", "BUDDHIST"]);
ExtensionTag ext3 = new('t', ["ca", "buddhist"]);
- // Records with ImmutableArray need element-wise comparison
- _ = ext1.Prefix.Should().Be(ext2.Prefix);
- _ = ext1.Tags.SequenceEqual(ext2.Tags).Should().BeTrue();
- _ = ext1.ToString().Should().Be(ext2.ToString());
+ _ = ext1.Equals(ext2).Should().BeTrue();
+ _ = (ext1 == ext2).Should().BeTrue();
+ _ = ext1.GetHashCode().Should().Be(ext2.GetHashCode());
- _ = ext1.Prefix.Should().NotBe(ext3.Prefix);
- _ = ext1.ToString().Should().NotBe(ext3.ToString());
+ _ = ext1.Equals(ext3).Should().BeFalse();
+ _ = (ext1 != ext3).Should().BeTrue();
}
[Fact]
@@ -599,15 +597,15 @@ public void PrivateUseTag_EnumerableConstructor_CreatesTag()
public void PrivateUseTag_RecordEquality_WorksCorrectly()
{
PrivateUseTag priv1 = new(["private1", "private2"]);
- PrivateUseTag priv2 = new(["private1", "private2"]);
+ PrivateUseTag priv2 = new(["PRIVATE1", "PRIVATE2"]);
PrivateUseTag priv3 = new(["other"]);
- // Records with ImmutableArray need element-wise comparison
- _ = priv1.Tags.SequenceEqual(priv2.Tags).Should().BeTrue();
- _ = priv1.ToString().Should().Be(priv2.ToString());
+ _ = priv1.Equals(priv2).Should().BeTrue();
+ _ = (priv1 == priv2).Should().BeTrue();
+ _ = priv1.GetHashCode().Should().Be(priv2.GetHashCode());
- _ = priv1.Tags.SequenceEqual(priv3.Tags).Should().BeFalse();
- _ = priv1.ToString().Should().NotBe(priv3.ToString());
+ _ = priv1.Equals(priv3).Should().BeFalse();
+ _ = (priv1 != priv3).Should().BeTrue();
}
[Fact]