Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 53 additions & 8 deletions LanguageTags/LanguageTag.cs
Original file line number Diff line number Diff line change
Expand Up @@ -320,16 +320,41 @@ public ExtensionTag()
public override string ToString() =>
Tags.IsEmpty ? string.Empty : $"{Prefix}-{string.Join('-', Tags)}";

/// <summary>
/// Creates a new extension tag with sorted and lowercased tags.
/// </summary>
/// <returns>A normalized copy of this extension tag.</returns>
internal ExtensionTag Normalize() =>
this with
{
Prefix = char.ToLowerInvariant(Prefix),
Tags = [.. Tags.Select(t => t.ToLowerInvariant()).OrderBy(t => t)],
};

/// <summary>
/// Determines whether this instance is equal to another <see cref="ExtensionTag"/>.
/// </summary>
/// <param name="other">The <see cref="ExtensionTag"/> to compare with.</param>
/// <returns>true if the extension tags are equal; otherwise, false.</returns>
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)
);

/// <summary>
/// Returns the hash code for this extension tag.
/// </summary>
/// <returns>A hash code for the current extension tag.</returns>
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();
}
}

/// <summary>
Expand Down Expand Up @@ -363,13 +388,33 @@ public PrivateUseTag()
public override string ToString() =>
Tags.IsEmpty ? string.Empty : $"{Prefix}-{string.Join('-', Tags)}";

/// <summary>
/// Creates a new private use tag with sorted and lowercased tags.
/// </summary>
/// <returns>A normalized copy of this private use tag.</returns>
internal PrivateUseTag Normalize() =>
this with
{
Tags = [.. Tags.Select(t => t.ToLowerInvariant()).OrderBy(t => t)],
};

/// <summary>
/// Determines whether this instance is equal to another <see cref="PrivateUseTag"/>.
/// </summary>
/// <param name="other">The <see cref="PrivateUseTag"/> to compare with.</param>
/// <returns>true if the private use tags are equal; otherwise, false.</returns>
public bool Equals(PrivateUseTag? other) =>
ReferenceEquals(this, other)
|| (other is not null && Tags.SequenceEqual(other.Tags, StringComparer.OrdinalIgnoreCase));

/// <summary>
/// Returns the hash code for this private use tag.
/// </summary>
/// <returns>A hash code for the current private use tag.</returns>
public override int GetHashCode()
{
HashCode hashCode = new();
foreach (string tag in Tags)
{
hashCode.Add(tag, StringComparer.OrdinalIgnoreCase);
}

return hashCode.ToHashCode();
}
}
9 changes: 8 additions & 1 deletion LanguageTags/LanguageTagBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -82,10 +82,17 @@ public LanguageTagBuilder VariantAddRange(IEnumerable<string> values)
/// <param name="values">The extension values.</param>
/// <returns>The builder instance for method chaining.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="values"/> is null.</exception>
/// <exception cref="ArgumentException">Thrown when <paramref name="values"/> is empty.</exception>
public LanguageTagBuilder ExtensionAdd(char prefix, IEnumerable<string> values)
{
ArgumentNullException.ThrowIfNull(values);
_languageTag._extensions.Add(new ExtensionTag(prefix, values));
ImmutableArray<string> tags = [.. values];
if (tags.IsEmpty)
{
throw new ArgumentException("Extension tags cannot be empty.", nameof(values));
}

_languageTag._extensions.Add(new ExtensionTag(prefix, tags));
return this;
}

Expand Down
7 changes: 5 additions & 2 deletions LanguageTags/LanguageTagParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
{
Expand Down Expand Up @@ -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))
)
)
Expand Down
14 changes: 10 additions & 4 deletions LanguageTagsCreate/HttpClientFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
),
}
)
Expand All @@ -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
)
),
}
)
Expand Down
4 changes: 0 additions & 4 deletions LanguageTagsCreate/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<int> Main(string[] args)
{
// Parse commandline
Expand Down
14 changes: 14 additions & 0 deletions LanguageTagsTests/LanguageTagBuilderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -161,6 +165,16 @@ public void ExtensionAdd_ThrowsOnNull()
.NotBeNull();
}

[Fact]
public void ExtensionAdd_ThrowsOnEmpty()
{
LanguageTagBuilder builder = new();
_ = Assert
.Throws<ArgumentException>(() => builder.ExtensionAdd('u', []))
.Should()
.NotBeNull();
}

[Fact]
public void PrivateUseAddRange_ThrowsOnNull()
{
Expand Down
1 change: 1 addition & 0 deletions LanguageTagsTests/LanguageTagParserTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
26 changes: 12 additions & 14 deletions LanguageTagsTests/LanguageTagTests.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using System.Collections.Immutable;
using System.Linq;

namespace ptr727.LanguageTags.Tests;

Expand Down Expand Up @@ -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]
Expand All @@ -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]
Expand Down