From 6eb259478b4dbf84634605fec6f6a1e28e7b3db9 Mon Sep 17 00:00:00 2001 From: Gerard Gunnewijk Date: Tue, 17 Jun 2025 15:22:36 +0200 Subject: [PATCH 1/3] Made primitives implement IParsable, dpi/pixel units can now be made with doubles instead of ints, and becaus of the IParsable, IFormatProvider is supported during parsing. --- .../JsonConverters/PointJsonConverter.cs | 20 + .../JsonConverters/RectangleJsonConverter.cs | 20 + .../JsonConverters/SizeJsonConverter.cs | 20 + .../JsonConverters/SpacingJsonConverter.cs | 20 + .../JsonConverters/UnitJsonConverter.cs | 23 +- .../JsonConverters/ValueJsonConverter.cs | 23 +- src/Synercoding.Primitives/Point.cs | 48 +- src/Synercoding.Primitives/Rectangle.cs | 54 +- src/Synercoding.Primitives/Size.cs | 48 +- src/Synercoding.Primitives/Spacing.cs | 58 +- src/Synercoding.Primitives/Unit.cs | 73 +- src/Synercoding.Primitives/UnitDesignation.cs | 2 +- src/Synercoding.Primitives/Value.cs | 55 +- src/Synercoding.Primitives/ValueCreator.cs | 4 +- .../PointTests.cs | 27 +- .../RectangleTests.cs | 24 +- .../Synercoding.Primitives.Tests/SizeTests.cs | 27 +- .../SpacingTests.cs | 24 +- .../UnitDesignationTests.cs | 5 +- .../Synercoding.Primitives.Tests/UnitTests.cs | 44 +- .../ValueTests.cs | 723 ++++++++++-------- 21 files changed, 903 insertions(+), 439 deletions(-) diff --git a/src/Synercoding.Primitives/JsonConverters/PointJsonConverter.cs b/src/Synercoding.Primitives/JsonConverters/PointJsonConverter.cs index 7208fcb..f6c78bf 100644 --- a/src/Synercoding.Primitives/JsonConverters/PointJsonConverter.cs +++ b/src/Synercoding.Primitives/JsonConverters/PointJsonConverter.cs @@ -4,10 +4,24 @@ namespace Synercoding.Primitives.JsonConverters; +/// +/// Converts a object to or from JSON. +/// public class PointJsonConverter : JsonConverter { + /// + /// Thread-safe instance of to be reused. + /// public static PointJsonConverter Instance { get; } = new(); + /// + /// Read a object from the . + /// + /// The reader that should contain a . + /// The expected type to convert. + /// An object that specifies serialization options to use. + /// The that was read from the . + /// Throws when the does not contain . public override Point Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { if (reader.TokenType == JsonTokenType.String) @@ -50,6 +64,12 @@ public override Point Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSe throw new JsonException(); } + /// + /// Write a to the . + /// + /// Writer to write the to. + /// The to write. + /// An object that specifies serialization options to use. public override void Write(Utf8JsonWriter writer, Point value, JsonSerializerOptions options) { writer.WriteStringValue(value.ToString()); diff --git a/src/Synercoding.Primitives/JsonConverters/RectangleJsonConverter.cs b/src/Synercoding.Primitives/JsonConverters/RectangleJsonConverter.cs index e860b27..1ea8744 100644 --- a/src/Synercoding.Primitives/JsonConverters/RectangleJsonConverter.cs +++ b/src/Synercoding.Primitives/JsonConverters/RectangleJsonConverter.cs @@ -4,10 +4,24 @@ namespace Synercoding.Primitives.JsonConverters; +/// +/// Converts a object to or from JSON. +/// public class RectangleJsonConverter : JsonConverter { + /// + /// Thread-safe instance of to be reused. + /// public static RectangleJsonConverter Instance { get; } = new(); + /// + /// Read a object from the . + /// + /// The reader that should contain a . + /// The expected type to convert. + /// An object that specifies serialization options to use. + /// The that was read from the . + /// Throws when the does not contain . public override Rectangle Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { if (reader.TokenType == JsonTokenType.String) @@ -58,6 +72,12 @@ public override Rectangle Read(ref Utf8JsonReader reader, Type typeToConvert, Js throw new JsonException(); } + /// + /// Write a to the . + /// + /// Writer to write the to. + /// The to write. + /// An object that specifies serialization options to use. public override void Write(Utf8JsonWriter writer, Rectangle value, JsonSerializerOptions options) { writer.WriteStringValue(value.ToString()); diff --git a/src/Synercoding.Primitives/JsonConverters/SizeJsonConverter.cs b/src/Synercoding.Primitives/JsonConverters/SizeJsonConverter.cs index 2d0ae51..6e39163 100644 --- a/src/Synercoding.Primitives/JsonConverters/SizeJsonConverter.cs +++ b/src/Synercoding.Primitives/JsonConverters/SizeJsonConverter.cs @@ -4,10 +4,24 @@ namespace Synercoding.Primitives.JsonConverters; +/// +/// Converts a object to or from JSON. +/// public class SizeJsonConverter : JsonConverter { + /// + /// Thread-safe instance of to be reused. + /// public static SizeJsonConverter Instance { get; } = new(); + /// + /// Read a object from the . + /// + /// The reader that should contain a . + /// The expected type to convert. + /// An object that specifies serialization options to use. + /// The that was read from the . + /// Throws when the does not contain . public override Size Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { if (reader.TokenType == JsonTokenType.String) @@ -50,6 +64,12 @@ public override Size Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSer throw new JsonException(); } + /// + /// Write a to the . + /// + /// Writer to write the to. + /// The to write. + /// An object that specifies serialization options to use. public override void Write(Utf8JsonWriter writer, Size value, JsonSerializerOptions options) { writer.WriteStringValue(value.ToString()); diff --git a/src/Synercoding.Primitives/JsonConverters/SpacingJsonConverter.cs b/src/Synercoding.Primitives/JsonConverters/SpacingJsonConverter.cs index 3f82f4d..aa06cd9 100644 --- a/src/Synercoding.Primitives/JsonConverters/SpacingJsonConverter.cs +++ b/src/Synercoding.Primitives/JsonConverters/SpacingJsonConverter.cs @@ -4,10 +4,24 @@ namespace Synercoding.Primitives.JsonConverters; +/// +/// Converts a object to or from JSON. +/// public class SpacingJsonConverter : JsonConverter { + /// + /// Thread-safe instance of to be reused. + /// public static SpacingJsonConverter Instance { get; } = new(); + /// + /// Read a object from the . + /// + /// The reader that should contain a . + /// The expected type to convert. + /// An object that specifies serialization options to use. + /// The that was read from the . + /// Throws when the does not contain . public override Spacing Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { if (reader.TokenType == JsonTokenType.String) @@ -58,6 +72,12 @@ public override Spacing Read(ref Utf8JsonReader reader, Type typeToConvert, Json throw new JsonException(); } + /// + /// Write a to the . + /// + /// Writer to write the to. + /// The to write. + /// An object that specifies serialization options to use. public override void Write(Utf8JsonWriter writer, Spacing value, JsonSerializerOptions options) { writer.WriteStringValue(value.ToString()); diff --git a/src/Synercoding.Primitives/JsonConverters/UnitJsonConverter.cs b/src/Synercoding.Primitives/JsonConverters/UnitJsonConverter.cs index fc5f0f0..fd73fbb 100644 --- a/src/Synercoding.Primitives/JsonConverters/UnitJsonConverter.cs +++ b/src/Synercoding.Primitives/JsonConverters/UnitJsonConverter.cs @@ -1,13 +1,28 @@ using System; +using System.Globalization; using System.Text.Json; using System.Text.Json.Serialization; namespace Synercoding.Primitives.JsonConverters; +/// +/// Converts a object to or from JSON. +/// public class UnitJsonConverter : JsonConverter { + /// + /// Thread-safe instance of to be reused. + /// public static UnitJsonConverter Instance { get; } = new(); + /// + /// Read a object from the . + /// + /// The reader that should contain a . + /// The expected type to convert. + /// An object that specifies serialization options to use. + /// The that was read from the . + /// Throws when the does not contain . public override Unit Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { if (reader.TokenType != JsonTokenType.String) @@ -15,12 +30,18 @@ public override Unit Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSer var textValue = reader.GetString() ?? throw new JsonException(); - if (Unit.TryParse(textValue, out var unit)) + if (Unit.TryParse(textValue, CultureInfo.InvariantCulture, out var unit)) return unit; throw new JsonException(); } + /// + /// Write a to the . + /// + /// Writer to write the to. + /// The to write. + /// An object that specifies serialization options to use. public override void Write(Utf8JsonWriter writer, Unit value, JsonSerializerOptions options) { writer.WriteStringValue(value.ToString()); diff --git a/src/Synercoding.Primitives/JsonConverters/ValueJsonConverter.cs b/src/Synercoding.Primitives/JsonConverters/ValueJsonConverter.cs index b632d1d..8458157 100644 --- a/src/Synercoding.Primitives/JsonConverters/ValueJsonConverter.cs +++ b/src/Synercoding.Primitives/JsonConverters/ValueJsonConverter.cs @@ -1,20 +1,35 @@ using System; +using System.Globalization; using System.Text.Json; using System.Text.Json.Serialization; namespace Synercoding.Primitives.JsonConverters; +/// +/// Converts a object to or from JSON. +/// public class ValueJsonConverter : JsonConverter { + /// + /// Thread-safe instance of to be reused. + /// public static ValueJsonConverter Instance { get; } = new(); + /// + /// Read a object from the . + /// + /// The reader that should contain a . + /// The expected type to convert. + /// An object that specifies serialization options to use. + /// The that was read from the . + /// Throws when the does not contain . public override Value Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { if (reader.TokenType == JsonTokenType.String) { var textValue = reader.GetString() ?? throw new JsonException(); - if (Value.TryParse(textValue, out var value)) + if (Value.TryParse(textValue, CultureInfo.InvariantCulture, out var value)) return value; throw new JsonException(); @@ -50,6 +65,12 @@ public override Value Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSe throw new JsonException(); } + /// + /// Write a to the . + /// + /// Writer to write the to. + /// The to write. + /// An object that specifies serialization options to use. public override void Write(Utf8JsonWriter writer, Value value, JsonSerializerOptions options) { writer.WriteStringValue(value.ToString()); diff --git a/src/Synercoding.Primitives/Point.cs b/src/Synercoding.Primitives/Point.cs index e2e1f57..a43d61e 100644 --- a/src/Synercoding.Primitives/Point.cs +++ b/src/Synercoding.Primitives/Point.cs @@ -1,5 +1,6 @@ using Synercoding.Primitives.Abstract; using System; +using System.Diagnostics.CodeAnalysis; using System.Text.Json.Serialization; using System.Text.RegularExpressions; @@ -9,7 +10,7 @@ namespace Synercoding.Primitives; /// A value type representing a point in a 2D space. /// [JsonConverter(typeof(JsonConverters.PointJsonConverter))] -public readonly record struct Point : IConvertable, IEquatable +public readonly record struct Point : IConvertable, IEquatable, IParsable { /// /// Constructor for a . @@ -85,7 +86,15 @@ public bool Equals(Point other) /// public override string ToString() - => $"X: {X}, Y: {Y}"; + => ToString(null); + + /// + /// Returns a string representation of the point using the specified format provider. + /// + /// The format provider to use for formatting numeric values. + /// A string representation of the point. + public string ToString(IFormatProvider? provider) + => $"X: {X.ToString(provider)}, Y: {Y.ToString(provider)}"; /// /// Parse a string into a @@ -94,8 +103,27 @@ public override string ToString() /// A that was represented by . /// Throws if can not be parsed. public static Point Parse(string s) + => Parse(s, null); + + /// + /// Try to converts a string representation of a into a . + /// + /// to be parsed. + /// Ref parameter with the parsed . + /// A to indicate if the parsing was successful. + public static bool TryParse([NotNullWhen(true)] string? s, out Point point) + => TryParse(s, null, out point); + + /// + /// Parse a string into a + /// + /// to be parsed. + /// Format provider used when parsing values. + /// A that was represented by . + /// Throws if can not be parsed. + public static Point Parse(string s, IFormatProvider? provider) { - if (TryParse(s, out var value)) + if (TryParse(s, provider, out var value)) return value; throw new ArgumentException("Argument can't be parsed.", nameof(s)); @@ -105,20 +133,24 @@ public static Point Parse(string s) /// Try to converts a string representation of a into a . /// /// to be parsed. - /// Ref parameter with the parsed . + /// Format provider used when parsing values. + /// Out parameter with the parsed . /// A to indicate if the parsing was successful. - public static bool TryParse(string s, out Point point) + public static bool TryParse([NotNullWhen(true)] string? s, IFormatProvider? provider, [MaybeNullWhen(false)] out Point result) { - point = default; + result = default; + + if (s is null) + return false; s = s.Trim(); var match = Regex.Match(s, "^X: ?(.+), ?Y: ?(.+)$"); if (match.Success && match.Groups.Count == 3) { - if (Value.TryParse(match.Groups[1].Value, out var x) && Value.TryParse(match.Groups[2].Value, out var y)) + if (Value.TryParse(match.Groups[1].Value, provider, out var x) && Value.TryParse(match.Groups[2].Value, provider, out var y)) { - point = new Point(x, y); + result = new Point(x, y); return true; } } diff --git a/src/Synercoding.Primitives/Rectangle.cs b/src/Synercoding.Primitives/Rectangle.cs index 76f00f0..fec3787 100644 --- a/src/Synercoding.Primitives/Rectangle.cs +++ b/src/Synercoding.Primitives/Rectangle.cs @@ -1,5 +1,6 @@ using Synercoding.Primitives.Abstract; using System; +using System.Diagnostics.CodeAnalysis; using System.Text.Json.Serialization; using System.Text.RegularExpressions; @@ -9,7 +10,7 @@ namespace Synercoding.Primitives; /// Value type representing a rectangle. /// [JsonConverter(typeof(JsonConverters.RectangleJsonConverter))] -public readonly record struct Rectangle : IConvertable, IEquatable +public readonly record struct Rectangle : IConvertable, IEquatable, IParsable { /// /// Constructor for . @@ -159,7 +160,15 @@ public bool Equals(Rectangle other) /// public override string ToString() - => $"LLX: {LLX}, LLY: {LLY}, URX: {URX}, URY: {URY}"; + => ToString(null); + + /// + /// Returns a string representation of the rectangle using the specified format provider. + /// + /// The format provider to use for formatting numeric values. + /// A string representation of the rectangle. + public string ToString(IFormatProvider? provider) + => $"LLX: {LLX.ToString(provider)}, LLY: {LLY.ToString(provider)}, URX: {URX.ToString(provider)}, URY: {URY.ToString(provider)}"; /// /// Parse a string into a @@ -168,8 +177,27 @@ public override string ToString() /// A that was represented by . /// Throws if can not be parsed. public static Rectangle Parse(string s) + => Parse(s, null); + + /// + /// Try to converts a string representation of a into a . + /// + /// to be parsed. + /// Out parameter with the parsed . + /// A to indicate if the parsing was successful. + public static bool TryParse([NotNullWhen(true)] string s, out Rectangle result) + => TryParse(s, null, out result); + + /// + /// Parse a string into a + /// + /// to be parsed. + /// Format provider used when parsing values. + /// A that was represented by . + /// Throws if can not be parsed. + public static Rectangle Parse(string s, IFormatProvider? provider) { - if (TryParse(s, out var value)) + if (TryParse(s, provider, out var value)) return value; throw new ArgumentException("Argument can't be parsed.", nameof(s)); @@ -179,23 +207,27 @@ public static Rectangle Parse(string s) /// Try to converts a string representation of a into a . /// /// to be parsed. - /// Ref parameter with the parsed . + /// Format provider used when parsing values. + /// Out parameter with the parsed . /// A to indicate if the parsing was successful. - public static bool TryParse(string s, out Rectangle rectangle) + public static bool TryParse([NotNullWhen(true)] string? s, IFormatProvider? provider, [MaybeNullWhen(false)] out Rectangle result) { - rectangle = default; + result = default; + + if(s is null) + return false; s = s.Trim(); var match = Regex.Match(s, "^LLX: ?(.+), ?LLY: ?(.+), ?URX: ?(.+), ?URY: ?(.+)$"); if (match.Success && match.Groups.Count == 5) { - if (Value.TryParse(match.Groups[1].Value, out var llx) - && Value.TryParse(match.Groups[2].Value, out var lly) - && Value.TryParse(match.Groups[3].Value, out var urx) - && Value.TryParse(match.Groups[4].Value, out var ury)) + if (Value.TryParse(match.Groups[1].Value, provider, out var llx) + && Value.TryParse(match.Groups[2].Value, provider, out var lly) + && Value.TryParse(match.Groups[3].Value, provider, out var urx) + && Value.TryParse(match.Groups[4].Value, provider, out var ury)) { - rectangle = new Rectangle(llx, lly, urx, ury); + result = new Rectangle(llx, lly, urx, ury); return true; } } diff --git a/src/Synercoding.Primitives/Size.cs b/src/Synercoding.Primitives/Size.cs index d056553..d9bf731 100644 --- a/src/Synercoding.Primitives/Size.cs +++ b/src/Synercoding.Primitives/Size.cs @@ -1,5 +1,6 @@ using Synercoding.Primitives.Abstract; using System; +using System.Diagnostics.CodeAnalysis; using System.Text.Json.Serialization; using System.Text.RegularExpressions; @@ -9,7 +10,7 @@ namespace Synercoding.Primitives; /// A value type representing a size using and . /// [JsonConverter(typeof(JsonConverters.SizeJsonConverter))] -public readonly record struct Size : IConvertable, IEquatable +public readonly record struct Size : IConvertable, IEquatable, IParsable { /// /// Constructor for a . @@ -106,7 +107,15 @@ public bool Equals(Size other) /// public override string ToString() - => $"W: {Width}, H: {Height}"; + => ToString(null); + + /// + /// Returns a string representation of the size using the specified format provider. + /// + /// The format provider to use for formatting numeric values. + /// A string representation of the size. + public string ToString(IFormatProvider? provider) + => $"W: {Width.ToString(provider)}, H: {Height.ToString(provider)}"; /// /// Parse a string into a @@ -115,8 +124,27 @@ public override string ToString() /// A that was represented by . /// Throws if can not be parsed. public static Size Parse(string s) + => Parse(s, null); + + /// + /// Try to converts a string representation of a into a . + /// + /// to be parsed. + /// Ref parameter with the parsed . + /// A to indicate if the parsing was successful. + public static bool TryParse([NotNullWhen(true)] string s, out Size result) + => TryParse(s, null, out result); + + /// + /// Parse a string into a + /// + /// to be parsed. + /// Format provider used when parsing values. + /// A that was represented by . + /// Throws if can not be parsed. + public static Size Parse(string s, IFormatProvider? provider) { - if (TryParse(s, out var value)) + if (TryParse(s, provider, out var value)) return value; throw new ArgumentException("Argument can't be parsed.", nameof(s)); @@ -126,20 +154,24 @@ public static Size Parse(string s) /// Try to converts a string representation of a into a . /// /// to be parsed. - /// Ref parameter with the parsed . + /// Format provider used when parsing values. + /// Out parameter with the parsed . /// A to indicate if the parsing was successful. - public static bool TryParse(string s, out Size size) + public static bool TryParse([NotNullWhen(true)] string? s, IFormatProvider? provider, [MaybeNullWhen(false)] out Size result) { - size = default; + result = default; + + if (s is null) + return false; s = s.Trim(); var match = Regex.Match(s, "^W: ?(.+), ?H: ?(.+)$"); if (match.Success && match.Groups.Count == 3) { - if(Value.TryParse(match.Groups[1].Value, out var width) && Value.TryParse(match.Groups[2].Value, out var height)) + if (Value.TryParse(match.Groups[1].Value, provider, out var width) && Value.TryParse(match.Groups[2].Value, provider, out var height)) { - size = new Size(width, height); + result = new Size(width, height); return true; } } diff --git a/src/Synercoding.Primitives/Spacing.cs b/src/Synercoding.Primitives/Spacing.cs index bd84858..a81268b 100644 --- a/src/Synercoding.Primitives/Spacing.cs +++ b/src/Synercoding.Primitives/Spacing.cs @@ -1,5 +1,6 @@ using Synercoding.Primitives.Abstract; using System; +using System.Diagnostics.CodeAnalysis; using System.Text.Json.Serialization; using System.Text.RegularExpressions; @@ -9,7 +10,7 @@ namespace Synercoding.Primitives; /// Value type representing spacing in or around an object. /// [JsonConverter(typeof(JsonConverters.SpacingJsonConverter))] -public readonly record struct Spacing : IConvertable, IEquatable +public readonly record struct Spacing : IConvertable, IEquatable, IParsable { /// /// Constructor for . @@ -117,11 +118,19 @@ public override int GetHashCode() /// public override string ToString() + => ToString(null); + + /// + /// Returns a string representation of the spacing using the specified format provider. + /// + /// The format provider to use for formatting numeric values. + /// A string representation of the spacing. + public string ToString(IFormatProvider? provider) { if (Left == Top && Top == Right && Right == Bottom) - return $"All: {Left}"; + return $"All: {Left.ToString(provider)}"; - return $"L: {Left}, T: {Top}, R: {Right}, B: {Bottom}"; + return $"L: {Left.ToString(provider)}, T: {Top.ToString(provider)}, R: {Right.ToString(provider)}, B: {Bottom.ToString(provider)}"; } /// @@ -143,8 +152,27 @@ public bool Equals(Spacing other) /// A that was represented by . /// Throws if can not be parsed. public static Spacing Parse(string s) + => Parse(s, null); + + /// + /// Try to converts a string representation of a into a . + /// + /// to be parsed. + /// Ref parameter with the parsed . + /// A to indicate if the parsing was successful. + public static bool TryParse([NotNullWhen(true)] string? s, [MaybeNullWhen(false)] out Spacing result) + => TryParse(s, null, out result); + + /// + /// Parse a string into a + /// + /// to be parsed. + /// Format provider used when parsing values. + /// A that was represented by . + /// Throws if can not be parsed. + public static Spacing Parse(string s, IFormatProvider? provider) { - if (TryParse(s, out var value)) + if (TryParse(s, provider, out var value)) return value; throw new ArgumentException("Argument can't be parsed.", nameof(s)); @@ -154,29 +182,33 @@ public static Spacing Parse(string s) /// Try to converts a string representation of a into a . /// /// to be parsed. - /// Ref parameter with the parsed . + /// Format provider used when parsing values. + /// Out parameter with the parsed . /// A to indicate if the parsing was successful. - public static bool TryParse(string s, out Spacing spacing) + public static bool TryParse([NotNullWhen(true)] string? s, IFormatProvider? provider, [MaybeNullWhen(false)] out Spacing result) { - spacing = default; + result = default; + + if (s is null) + return false; s = s.Trim(); if (s.StartsWith("All:") && Value.TryParse(s.Substring(4).TrimStart(), out var all)) { - spacing = new Spacing(all); + result = new Spacing(all); return true; } var match = Regex.Match(s, "^L: ?(.+), ?T: ?(.+), ?R: ?(.+), ?B: ?(.+)$"); if (match.Success && match.Groups.Count == 5) { - if (Value.TryParse(match.Groups[1].Value, out var left) - && Value.TryParse(match.Groups[2].Value, out var top) - && Value.TryParse(match.Groups[3].Value, out var right) - && Value.TryParse(match.Groups[4].Value, out var bottom)) + if (Value.TryParse(match.Groups[1].Value, provider, out var left) + && Value.TryParse(match.Groups[2].Value, provider, out var top) + && Value.TryParse(match.Groups[3].Value, provider, out var right) + && Value.TryParse(match.Groups[4].Value, provider, out var bottom)) { - spacing = new Spacing(left, top, right, bottom); + result = new Spacing(left, top, right, bottom); return true; } } diff --git a/src/Synercoding.Primitives/Unit.cs b/src/Synercoding.Primitives/Unit.cs index 14a5f65..b1a0fda 100644 --- a/src/Synercoding.Primitives/Unit.cs +++ b/src/Synercoding.Primitives/Unit.cs @@ -1,7 +1,7 @@ using Synercoding.Primitives.Extensions; using System; +using System.Diagnostics.CodeAnalysis; using System.Text.Json.Serialization; -using System.Text.RegularExpressions; namespace Synercoding.Primitives; @@ -9,13 +9,13 @@ namespace Synercoding.Primitives; /// Value type representing an . /// [JsonConverter(typeof(JsonConverters.UnitJsonConverter))] -public readonly record struct Unit : IEquatable +public readonly record struct Unit : IEquatable, IParsable { private Unit(double perInch, UnitDesignation unitDesignation) { if (perInch <= 0) throw new ArgumentOutOfRangeException(nameof(perInch), "The value needs to be a non-zero-non-negative number."); - if (!Enum.IsDefined(typeof(UnitDesignation), unitDesignation)) + if (!Enum.IsDefined(unitDesignation)) throw new ArgumentOutOfRangeException(nameof(unitDesignation), $"The value needs to be a defined value of {nameof(UnitDesignation)}."); PerInch = perInch; @@ -43,8 +43,12 @@ public bool Equals(Unit other) } /// - public override string ToString() - => $"{Designation.Shortform()} ({PerInch} per inch)"; + public override string ToString() + => Designation switch + { + UnitDesignation.Pixels => $"dpi({PerInch})", + var x => x.Shortform() + }; /// public override int GetHashCode() => HashCode.Combine(PerInch, Designation); @@ -74,14 +78,14 @@ public override string ToString() /// /// The DPI the will represent. /// The representing pixels at a certain DPI. - public static Unit Pixels(int dpi) => new Unit(dpi, UnitDesignation.Pixels); + public static Unit Pixels(double dpi) => new Unit(dpi, UnitDesignation.Pixels); /// - /// Get the that correspons to the . + /// Get the that corresponds to the . /// /// The to get the for. /// The correct . - /// Throws when is provided. can only be used in combination with DPI by using the method. + /// Throws when is provided. can only be used in combination with DPI by using the method. public static Unit FromDesignation(UnitDesignation designation) => designation switch { @@ -93,15 +97,24 @@ public static Unit FromDesignation(UnitDesignation designation) _ => throw new NotImplementedException($"{designation} can not be used to get a Unit type.") }; + /// + /// Parse a string into a . + /// + /// to be parsed. + /// A that was represented by . + public static Unit Parse(string s) + => Parse(s, null); + /// /// Parse a string into a /// /// to be parsed. + /// Format provider used when parsing DPI values. /// A that was represented by . /// Throws if can not be parsed. - public static Unit Parse(string s) + public static Unit Parse(string s, IFormatProvider? provider) { - if (TryParse(s, out var unit)) + if (TryParse(s, provider, out var unit)) return unit; throw new ArgumentException("Argument can't be parsed.", nameof(s)); @@ -111,41 +124,53 @@ public static Unit Parse(string s) /// Try to converts a string representation of a into a . /// /// to be parsed. - /// Ref parameter with the parsed . + /// Out parameter with the parsed . + /// A to indicate if the parsing was successful. + public static bool TryParse([NotNullWhen(true)] string? s, [MaybeNullWhen(false)] out Unit result) + => TryParse(s, null, out result); + + /// + /// Try to converts a string representation of a into a . + /// + /// to be parsed. + /// Format provider used when parsing DPI values. + /// Out parameter with the parsed . /// A to indicate if the parsing was successful. - public static bool TryParse(string s, out Unit unit) + public static bool TryParse([NotNullWhen(true)] string? s, IFormatProvider? provider, [MaybeNullWhen(false)] out Unit result) { + result = default; + if (string.IsNullOrEmpty(s)) + return false; + s = s.Trim(); if (Enum.TryParse(s, true, out var designation) && designation != UnitDesignation.Pixels) { - unit = FromDesignation(designation); + result = FromDesignation(designation); return true; } - unit = s.ToLowerInvariant() switch + result = s.ToLowerInvariant() switch { UnitDesignationExtensions.MM => Millimeters, UnitDesignationExtensions.CM => Centimeters, UnitDesignationExtensions.IN => Inches, UnitDesignationExtensions.PTS => Points, - "millimeter" => Millimeters, - "centimeter" => Centimeters, - "inch" => Inches, - "point" => Points, + "millimeter" or "millimeters" => Millimeters, + "centimeter" or "centimeters" => Centimeters, + "inch" or "inches" or "\"" => Inches, + "point" or "points" => Points, _ => default }; - if(unit == default && s.StartsWith("dpi")) + if (result == default && s.StartsWith("dpi(", StringComparison.OrdinalIgnoreCase) && s.EndsWith(')')) { - var match = Regex.Match(s, "^dpi\\(([1-9][0-9]*(?:\\.[0-9]+)?)\\)$"); - if(match.Success && match.Groups.Count == 2) + if (double.TryParse(s[4..^1], provider, out double dpi)) { - var dpi = int.Parse(match.Groups[1].Value); - unit = Pixels(dpi); + result = Pixels(dpi); } } - return unit != default; + return result != default; } } diff --git a/src/Synercoding.Primitives/UnitDesignation.cs b/src/Synercoding.Primitives/UnitDesignation.cs index e5f579f..47d9d0e 100644 --- a/src/Synercoding.Primitives/UnitDesignation.cs +++ b/src/Synercoding.Primitives/UnitDesignation.cs @@ -25,4 +25,4 @@ public enum UnitDesignation : byte /// Inches /// Inches = 5 -} +} \ No newline at end of file diff --git a/src/Synercoding.Primitives/Value.cs b/src/Synercoding.Primitives/Value.cs index 2969d56..e569cea 100644 --- a/src/Synercoding.Primitives/Value.cs +++ b/src/Synercoding.Primitives/Value.cs @@ -1,6 +1,7 @@ using Synercoding.Primitives.Abstract; using Synercoding.Primitives.Extensions; using System; +using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Text.Json.Serialization; @@ -11,7 +12,7 @@ namespace Synercoding.Primitives; /// /// 3mm or 2 inches [JsonConverter(typeof(JsonConverters.ValueJsonConverter))] -public readonly record struct Value : IConvertable, IComparable, IComparable, IEquatable +public readonly record struct Value : IConvertable, IComparable, IComparable, IEquatable, IParsable { private const int ROUND_DIGITS = 15; @@ -77,7 +78,15 @@ public override int GetHashCode() /// public override string ToString() - => $"{Raw.ToString(CultureInfo.InvariantCulture)} {Unit.Designation.Shortform()}"; + => ToString(null); + + /// + /// Returns a string representation of the value using the specified format provider. + /// + /// The format provider to use for formatting numeric values. + /// A string representation of the value. + public string ToString(IFormatProvider? provider) + => $"{Raw.ToString(provider)} {Unit.Designation.Shortform()}"; /// public bool Equals(Value other) @@ -234,9 +243,33 @@ public bool Equals(Value other) /// to be parsed. /// A that was represented by . /// Throws if can not be parsed. - public static Value Parse(string s) + /// Throws when is null. + public static Value Parse(string? s) + => Parse(s, null); + + /// + /// Try to converts a string representation of a into a . + /// + /// to be parsed. + /// Out parameter with the parsed . + /// A to indicate if the parsing was successful. + public static bool TryParse([NotNullWhen(true)] string? s, out Value value) + => TryParse(s, null, out value); + + /// + /// Parse a string into a + /// + /// to be parsed. + /// Format provider used when parsing values. + /// The parsed value + /// Throws if can not be parsed. + /// Throws when is null. + public static Value Parse(string? s, IFormatProvider? provider) { - if (TryParse(s, out var value)) + if (s is null) + throw new ArgumentNullException(nameof(s)); + + if (TryParse(s, provider, out var value)) return value; throw new ArgumentException("Argument can't be parsed.", nameof(s)); @@ -246,11 +279,15 @@ public static Value Parse(string s) /// Try to converts a string representation of a into a . /// /// to be parsed. - /// Ref parameter with the parsed . + /// Format provider used when parsing values. + /// Out parameter with the parsed . /// A to indicate if the parsing was successful. - public static bool TryParse(string s, out Value value) + public static bool TryParse([NotNullWhen(true)] string? s, IFormatProvider? provider, [MaybeNullWhen(false)] out Value result) { - value = default; + result = default; + + if (s is null) + return false; s = s.Trim(); @@ -258,9 +295,9 @@ public static bool TryParse(string s, out Value value) while (index < s.Length) { - if (Unit.TryParse(s.Substring(index), out var unit) && double.TryParse(s.Substring(0, index), NumberStyles.Float | NumberStyles.AllowThousands, CultureInfo.InvariantCulture, out var number)) + if (Unit.TryParse(s.Substring(index), provider, out var unit) && double.TryParse(s.Substring(0, index), provider, out var number)) { - value = new Value(number, unit); + result = new Value(number, unit); return true; } index++; diff --git a/src/Synercoding.Primitives/ValueCreator.cs b/src/Synercoding.Primitives/ValueCreator.cs index 6377b94..ed6a690 100644 --- a/src/Synercoding.Primitives/ValueCreator.cs +++ b/src/Synercoding.Primitives/ValueCreator.cs @@ -34,10 +34,10 @@ public static class ValueCreator public static Value Pts(double pts) => new Value(pts, Unit.Points); /// - /// Create a new with + /// Create a new with /// /// The amount of pixels /// The dpi /// A new object. - public static Value Pixels(double dots, int dpi) => new Value(dots, Unit.Pixels(dpi)); + public static Value Pixels(double dots, double dpi) => new Value(dots, Unit.Pixels(dpi)); } diff --git a/tests/Synercoding.Primitives.Tests/PointTests.cs b/tests/Synercoding.Primitives.Tests/PointTests.cs index 4a82122..0963351 100644 --- a/tests/Synercoding.Primitives.Tests/PointTests.cs +++ b/tests/Synercoding.Primitives.Tests/PointTests.cs @@ -1,10 +1,12 @@ using System.Collections.Generic; +using System.Globalization; using Xunit; namespace Synercoding.Primitives.Tests; public class PointTests { + [Fact] public void EqualityOperator_InchAndMillimeters_NotEqual() { @@ -35,10 +37,10 @@ public void EqualityOperator_InchAnd254Millimeters_AreEqual() [Theory] [MemberData(nameof(DataForTryParse_With_IsCorrect))] - public void TryParse_With_IsCorrect(string input, Point value) + public void TryParse_With_IsCorrect(string input, Point value, CultureInfo culture) { // Act - _ = Point.TryParse(input, out var result); + _ = Point.TryParse(input, culture, out var result); // Assert Assert.Equal(value, result); @@ -47,16 +49,21 @@ public void TryParse_With_IsCorrect(string input, Point value) public static IEnumerable DataForTryParse_With_IsCorrect => new[] { - new object[]{ "X:11 mm,Y:123.14 mm", new Point(11, 123.14, Unit.Millimeters) }, + new object[]{ "X:11 mm,Y:123.14 mm", new Point(11, 123.14, Unit.Millimeters), CultureInfo.InvariantCulture }, + new object[]{ "X:1.5 mm,Y:2.75 cm", new Point(new Value(1.5, Unit.Millimeters), new Value(2.75, Unit.Centimeters)), new CultureInfo("en-US") }, + new object[]{ "X:1,5 mm,Y:2,75 cm", new Point(new Value(1.5, Unit.Millimeters), new Value(2.75, Unit.Centimeters)), new CultureInfo("de-DE") }, + new object[]{ "X:1,5 mm,Y:2,75 cm", new Point(new Value(1.5, Unit.Millimeters), new Value(2.75, Unit.Centimeters)), new CultureInfo("fr-FR") }, + new object[]{ "X:1,234.56 in,Y:-789.12 pts", new Point(new Value(1234.56, Unit.Inches), new Value(-789.12, Unit.Points)), new CultureInfo("en-US") }, + new object[]{ "X:1.234,56 in,Y:-789,12 pts", new Point(new Value(1234.56, Unit.Inches), new Value(-789.12, Unit.Points)), new CultureInfo("de-DE") }, }; [Theory] [MemberData(nameof(DataForToString_TryParse_Same_Value))] - public void ToString_TryParse_Same_Value(Point input) + public void ToString_TryParse_Same_Value(Point input, CultureInfo culture) { // Act - var asText = input.ToString(); - var parsed = Point.Parse(asText); + var asText = input.ToString(culture); + var parsed = Point.Parse(asText, culture); // Assert Assert.Equal(input, parsed); @@ -65,7 +72,13 @@ public void ToString_TryParse_Same_Value(Point input) public static IEnumerable DataForToString_TryParse_Same_Value => new[] { - new object[]{ new Point(1.23, 3.21, Unit.Millimeters) }, + new object[]{ new Point(1.23, 3.21, Unit.Millimeters), CultureInfo.InvariantCulture }, + new object[]{ new Point(1.5, 2.75, Unit.Millimeters), new CultureInfo("en-US") }, + new object[]{ new Point(1.5, 2.75, Unit.Centimeters), new CultureInfo("de-DE") }, + new object[]{ new Point(3.25, 4.5, Unit.Inches), new CultureInfo("fr-FR") }, + new object[]{ new Point(new Value(1234.56, Unit.Millimeters), new Value(-789.12, Unit.Points)), new CultureInfo("en-US") }, + new object[]{ new Point(new Value(1234.56, Unit.Millimeters), new Value(-789.12, Unit.Points)), new CultureInfo("de-DE") }, + new object[]{ new Point(new Value(1234.56, Unit.Millimeters), new Value(-789.12, Unit.Points)), new CultureInfo("fr-FR") }, }; [Theory] diff --git a/tests/Synercoding.Primitives.Tests/RectangleTests.cs b/tests/Synercoding.Primitives.Tests/RectangleTests.cs index 2b0398c..d92404d 100644 --- a/tests/Synercoding.Primitives.Tests/RectangleTests.cs +++ b/tests/Synercoding.Primitives.Tests/RectangleTests.cs @@ -1,11 +1,13 @@ using Synercoding.Primitives.Extensions; using System.Collections.Generic; +using System.Globalization; using Xunit; namespace Synercoding.Primitives.Tests; public class RectangleTests { + [Fact] public void Expand_Spacing_ResultsInLargerRectangle() { @@ -51,10 +53,10 @@ public void EqualityOperator_InchAnd254Millimeters_AreEqual() [Theory] [MemberData(nameof(DataForTryParse_With_IsCorrect))] - public void TryParse_With_IsCorrect(string input, Rectangle value) + public void TryParse_With_IsCorrect(string input, Rectangle value, CultureInfo culture) { // Act - _ = Rectangle.TryParse(input, out var result); + _ = Rectangle.TryParse(input, culture, out var result); // Assert Assert.Equal(value, result); @@ -63,16 +65,19 @@ public void TryParse_With_IsCorrect(string input, Rectangle value) public static IEnumerable DataForTryParse_With_IsCorrect => new[] { - new object[]{ "LLX:10 mm,LLY:11 mm,URX:110 mm,URY:111 mm", new Rectangle(10,11,110,111, Unit.Millimeters) }, + new object[]{ "LLX:10.5 mm,LLY:11 mm,URX:110 mm,URY:111 mm", new Rectangle(10.5,11,110,111, Unit.Millimeters), CultureInfo.InvariantCulture }, + new object[]{ "LLX:1.5 mm,LLY:2.75 cm,URX:3.25 in,URY:4.5 pts", new Rectangle(new Value(1.5, Unit.Millimeters), new Value(2.75, Unit.Centimeters), new Value(3.25, Unit.Inches), new Value(4.5, Unit.Points)), new CultureInfo("en-US") }, + new object[]{ "LLX:1,5 mm,LLY:2,75 cm,URX:3,25 in,URY:4,5 pts", new Rectangle(new Value(1.5, Unit.Millimeters), new Value(2.75, Unit.Centimeters), new Value(3.25, Unit.Inches), new Value(4.5, Unit.Points)), new CultureInfo("de-DE") }, + new object[]{ "LLX:1,5 mm,LLY:2,75 cm,URX:3,25 in,URY:4,5 pts", new Rectangle(new Value(1.5, Unit.Millimeters), new Value(2.75, Unit.Centimeters), new Value(3.25, Unit.Inches), new Value(4.5, Unit.Points)), new CultureInfo("fr-FR") }, }; [Theory] [MemberData(nameof(DataForToString_TryParse_Same_Value))] - public void ToString_TryParse_Same_Value(Rectangle input) + public void ToString_TryParse_Same_Value(Rectangle input, CultureInfo culture) { // Act - var asText = input.ToString(); - var parsed = Rectangle.Parse(asText); + var asText = input.ToString(culture); + var parsed = Rectangle.Parse(asText, culture); // Assert Assert.Equal(input, parsed); @@ -81,7 +86,12 @@ public void ToString_TryParse_Same_Value(Rectangle input) public static IEnumerable DataForToString_TryParse_Same_Value => new[] { - new object[]{ new Rectangle(1.23, 3.21, 12.34, 43.21, Unit.Millimeters) }, + new object[]{ new Rectangle(1.23, 3.21, 12.34, 43.21, Unit.Millimeters), CultureInfo.InvariantCulture }, + new object[]{ new Rectangle(1.5, 2.75, 3.25, 4.5, Unit.Millimeters), new CultureInfo("en-US") }, + new object[]{ new Rectangle(1.5, 2.75, 3.25, 4.5, Unit.Centimeters), new CultureInfo("de-DE") }, + new object[]{ new Rectangle(1.5, 2.75, 3.25, 4.5, Unit.Inches), new CultureInfo("fr-FR") }, + new object[]{ new Rectangle(new Value(1234.56, Unit.Millimeters), new Value(789.12, Unit.Centimeters), new Value(456.78, Unit.Inches), new Value(123.45, Unit.Points)), new CultureInfo("en-US") }, + new object[]{ new Rectangle(new Value(1234.56, Unit.Millimeters), new Value(789.12, Unit.Centimeters), new Value(456.78, Unit.Inches), new Value(123.45, Unit.Points)), new CultureInfo("de-DE") }, }; [Theory] diff --git a/tests/Synercoding.Primitives.Tests/SizeTests.cs b/tests/Synercoding.Primitives.Tests/SizeTests.cs index 3fd7434..8f21938 100644 --- a/tests/Synercoding.Primitives.Tests/SizeTests.cs +++ b/tests/Synercoding.Primitives.Tests/SizeTests.cs @@ -1,10 +1,12 @@ using System.Collections.Generic; +using System.Globalization; using Xunit; namespace Synercoding.Primitives.Tests; public class SizeTests { + [Fact] public void EqualityOperator_InchAndMillimeters_NotEqual() { @@ -35,10 +37,10 @@ public void EqualityOperator_InchAnd254Millimeters_AreEqual() [Theory] [MemberData(nameof(DataForTryParse_With_IsCorrect))] - public void TryParse_With_IsCorrect(string input, Size value) + public void TryParse_With_IsCorrect(string input, Size value, CultureInfo culture) { // Act - _ = Size.TryParse(input, out var result); + _ = Size.TryParse(input, culture, out var result); // Assert Assert.Equal(value, result); @@ -47,16 +49,21 @@ public void TryParse_With_IsCorrect(string input, Size value) public static IEnumerable DataForTryParse_With_IsCorrect => new[] { - new object[]{ "W:11 mm,H:123.14 mm", new Size(11, 123.14, Unit.Millimeters) }, + new object[]{ "W:11 mm,H:123.14 mm", new Size(11, 123.14, Unit.Millimeters), CultureInfo.InvariantCulture }, + new object[]{ "W:1.5 mm,H:2.75 cm", new Size(new Value(1.5, Unit.Millimeters), new Value(2.75, Unit.Centimeters)), new CultureInfo("en-US") }, + new object[]{ "W:1,5 mm,H:2,75 cm", new Size(new Value(1.5, Unit.Millimeters), new Value(2.75, Unit.Centimeters)), new CultureInfo("de-DE") }, + new object[]{ "W:1,5 mm,H:2,75 cm", new Size(new Value(1.5, Unit.Millimeters), new Value(2.75, Unit.Centimeters)), new CultureInfo("fr-FR") }, + new object[]{ "W:1,234.56 in,H:-789.12 pts", new Size(new Value(1234.56, Unit.Inches), new Value(-789.12, Unit.Points)), new CultureInfo("en-US") }, + new object[]{ "W:1.234,56 in,H:-789,12 pts", new Size(new Value(1234.56, Unit.Inches), new Value(-789.12, Unit.Points)), new CultureInfo("de-DE") }, }; [Theory] [MemberData(nameof(DataForToString_TryParse_Same_Value))] - public void ToString_TryParse_Same_Value(Size input) + public void ToString_TryParse_Same_Value(Size input, CultureInfo culture) { // Act - var asText = input.ToString(); - var parsed = Size.Parse(asText); + var asText = input.ToString(culture); + var parsed = Size.Parse(asText, culture); // Assert Assert.Equal(input, parsed); @@ -65,7 +72,13 @@ public void ToString_TryParse_Same_Value(Size input) public static IEnumerable DataForToString_TryParse_Same_Value => new[] { - new object[]{ new Size(1.23, 3.21, Unit.Millimeters) }, + new object[]{ new Size(1.23, 3.21, Unit.Millimeters), CultureInfo.InvariantCulture }, + new object[]{ new Size(1.5, 2.75, Unit.Millimeters), new CultureInfo("en-US") }, + new object[]{ new Size(1.5, 2.75, Unit.Centimeters), new CultureInfo("de-DE") }, + new object[]{ new Size(3.25, 4.5, Unit.Inches), new CultureInfo("fr-FR") }, + new object[]{ new Size(new Value(1234.56, Unit.Millimeters), new Value(-789.12, Unit.Points)), new CultureInfo("en-US") }, + new object[]{ new Size(new Value(1234.56, Unit.Millimeters), new Value(-789.12, Unit.Points)), new CultureInfo("de-DE") }, + new object[]{ new Size(new Value(1234.56, Unit.Millimeters), new Value(-789.12, Unit.Points)), new CultureInfo("fr-FR") }, }; [Theory] diff --git a/tests/Synercoding.Primitives.Tests/SpacingTests.cs b/tests/Synercoding.Primitives.Tests/SpacingTests.cs index 74077d4..4fdfd89 100644 --- a/tests/Synercoding.Primitives.Tests/SpacingTests.cs +++ b/tests/Synercoding.Primitives.Tests/SpacingTests.cs @@ -1,10 +1,12 @@ using System.Collections.Generic; +using System.Globalization; using Xunit; namespace Synercoding.Primitives.Tests; public class SpacingTests { + [Fact] public void EqualityOperator_InchAndMillimeters_NotEqual() { @@ -35,10 +37,10 @@ public void EqualityOperator_InchAnd254Millimeters_AreEqual() [Theory] [MemberData(nameof(DataForTryParse_With_IsCorrect))] - public void TryParse_With_IsCorrect(string input, Spacing value) + public void TryParse_With_IsCorrect(string input, Spacing value, CultureInfo culture) { // Act - _ = Spacing.TryParse(input, out var result); + _ = Spacing.TryParse(input, culture, out var result); // Assert Assert.Equal(value, result); @@ -47,16 +49,19 @@ public void TryParse_With_IsCorrect(string input, Spacing value) public static IEnumerable DataForTryParse_With_IsCorrect => new[] { - new object[]{ "L:10 mm,T:11 mm,R:110 mm,B:111 mm", new Spacing(10,11,110,111, Unit.Millimeters) }, + new object[]{ "L:10.5 mm,T:11 mm,R:110 mm,B:111 mm", new Spacing(10.5,11,110,111, Unit.Millimeters), CultureInfo.InvariantCulture }, + new object[]{ "L:1.5 mm,T:2.75 cm,R:3.25 in,B:4.5 pts", new Spacing(new Value(1.5, Unit.Millimeters), new Value(2.75, Unit.Centimeters), new Value(3.25, Unit.Inches), new Value(4.5, Unit.Points)), new CultureInfo("en-US") }, + new object[]{ "L:1,5 mm,T:2,75 cm,R:3,25 in,B:4,5 pts", new Spacing(new Value(1.5, Unit.Millimeters), new Value(2.75, Unit.Centimeters), new Value(3.25, Unit.Inches), new Value(4.5, Unit.Points)), new CultureInfo("de-DE") }, + new object[]{ "L:1,5 mm,T:2,75 cm,R:3,25 in,B:4,5 pts", new Spacing(new Value(1.5, Unit.Millimeters), new Value(2.75, Unit.Centimeters), new Value(3.25, Unit.Inches), new Value(4.5, Unit.Points)), new CultureInfo("fr-FR") }, }; [Theory] [MemberData(nameof(DataForToString_TryParse_Same_Value))] - public void ToString_TryParse_Same_Value(Spacing input) + public void ToString_TryParse_Same_Value(Spacing input, CultureInfo culture) { // Act - var asText = input.ToString(); - var parsed = Spacing.Parse(asText); + var asText = input.ToString(culture); + var parsed = Spacing.Parse(asText, culture); // Assert Assert.Equal(input, parsed); @@ -65,7 +70,12 @@ public void ToString_TryParse_Same_Value(Spacing input) public static IEnumerable DataForToString_TryParse_Same_Value => new[] { - new object[]{ new Spacing(1.23, 3.21, 12.34, 43.21, Unit.Millimeters) }, + new object[]{ new Spacing(1.23, 3.21, 12.34, 43.21, Unit.Millimeters), CultureInfo.InvariantCulture }, + new object[]{ new Spacing(1.5, 2.75, 3.25, 4.5, Unit.Millimeters), new CultureInfo("en-US") }, + new object[]{ new Spacing(1.5, 2.75, 3.25, 4.5, Unit.Centimeters), new CultureInfo("de-DE") }, + new object[]{ new Spacing(1.5, 2.75, 3.25, 4.5, Unit.Inches), new CultureInfo("fr-FR") }, + new object[]{ new Spacing(new Value(1234.56, Unit.Millimeters), new Value(789.12, Unit.Centimeters), new Value(456.78, Unit.Inches), new Value(123.45, Unit.Points)), new CultureInfo("en-US") }, + new object[]{ new Spacing(new Value(1234.56, Unit.Millimeters), new Value(789.12, Unit.Centimeters), new Value(456.78, Unit.Inches), new Value(123.45, Unit.Points)), new CultureInfo("de-DE") }, }; [Theory] diff --git a/tests/Synercoding.Primitives.Tests/UnitDesignationTests.cs b/tests/Synercoding.Primitives.Tests/UnitDesignationTests.cs index baeea32..192c48e 100644 --- a/tests/Synercoding.Primitives.Tests/UnitDesignationTests.cs +++ b/tests/Synercoding.Primitives.Tests/UnitDesignationTests.cs @@ -1,6 +1,5 @@ using Synercoding.Primitives.Extensions; using System; -using System.Linq; using Xunit; namespace Synercoding.Primitives.Tests; @@ -10,8 +9,8 @@ public class UnitDesignationTests [Fact] public void Shortform_AllEnums_HaveShortform() { - // Arrange - var unitDesignations = Enum.GetValues(typeof(UnitDesignation)).OfType(); + // Arrange + var unitDesignations = Enum.GetValues(); // Act foreach (var designation in unitDesignations) diff --git a/tests/Synercoding.Primitives.Tests/UnitTests.cs b/tests/Synercoding.Primitives.Tests/UnitTests.cs index b683780..78db200 100644 --- a/tests/Synercoding.Primitives.Tests/UnitTests.cs +++ b/tests/Synercoding.Primitives.Tests/UnitTests.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Globalization; using Xunit; namespace Synercoding.Primitives.Tests; @@ -9,11 +10,21 @@ public class UnitTests [MemberData(nameof(DataFor_TryParse_ReturnsTrue_And_Unit))] public void TryParse_ReturnsTrue_And_Unit(string unit, Unit expected) { - // Act - Unit.TryParse(unit, out var result); + var oldCulture = CultureInfo.CurrentCulture; + try + { + // Assume invariant culture as the default culture for this test. + CultureInfo.CurrentCulture = CultureInfo.InvariantCulture; - // Assert - Assert.Equal(expected, result); + // Act + Unit.TryParse(unit, out var result); + + // Assert + Assert.Equal(expected, result); + }finally + { + CultureInfo.CurrentCulture = oldCulture; + } } public static IEnumerable DataFor_TryParse_ReturnsTrue_And_Unit() @@ -28,8 +39,33 @@ public static IEnumerable DataFor_TryParse_ReturnsTrue_And_Unit() new object[]{ "in", Unit.Inches }, new object[]{ "inch", Unit.Inches }, new object[]{ "inches", Unit.Inches }, + new object[]{ "\"", Unit.Inches }, new object[]{ "pts", Unit.Points }, new object[]{ "points", Unit.Points }, new object[]{ "dpi(150)", Unit.Pixels(150) }, + new object[]{ "dpi(150.5)", Unit.Pixels(150.5) }, + }; + + [Theory] + [MemberData(nameof(DataFor_TryParse_WithCulture_ReturnsTrue_And_Unit))] + public void TryParse_WithCulture_ReturnsTrue_And_Unit(string unit, Unit expected, CultureInfo culture) + { + // Act + Unit.TryParse(unit, culture, out var result); + + // Assert + Assert.Equal(expected, result); + } + + public static IEnumerable DataFor_TryParse_WithCulture_ReturnsTrue_And_Unit() + => new[] + { + new object[]{ "dpi(150.5)", Unit.Pixels(150.5), CultureInfo.InvariantCulture }, + new object[]{ "dpi(150.5)", Unit.Pixels(150.5), new CultureInfo("en-US") }, + new object[]{ "dpi(150,5)", Unit.Pixels(150.5), new CultureInfo("de-DE") }, + new object[]{ "dpi(150,5)", Unit.Pixels(150.5), new CultureInfo("fr-FR") }, + new object[]{ "dpi(1234.56)", Unit.Pixels(1234.56), new CultureInfo("en-US") }, + new object[]{ "dpi(1.234,56)", Unit.Pixels(1234.56), new CultureInfo("de-DE") }, + new object[]{ "dpi(1 234,56)", Unit.Pixels(1234.56), new CultureInfo("fr-FR") }, }; } diff --git a/tests/Synercoding.Primitives.Tests/ValueTests.cs b/tests/Synercoding.Primitives.Tests/ValueTests.cs index 509c357..62a35f7 100644 --- a/tests/Synercoding.Primitives.Tests/ValueTests.cs +++ b/tests/Synercoding.Primitives.Tests/ValueTests.cs @@ -1,375 +1,446 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using Xunit; -namespace Synercoding.Primitives.Tests +namespace Synercoding.Primitives.Tests; + +public class ValueTests { - public class ValueTests + + [Fact] + public void LessThanOperator_MillimetersAndInch_IsSmaller() { - [Fact] - public void LessThanOperator_MillimetersAndInch_IsSmaller() - { - // Arrange - var mm = new Value(1, Unit.Millimeters); - var inch = new Value(1, Unit.Inches); + // Arrange + var mm = new Value(1, Unit.Millimeters); + var inch = new Value(1, Unit.Inches); - // Act - var result = mm < inch; + // Act + var result = mm < inch; - // Assert - Assert.True(result); - } + // Assert + Assert.True(result); + } - [Fact] - public void LessThanOperator_PointsAndMillimeters_IsSmaller() - { - // Arrange - var mm = new Value(1, Unit.Millimeters); - var point = new Value(1, Unit.Points); + [Fact] + public void LessThanOperator_PointsAndMillimeters_IsSmaller() + { + // Arrange + var mm = new Value(1, Unit.Millimeters); + var point = new Value(1, Unit.Points); - // Act - var result = point < mm; + // Act + var result = point < mm; - // Assert - Assert.True(result); - } + // Assert + Assert.True(result); + } - [Fact] - public void EqualsOperator_25dot4MmAndOneInch_AreEqual() - { - // Arrange - var mm = new Value(25.4, Unit.Millimeters); - var inch = new Value(1, Unit.Inches); + [Fact] + public void EqualsOperator_25dot4MmAndOneInch_AreEqual() + { + // Arrange + var mm = new Value(25.4, Unit.Millimeters); + var inch = new Value(1, Unit.Inches); - // Act - var result = mm == inch; + // Act + var result = mm == inch; - // Assert - Assert.True(result); - } + // Assert + Assert.True(result); + } - [Fact] - public void Convert_OneInchToMillimeters_25dot4Millimeters() - { - // Arrange - var inch = new Value(1, Unit.Inches); + [Fact] + public void Convert_OneInchToMillimeters_25dot4Millimeters() + { + // Arrange + var inch = new Value(1, Unit.Inches); - // Act - var mm = inch.ConvertTo(Unit.Millimeters); + // Act + var mm = inch.ConvertTo(Unit.Millimeters); - // Assert - Assert.True(mm.Unit == Unit.Millimeters); - Assert.True(mm.Raw == 25.4); - } + // Assert + Assert.True(mm.Unit == Unit.Millimeters); + Assert.True(mm.Raw == 25.4); + } - [Fact] - public void EqualsOperator_72pointsAndOneInch_AreEqual() - { - // Arrange - var points = new Value(72, Unit.Points); - var inch = new Value(1, Unit.Inches); + [Fact] + public void EqualsOperator_72pointsAndOneInch_AreEqual() + { + // Arrange + var points = new Value(72, Unit.Points); + var inch = new Value(1, Unit.Inches); - // Act - var result = points == inch; + // Act + var result = points == inch; - // Assert - Assert.True(result); - } + // Assert + Assert.True(result); + } - [Fact] - public void EqualsOperator_600pxAt300dpiAndTwoInches_AreEqual() - { - // Arrange - var pixels = new Value(600, Unit.Pixels(300)); - var inch = new Value(2, Unit.Inches); + [Fact] + public void EqualsOperator_600pxAt300dpiAndTwoInches_AreEqual() + { + // Arrange + var pixels = new Value(600, Unit.Pixels(300)); + var inch = new Value(2, Unit.Inches); - // Act - var result = pixels == inch; + // Act + var result = pixels == inch; - // Assert - Assert.True(result); - } + // Assert + Assert.True(result); + } - [Fact] - public void AdditionOperator_MillimetersAndInches_ResultIs26dot4mm() - { - // Arrange - var mm = new Value(1, Unit.Millimeters); - var inch = new Value(1, Unit.Inches); - var expected = new Value(26.4, Unit.Millimeters); + [Fact] + public void AdditionOperator_MillimetersAndInches_ResultIs26dot4mm() + { + // Arrange + var mm = new Value(1, Unit.Millimeters); + var inch = new Value(1, Unit.Inches); + var expected = new Value(26.4, Unit.Millimeters); - // Act - var result = mm + inch; + // Act + var result = mm + inch; - // Assert - Assert.Equal(expected, result); - } + // Assert + Assert.Equal(expected, result); + } - [Fact] - public void DivisionOperator_1cmAnd1mm_containes10times() - { - // Arrange - var cm = new Value(1, Unit.Centimeters); - var mm = new Value(1, Unit.Millimeters); - var expected = 10; + [Fact] + public void DivisionOperator_1cmAnd1mm_containes10times() + { + // Arrange + var cm = new Value(1, Unit.Centimeters); + var mm = new Value(1, Unit.Millimeters); + var expected = 10; - // Act - var result = cm / mm; + // Act + var result = cm / mm; - // Assert - Assert.Equal(expected, result); - } + // Assert + Assert.Equal(expected, result); + } - [Fact] - public void DivisionOperator_1cmAnd2_Is0dot5cm() - { - // Arrange - var cm = new Value(1, Unit.Centimeters); - var amount = 2; - var expected = new Value(0.5, Unit.Centimeters); + [Fact] + public void DivisionOperator_1cmAnd2_Is0dot5cm() + { + // Arrange + var cm = new Value(1, Unit.Centimeters); + var amount = 2; + var expected = new Value(0.5, Unit.Centimeters); - // Act - var result = cm / amount; + // Act + var result = cm / amount; - // Assert - Assert.Equal(expected, result); - } + // Assert + Assert.Equal(expected, result); + } - [Fact] - public void FromDesignation_AllEnumValuesExceptPx_DoesNotThrowNotImplementedException() - { - var unitDesignations = Enum.GetValues(typeof(UnitDesignation)) - .OfType() - .Where(x => x != UnitDesignation.Pixels) // Exclude pixels, only works when providing a DPI - .ToArray(); + [Fact] + public void FromDesignation_AllEnumValuesExceptPx_DoesNotThrowNotImplementedException() + { + var unitDesignations = Enum.GetValues(typeof(UnitDesignation)) + .OfType() + .Where(x => x != UnitDesignation.Pixels) // Exclude pixels, only works when providing a DPI + .ToArray(); - foreach (var designation in unitDesignations) - _ = Unit.FromDesignation(designation); - } + foreach (var designation in unitDesignations) + _ = Unit.FromDesignation(designation); + } - [Fact] - public void FromDesignation_WithPixels_ThrowsArgumentException() - { - var designation = UnitDesignation.Pixels; + [Fact] + public void FromDesignation_WithPixels_ThrowsArgumentException() + { + var designation = UnitDesignation.Pixels; - Assert.Throws(() => Unit.FromDesignation(designation)); - } + Assert.Throws(() => Unit.FromDesignation(designation)); + } - [Fact] - public void MultiplicationOperator_WithLeftDouble_IsUnitValue() - { - // Arrange - var left = 2d; - var right = new Value(5, Unit.Millimeters); - var expected = new Value(10, Unit.Millimeters); - - // Act - var result = left * right; - - // Assert - Assert.Equal(expected, result); - Assert.Equal(expected.Raw, result.Raw); - Assert.Equal(expected.Unit, result.Unit); - } - - [Fact] - public void MultiplicationOperator_WithRightDouble_IsUnitValue() - { - // Arrange - var left = new Value(5, Unit.Millimeters); - var right = 2d; - var expected = new Value(10, Unit.Millimeters); - - // Act - var result = left * right; - - // Assert - Assert.Equal(expected, result); - Assert.Equal(expected.Raw, result.Raw); - Assert.Equal(expected.Unit, result.Unit); - } - - [Theory] - [InlineData("1mm")] - [InlineData("100cm")] - [InlineData("100pts")] - [InlineData("300dpi(100)")] - [InlineData("100dpi(300)")] - [InlineData("1 mm")] - [InlineData("100 cm")] - [InlineData("100 pts")] - [InlineData("300 dpi(100)")] - [InlineData("100 dpi(300)")] - public void TryParse_With_CanParse(string input) - { - // Act - var result = Value.TryParse(input, out _); + [Fact] + public void MultiplicationOperator_WithLeftDouble_IsUnitValue() + { + // Arrange + var left = 2d; + var right = new Value(5, Unit.Millimeters); + var expected = new Value(10, Unit.Millimeters); + + // Act + var result = left * right; + + // Assert + Assert.Equal(expected, result); + Assert.Equal(expected.Raw, result.Raw); + Assert.Equal(expected.Unit, result.Unit); + } - // Assert - Assert.True(result); - } + [Fact] + public void MultiplicationOperator_WithRightDouble_IsUnitValue() + { + // Arrange + var left = new Value(5, Unit.Millimeters); + var right = 2d; + var expected = new Value(10, Unit.Millimeters); + + // Act + var result = left * right; + + // Assert + Assert.Equal(expected, result); + Assert.Equal(expected.Raw, result.Raw); + Assert.Equal(expected.Unit, result.Unit); + } - [Theory] - [MemberData(nameof(DataForToString_TryParse_Same_Value))] - public void ToString_TryParse_Same_Value(Value input) - { - // Act - var asText = input.ToString(); - var parsed = Value.Parse(asText); + [Theory] + [InlineData("1mm")] + [InlineData("100cm")] + [InlineData("100pts")] + [InlineData("300dpi(100)")] + [InlineData("100dpi(300)")] + [InlineData("1 mm")] + [InlineData("100 cm")] + [InlineData("100 pts")] + [InlineData("300 dpi(100)")] + [InlineData("100 dpi(300)")] + [InlineData("1 in")] + [InlineData("1 inch")] + [InlineData("1 inches")] + [InlineData("1in")] + [InlineData("1inch")] + [InlineData("1inches")] + [InlineData("1\"")] + public void TryParse_With_CanParse(string input) + { + // Act + var result = Value.TryParse(input, out _); - // Assert - Assert.Equal(input, parsed); - } + // Assert + Assert.True(result); + } - [Theory] - [MemberData(nameof(DataForTryParse_With_IsCorrect))] - public void TryParse_With_IsCorrect(string input, Value value) - { - // Act - _ = Value.TryParse(input, out var result); + [Theory] + [MemberData(nameof(DataForToString_TryParse_Same_Value))] + public void ToString_TryParse_Same_Value(Value input, CultureInfo culture) + { + // Act + var asText = input.ToString(culture); + var parsed = Value.Parse(asText, culture); - // Assert - Assert.Equal(value, result); - } + // Assert + Assert.Equal(input, parsed); + } - [Theory] - [MemberData(nameof(DataForConvert_To_Json_IsCorrect))] - public void Convert_To_Json_IsCorrect(Value value, string expected) - { - // Act - var result = System.Text.Json.JsonSerializer.Serialize(value); + [Theory] + [MemberData(nameof(DataForTryParse_With_IsCorrect))] + public void TryParse_With_IsCorrect(string input, Value value, CultureInfo culture) + { + // Act + _ = Value.TryParse(input, culture, out var result); - // Assert - Assert.Equal(expected, result); - } + // Assert + Assert.Equal(value, result); + } + + [Theory] + [MemberData(nameof(DataForConvert_To_Json_IsCorrect))] + public void Convert_To_Json_IsCorrect(Value value, string expected) + { + // Act + var result = System.Text.Json.JsonSerializer.Serialize(value); + + // Assert + Assert.Equal(expected, result); + } + + [Theory] + [MemberData(nameof(DataForConvert_From_Json_IsCorrect))] + public void Convert_From_Json_IsCorrect(string value, Value expected) + { + // Act + var result = System.Text.Json.JsonSerializer.Deserialize(value); + + // Assert + Assert.Equal(expected, result); + } - [Theory] - [MemberData(nameof(DataForConvert_From_Json_IsCorrect))] - public void Convert_From_Json_IsCorrect(string value, Value expected) + public static IEnumerable DataForConvert_To_Json_IsCorrect + => new[] + { + new object[]{ new Value(1, Unit.Millimeters), "\"1 mm\"" }, + new object[]{ new Value(10, Unit.Millimeters), "\"10 mm\"" }, + new object[]{ new Value(-15, Unit.Millimeters), "\"-15 mm\"" }, + new object[]{ new Value(1, Unit.Points), "\"1 pts\"" }, + new object[]{ new Value(10, Unit.Points), "\"10 pts\"" }, + new object[]{ new Value(-15, Unit.Points), "\"-15 pts\"" }, + new object[]{ new Value(1, Unit.Inches), "\"1 in\"" }, + new object[]{ new Value(10, Unit.Inches), "\"10 in\"" }, + new object[]{ new Value(-15, Unit.Inches), "\"-15 in\"" }, + new object[]{ new Value(1, Unit.Centimeters), "\"1 cm\"" }, + new object[]{ new Value(10, Unit.Centimeters), "\"10 cm\"" }, + new object[]{ new Value(-15, Unit.Centimeters), "\"-15 cm\"" }, + }; + + public static IEnumerable DataForConvert_From_Json_IsCorrect + => new[] + { + new object[]{ "\"1 mm\"", new Value(1, Unit.Millimeters) }, + new object[]{ "\"10 mm\"", new Value(10, Unit.Millimeters) }, + new object[]{ "\"-15 mm\"", new Value(-15, Unit.Millimeters) }, + new object[]{ "\"1 pts\"", new Value(1, Unit.Points) }, + new object[]{ "\"10 pts\"", new Value(10, Unit.Points) }, + new object[]{ "\"-15 pts\"", new Value(-15, Unit.Points) }, + new object[]{ "\"1 in\"", new Value(1, Unit.Inches) }, + new object[]{ "\"10 in\"", new Value(10, Unit.Inches) }, + new object[]{ "\"-15 in\"", new Value(-15, Unit.Inches) }, + new object[]{ "\"1 cm\"", new Value(1, Unit.Centimeters) }, + new object[]{ "\"10 cm\"", new Value(10, Unit.Centimeters) }, + new object[]{ "\"-15 cm\"", new Value(-15, Unit.Centimeters) }, + + new object[]{ "\"1mm\"", new Value(1, Unit.Millimeters) }, + new object[]{ "\"10mm\"", new Value(10, Unit.Millimeters) }, + new object[]{ "\"-15mm\"", new Value(-15, Unit.Millimeters) }, + new object[]{ "\"1pts\"", new Value(1, Unit.Points) }, + new object[]{ "\"10pts\"", new Value(10, Unit.Points) }, + new object[]{ "\"-15pts\"", new Value(-15, Unit.Points) }, + new object[]{ "\"1in\"", new Value(1, Unit.Inches) }, + new object[]{ "\"10in\"", new Value(10, Unit.Inches) }, + new object[]{ "\"-15in\"", new Value(-15, Unit.Inches) }, + new object[]{ "\"1cm\"", new Value(1, Unit.Centimeters) }, + new object[]{ "\"10cm\"", new Value(10, Unit.Centimeters) }, + new object[]{ "\"-15cm\"", new Value(-15, Unit.Centimeters) }, + + new object[]{ "{ \"Raw\": 12.3, \"Unit\": \"mm\" }", new Value(12.3, Unit.Millimeters) }, + }; + + public static IEnumerable DataForToString_TryParse_Same_Value + => new[] + { + new object[]{ new Value(1, Unit.Millimeters), CultureInfo.InvariantCulture }, + new object[]{ new Value(1, Unit.Millimeters), CultureInfo.InvariantCulture }, + new object[]{ new Value(-1, Unit.Millimeters), CultureInfo.InvariantCulture }, + new object[]{ new Value(-1, Unit.Millimeters), CultureInfo.InvariantCulture }, + new object[]{ new Value(1.10, Unit.Millimeters), CultureInfo.InvariantCulture }, + new object[]{ new Value(1.10, Unit.Millimeters), CultureInfo.InvariantCulture }, + new object[]{ new Value(-1.10, Unit.Millimeters), CultureInfo.InvariantCulture }, + new object[]{ new Value(-1.10, Unit.Millimeters), CultureInfo.InvariantCulture }, + new object[]{ new Value(10, Unit.Centimeters), CultureInfo.InvariantCulture }, + new object[]{ new Value(10, Unit.Centimeters), CultureInfo.InvariantCulture }, + new object[]{ new Value(-10, Unit.Centimeters), CultureInfo.InvariantCulture }, + new object[]{ new Value(-10, Unit.Centimeters), CultureInfo.InvariantCulture }, + + new object[]{ new Value(1.5, Unit.Millimeters), new CultureInfo("en-US") }, + new object[]{ new Value(-2.75, Unit.Centimeters), new CultureInfo("en-US") }, + new object[]{ new Value(3.25, Unit.Inches), new CultureInfo("en-US") }, + new object[]{ new Value(12.5, Unit.Points), new CultureInfo("en-US") }, + + new object[]{ new Value(1.5, Unit.Millimeters), new CultureInfo("de-DE") }, + new object[]{ new Value(-2.75, Unit.Centimeters), new CultureInfo("de-DE") }, + new object[]{ new Value(3.25, Unit.Inches), new CultureInfo("de-DE") }, + new object[]{ new Value(12.5, Unit.Points), new CultureInfo("de-DE") }, + + new object[]{ new Value(1.5, Unit.Millimeters), new CultureInfo("fr-FR") }, + new object[]{ new Value(-2.75, Unit.Centimeters), new CultureInfo("fr-FR") }, + new object[]{ new Value(3.25, Unit.Inches), new CultureInfo("fr-FR") }, + new object[]{ new Value(12.5, Unit.Points), new CultureInfo("fr-FR") }, + + new object[]{ new Value(1234.56, Unit.Millimeters), new CultureInfo("en-US") }, + new object[]{ new Value(1234.56, Unit.Millimeters), new CultureInfo("de-DE") }, + new object[]{ new Value(1234.56, Unit.Millimeters), new CultureInfo("fr-FR") }, + }; + + public static IEnumerable DataForTryParse_With_IsCorrect + => new[] { - // Act - var result = System.Text.Json.JsonSerializer.Deserialize(value); - - // Assert - Assert.Equal(expected, result); - } - - public static IEnumerable DataForConvert_To_Json_IsCorrect - => new[] - { - new object[]{ new Value(1, Unit.Millimeters), "\"1 mm\"" }, - new object[]{ new Value(10, Unit.Millimeters), "\"10 mm\"" }, - new object[]{ new Value(-15, Unit.Millimeters), "\"-15 mm\"" }, - new object[]{ new Value(1, Unit.Points), "\"1 pts\"" }, - new object[]{ new Value(10, Unit.Points), "\"10 pts\"" }, - new object[]{ new Value(-15, Unit.Points), "\"-15 pts\"" }, - new object[]{ new Value(1, Unit.Inches), "\"1 in\"" }, - new object[]{ new Value(10, Unit.Inches), "\"10 in\"" }, - new object[]{ new Value(-15, Unit.Inches), "\"-15 in\"" }, - new object[]{ new Value(1, Unit.Centimeters), "\"1 cm\"" }, - new object[]{ new Value(10, Unit.Centimeters), "\"10 cm\"" }, - new object[]{ new Value(-15, Unit.Centimeters), "\"-15 cm\"" }, - }; - - public static IEnumerable DataForConvert_From_Json_IsCorrect - => new[] - { - new object[]{ "\"1 mm\"", new Value(1, Unit.Millimeters) }, - new object[]{ "\"10 mm\"", new Value(10, Unit.Millimeters) }, - new object[]{ "\"-15 mm\"", new Value(-15, Unit.Millimeters) }, - new object[]{ "\"1 pts\"", new Value(1, Unit.Points) }, - new object[]{ "\"10 pts\"", new Value(10, Unit.Points) }, - new object[]{ "\"-15 pts\"", new Value(-15, Unit.Points) }, - new object[]{ "\"1 in\"", new Value(1, Unit.Inches) }, - new object[]{ "\"10 in\"", new Value(10, Unit.Inches) }, - new object[]{ "\"-15 in\"", new Value(-15, Unit.Inches) }, - new object[]{ "\"1 cm\"", new Value(1, Unit.Centimeters) }, - new object[]{ "\"10 cm\"", new Value(10, Unit.Centimeters) }, - new object[]{ "\"-15 cm\"", new Value(-15, Unit.Centimeters) }, - - new object[]{ "\"1mm\"", new Value(1, Unit.Millimeters) }, - new object[]{ "\"10mm\"", new Value(10, Unit.Millimeters) }, - new object[]{ "\"-15mm\"", new Value(-15, Unit.Millimeters) }, - new object[]{ "\"1pts\"", new Value(1, Unit.Points) }, - new object[]{ "\"10pts\"", new Value(10, Unit.Points) }, - new object[]{ "\"-15pts\"", new Value(-15, Unit.Points) }, - new object[]{ "\"1in\"", new Value(1, Unit.Inches) }, - new object[]{ "\"10in\"", new Value(10, Unit.Inches) }, - new object[]{ "\"-15in\"", new Value(-15, Unit.Inches) }, - new object[]{ "\"1cm\"", new Value(1, Unit.Centimeters) }, - new object[]{ "\"10cm\"", new Value(10, Unit.Centimeters) }, - new object[]{ "\"-15cm\"", new Value(-15, Unit.Centimeters) }, - - new object[]{ "{ \"Raw\": 12.3, \"Unit\": \"mm\" }", new Value(12.3, Unit.Millimeters) }, - }; - - public static IEnumerable DataForToString_TryParse_Same_Value - => new[] - { - new object[]{ new Value(1, Unit.Millimeters) }, - new object[]{ new Value(1, Unit.Millimeters) }, - new object[]{ new Value(-1, Unit.Millimeters) }, - new object[]{ new Value(-1, Unit.Millimeters) }, - new object[]{ new Value(1.10, Unit.Millimeters) }, - new object[]{ new Value(1.10, Unit.Millimeters) }, - new object[]{ new Value(-1.10, Unit.Millimeters) }, - new object[]{ new Value(-1.10, Unit.Millimeters) }, - new object[]{ new Value(10, Unit.Centimeters) }, - new object[]{ new Value(10, Unit.Centimeters) }, - new object[]{ new Value(-10, Unit.Centimeters) }, - new object[]{ new Value(-10, Unit.Centimeters) }, - }; - - public static IEnumerable DataForTryParse_With_IsCorrect - => new[] - { - new object[]{ "1mm", new Value(1, Unit.Millimeters) }, - new object[]{ "1 mm", new Value(1, Unit.Millimeters) }, - new object[]{ "-1mm", new Value(-1, Unit.Millimeters) }, - new object[]{ "-1 mm", new Value(-1, Unit.Millimeters) }, - new object[]{ "1.10mm", new Value(1.10, Unit.Millimeters) }, - new object[]{ "1.10 mm", new Value(1.10, Unit.Millimeters) }, - new object[]{ "-1.10mm", new Value(-1.10, Unit.Millimeters) }, - new object[]{ "-1.10 mm", new Value(-1.10, Unit.Millimeters) }, - new object[]{ "1in", new Value(1, Unit.Inches) }, - new object[]{ "1 in", new Value(1, Unit.Inches) }, - new object[]{ "-1in", new Value(-1, Unit.Inches) }, - new object[]{ "-1 in", new Value(-1, Unit.Inches) }, - new object[]{ "10cm", new Value(10, Unit.Centimeters) }, - new object[]{ "10 cm", new Value(10, Unit.Centimeters) }, - new object[]{ "-10cm", new Value(-10, Unit.Centimeters) }, - new object[]{ "-10 cm", new Value(-10, Unit.Centimeters) }, - new object[]{ "10pts", new Value(10, Unit.Points) }, - new object[]{ "10 pts", new Value(10, Unit.Points) }, - new object[]{ "-10pts", new Value(-10, Unit.Points) }, - new object[]{ "-10 pts", new Value(-10, Unit.Points) }, - - new object[]{ "1millimeters", new Value(1, Unit.Millimeters) }, - new object[]{ "1 millimeters", new Value(1, Unit.Millimeters) }, - new object[]{ "-1millimeters", new Value(-1, Unit.Millimeters) }, - new object[]{ "-1 millimeters", new Value(-1, Unit.Millimeters) }, - new object[]{ "1inches", new Value(1, Unit.Inches) }, - new object[]{ "1 inches", new Value(1, Unit.Inches) }, - new object[]{ "-1inches", new Value(-1, Unit.Inches) }, - new object[]{ "-1 inches", new Value(-1, Unit.Inches) }, - new object[]{ "1inch", new Value(1, Unit.Inches) }, - new object[]{ "1 inch", new Value(1, Unit.Inches) }, - new object[]{ "-1inch", new Value(-1, Unit.Inches) }, - new object[]{ "-1 inch", new Value(-1, Unit.Inches) }, - new object[]{ "10centimeters", new Value(10, Unit.Centimeters) }, - new object[]{ "10 centimeters", new Value(10, Unit.Centimeters) }, - new object[]{ "-10centimeters", new Value(-10, Unit.Centimeters) }, - new object[]{ "-10 centimeters", new Value(-10, Unit.Centimeters) }, - new object[]{ "10points", new Value(10, Unit.Points) }, - new object[]{ "10 points", new Value(10, Unit.Points) }, - new object[]{ "-10points", new Value(-10, Unit.Points) }, - new object[]{ "-10 points", new Value(-10, Unit.Points) }, - - new object[]{ "10dpi(300)", new Value(10, Unit.Pixels(300)) }, - new object[]{ "10 dpi(300)", new Value(10, Unit.Pixels(300)) }, - new object[]{ "-10dpi(300)", new Value(-10, Unit.Pixels(300)) }, - new object[]{ "-10 dpi(300)", new Value(-10, Unit.Pixels(300)) }, - }; + new object[]{ "1mm", new Value(1, Unit.Millimeters), CultureInfo.InvariantCulture }, + new object[]{ "1 mm", new Value(1, Unit.Millimeters), CultureInfo.InvariantCulture }, + new object[]{ "-1mm", new Value(-1, Unit.Millimeters), CultureInfo.InvariantCulture }, + new object[]{ "-1 mm", new Value(-1, Unit.Millimeters), CultureInfo.InvariantCulture }, + new object[]{ "1.10mm", new Value(1.10, Unit.Millimeters), CultureInfo.InvariantCulture }, + new object[]{ "1.10 mm", new Value(1.10, Unit.Millimeters), CultureInfo.InvariantCulture }, + new object[]{ "-1.10mm", new Value(-1.10, Unit.Millimeters), CultureInfo.InvariantCulture }, + new object[]{ "-1.10 mm", new Value(-1.10, Unit.Millimeters), CultureInfo.InvariantCulture }, + new object[]{ "1in", new Value(1, Unit.Inches), CultureInfo.InvariantCulture }, + new object[]{ "1 in", new Value(1, Unit.Inches), CultureInfo.InvariantCulture }, + new object[]{ "-1in", new Value(-1, Unit.Inches), CultureInfo.InvariantCulture }, + new object[]{ "-1 in", new Value(-1, Unit.Inches), CultureInfo.InvariantCulture }, + new object[]{ "10cm", new Value(10, Unit.Centimeters), CultureInfo.InvariantCulture }, + new object[]{ "10 cm", new Value(10, Unit.Centimeters), CultureInfo.InvariantCulture }, + new object[]{ "-10cm", new Value(-10, Unit.Centimeters), CultureInfo.InvariantCulture }, + new object[]{ "-10 cm", new Value(-10, Unit.Centimeters), CultureInfo.InvariantCulture }, + new object[]{ "10pts", new Value(10, Unit.Points), CultureInfo.InvariantCulture }, + new object[]{ "10 pts", new Value(10, Unit.Points), CultureInfo.InvariantCulture }, + new object[]{ "-10pts", new Value(-10, Unit.Points), CultureInfo.InvariantCulture }, + new object[]{ "-10 pts", new Value(-10, Unit.Points), CultureInfo.InvariantCulture }, + + new object[]{ "1millimeters", new Value(1, Unit.Millimeters), CultureInfo.InvariantCulture }, + new object[]{ "1 millimeters", new Value(1, Unit.Millimeters), CultureInfo.InvariantCulture }, + new object[]{ "-1millimeters", new Value(-1, Unit.Millimeters), CultureInfo.InvariantCulture }, + new object[]{ "-1 millimeters", new Value(-1, Unit.Millimeters), CultureInfo.InvariantCulture }, + new object[]{ "1inches", new Value(1, Unit.Inches), CultureInfo.InvariantCulture }, + new object[]{ "1 inches", new Value(1, Unit.Inches), CultureInfo.InvariantCulture }, + new object[]{ "-1inches", new Value(-1, Unit.Inches), CultureInfo.InvariantCulture }, + new object[]{ "-1 inches", new Value(-1, Unit.Inches), CultureInfo.InvariantCulture }, + new object[]{ "1inch", new Value(1, Unit.Inches), CultureInfo.InvariantCulture }, + new object[]{ "1 inch", new Value(1, Unit.Inches), CultureInfo.InvariantCulture }, + new object[]{ "-1inch", new Value(-1, Unit.Inches), CultureInfo.InvariantCulture }, + new object[]{ "-1 inch", new Value(-1, Unit.Inches), CultureInfo.InvariantCulture }, + new object[]{ "10centimeters", new Value(10, Unit.Centimeters), CultureInfo.InvariantCulture }, + new object[]{ "10 centimeters", new Value(10, Unit.Centimeters), CultureInfo.InvariantCulture }, + new object[]{ "-10centimeters", new Value(-10, Unit.Centimeters), CultureInfo.InvariantCulture }, + new object[]{ "-10 centimeters", new Value(-10, Unit.Centimeters), CultureInfo.InvariantCulture }, + new object[]{ "10points", new Value(10, Unit.Points), CultureInfo.InvariantCulture }, + new object[]{ "10 points", new Value(10, Unit.Points), CultureInfo.InvariantCulture }, + new object[]{ "-10points", new Value(-10, Unit.Points), CultureInfo.InvariantCulture }, + new object[]{ "-10 points", new Value(-10, Unit.Points), CultureInfo.InvariantCulture }, + + new object[]{ "10dpi(300)", new Value(10, Unit.Pixels(300)), CultureInfo.InvariantCulture }, + new object[]{ "10 dpi(300)", new Value(10, Unit.Pixels(300)), CultureInfo.InvariantCulture }, + new object[]{ "-10dpi(300)", new Value(-10, Unit.Pixels(300)), CultureInfo.InvariantCulture }, + new object[]{ "-10 dpi(300)", new Value(-10, Unit.Pixels(300)), CultureInfo.InvariantCulture }, + + new object[]{ "1.5mm", new Value(1.5, Unit.Millimeters), new CultureInfo("en-US") }, + new object[]{ "1.5 mm", new Value(1.5, Unit.Millimeters), new CultureInfo("en-US") }, + new object[]{ "-2.75cm", new Value(-2.75, Unit.Centimeters), new CultureInfo("en-US") }, + new object[]{ "1,234.56in", new Value(1234.56, Unit.Inches), new CultureInfo("en-US") }, + new object[]{ "150dpi(300)", new Value(150, Unit.Pixels(300)), new CultureInfo("en-US") }, + + new object[]{ "1,5mm", new Value(1.5, Unit.Millimeters), new CultureInfo("de-DE") }, + new object[]{ "1,5 mm", new Value(1.5, Unit.Millimeters), new CultureInfo("de-DE") }, + new object[]{ "-2,75cm", new Value(-2.75, Unit.Centimeters), new CultureInfo("de-DE") }, + new object[]{ "1.234,56in", new Value(1234.56, Unit.Inches), new CultureInfo("de-DE") }, + new object[]{ "150dpi(300,5)", new Value(150, Unit.Pixels(300.5)), new CultureInfo("de-DE") }, + + new object[]{ "1,5mm", new Value(1.5, Unit.Millimeters), new CultureInfo("fr-FR") }, + new object[]{ "1,5 mm", new Value(1.5, Unit.Millimeters), new CultureInfo("fr-FR") }, + new object[]{ "-2,75cm", new Value(-2.75, Unit.Centimeters), new CultureInfo("fr-FR") }, + new object[]{ "1 234,56in", new Value(1234.56, Unit.Inches), new CultureInfo("fr-FR") }, + new object[]{ "150dpi(300,5)", new Value(150, Unit.Pixels(300.5)), new CultureInfo("fr-FR") }, + }; + + [Theory] + [MemberData(nameof(DataForToString_WithCulture_FormatsCorrectly))] + public void ToString_WithCulture_FormatsCorrectly(Value input, CultureInfo culture, string expected) + { + // Act + var result = input.ToString(culture); + + // Assert + Assert.Equal(expected, result); } + + public static IEnumerable DataForToString_WithCulture_FormatsCorrectly + => new[] + { + new object[]{ new Value(1.5, Unit.Millimeters), CultureInfo.InvariantCulture, "1.5 mm" }, + new object[]{ new Value(1.5, Unit.Millimeters), new CultureInfo("en-US"), "1.5 mm" }, + new object[]{ new Value(1.5, Unit.Millimeters), new CultureInfo("de-DE"), "1,5 mm" }, + new object[]{ new Value(1.5, Unit.Millimeters), new CultureInfo("fr-FR"), "1,5 mm" }, + new object[]{ new Value(1234.56, Unit.Centimeters), new CultureInfo("en-US"), "1234.56 cm" }, + new object[]{ new Value(1234.56, Unit.Centimeters), new CultureInfo("de-DE"), "1234,56 cm" }, + new object[]{ new Value(1234.56, Unit.Centimeters), new CultureInfo("fr-FR"), "1234,56 cm" }, + new object[]{ new Value(-789.12, Unit.Inches), new CultureInfo("en-US"), "-789.12 in" }, + new object[]{ new Value(-789.12, Unit.Inches), new CultureInfo("de-DE"), "-789,12 in" }, + new object[]{ new Value(-789.12, Unit.Inches), new CultureInfo("fr-FR"), "-789,12 in" }, + }; } From 49c5384c0eefbe8b1421345da2ddfa86c8434371 Mon Sep 17 00:00:00 2001 From: Gerard Gunnewijk Date: Tue, 17 Jun 2025 15:29:38 +0200 Subject: [PATCH 2/3] Ensure jsonconvertors use invariant culture --- .../JsonConverters/PointJsonConverter.cs | 3 ++- .../JsonConverters/RectangleJsonConverter.cs | 3 ++- .../JsonConverters/SizeJsonConverter.cs | 3 ++- .../JsonConverters/SpacingJsonConverter.cs | 3 ++- .../JsonConverters/UnitJsonConverter.cs | 2 +- .../JsonConverters/ValueJsonConverter.cs | 2 +- src/Synercoding.Primitives/PackageDetails.props | 2 +- src/Synercoding.Primitives/Unit.cs | 10 +++++++++- 8 files changed, 20 insertions(+), 8 deletions(-) diff --git a/src/Synercoding.Primitives/JsonConverters/PointJsonConverter.cs b/src/Synercoding.Primitives/JsonConverters/PointJsonConverter.cs index f6c78bf..7fc31d2 100644 --- a/src/Synercoding.Primitives/JsonConverters/PointJsonConverter.cs +++ b/src/Synercoding.Primitives/JsonConverters/PointJsonConverter.cs @@ -1,4 +1,5 @@ using System; +using System.Globalization; using System.Text.Json; using System.Text.Json.Serialization; @@ -72,6 +73,6 @@ public override Point Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSe /// An object that specifies serialization options to use. public override void Write(Utf8JsonWriter writer, Point value, JsonSerializerOptions options) { - writer.WriteStringValue(value.ToString()); + writer.WriteStringValue(value.ToString(CultureInfo.InvariantCulture)); } } \ No newline at end of file diff --git a/src/Synercoding.Primitives/JsonConverters/RectangleJsonConverter.cs b/src/Synercoding.Primitives/JsonConverters/RectangleJsonConverter.cs index 1ea8744..a4a1b70 100644 --- a/src/Synercoding.Primitives/JsonConverters/RectangleJsonConverter.cs +++ b/src/Synercoding.Primitives/JsonConverters/RectangleJsonConverter.cs @@ -1,4 +1,5 @@ using System; +using System.Globalization; using System.Text.Json; using System.Text.Json.Serialization; @@ -80,6 +81,6 @@ public override Rectangle Read(ref Utf8JsonReader reader, Type typeToConvert, Js /// An object that specifies serialization options to use. public override void Write(Utf8JsonWriter writer, Rectangle value, JsonSerializerOptions options) { - writer.WriteStringValue(value.ToString()); + writer.WriteStringValue(value.ToString(CultureInfo.InvariantCulture)); } } diff --git a/src/Synercoding.Primitives/JsonConverters/SizeJsonConverter.cs b/src/Synercoding.Primitives/JsonConverters/SizeJsonConverter.cs index 6e39163..b9db000 100644 --- a/src/Synercoding.Primitives/JsonConverters/SizeJsonConverter.cs +++ b/src/Synercoding.Primitives/JsonConverters/SizeJsonConverter.cs @@ -1,4 +1,5 @@ using System; +using System.Globalization; using System.Text.Json; using System.Text.Json.Serialization; @@ -72,6 +73,6 @@ public override Size Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSer /// An object that specifies serialization options to use. public override void Write(Utf8JsonWriter writer, Size value, JsonSerializerOptions options) { - writer.WriteStringValue(value.ToString()); + writer.WriteStringValue(value.ToString(CultureInfo.InvariantCulture)); } } diff --git a/src/Synercoding.Primitives/JsonConverters/SpacingJsonConverter.cs b/src/Synercoding.Primitives/JsonConverters/SpacingJsonConverter.cs index aa06cd9..de5dfd9 100644 --- a/src/Synercoding.Primitives/JsonConverters/SpacingJsonConverter.cs +++ b/src/Synercoding.Primitives/JsonConverters/SpacingJsonConverter.cs @@ -1,4 +1,5 @@ using System; +using System.Globalization; using System.Text.Json; using System.Text.Json.Serialization; @@ -80,6 +81,6 @@ public override Spacing Read(ref Utf8JsonReader reader, Type typeToConvert, Json /// An object that specifies serialization options to use. public override void Write(Utf8JsonWriter writer, Spacing value, JsonSerializerOptions options) { - writer.WriteStringValue(value.ToString()); + writer.WriteStringValue(value.ToString(CultureInfo.InvariantCulture)); } } diff --git a/src/Synercoding.Primitives/JsonConverters/UnitJsonConverter.cs b/src/Synercoding.Primitives/JsonConverters/UnitJsonConverter.cs index fd73fbb..45b14b3 100644 --- a/src/Synercoding.Primitives/JsonConverters/UnitJsonConverter.cs +++ b/src/Synercoding.Primitives/JsonConverters/UnitJsonConverter.cs @@ -44,7 +44,7 @@ public override Unit Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSer /// An object that specifies serialization options to use. public override void Write(Utf8JsonWriter writer, Unit value, JsonSerializerOptions options) { - writer.WriteStringValue(value.ToString()); + writer.WriteStringValue(value.ToString(CultureInfo.InvariantCulture)); } } diff --git a/src/Synercoding.Primitives/JsonConverters/ValueJsonConverter.cs b/src/Synercoding.Primitives/JsonConverters/ValueJsonConverter.cs index 8458157..87a9a28 100644 --- a/src/Synercoding.Primitives/JsonConverters/ValueJsonConverter.cs +++ b/src/Synercoding.Primitives/JsonConverters/ValueJsonConverter.cs @@ -73,6 +73,6 @@ public override Value Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSe /// An object that specifies serialization options to use. public override void Write(Utf8JsonWriter writer, Value value, JsonSerializerOptions options) { - writer.WriteStringValue(value.ToString()); + writer.WriteStringValue(value.ToString(CultureInfo.InvariantCulture)); } } diff --git a/src/Synercoding.Primitives/PackageDetails.props b/src/Synercoding.Primitives/PackageDetails.props index 8beb102..a3db11f 100644 --- a/src/Synercoding.Primitives/PackageDetails.props +++ b/src/Synercoding.Primitives/PackageDetails.props @@ -10,7 +10,7 @@ Synercoding.Primitives Synercoding.Primitives Primitives with units attached (think mm, cm, in, px) that can be used for 2D graphics. - Limit to latest .NET version (8) and add System.Json serialization support. + Added IParsable support and added overload for ToString to be culture aware. \ No newline at end of file diff --git a/src/Synercoding.Primitives/Unit.cs b/src/Synercoding.Primitives/Unit.cs index b1a0fda..7583338 100644 --- a/src/Synercoding.Primitives/Unit.cs +++ b/src/Synercoding.Primitives/Unit.cs @@ -44,9 +44,17 @@ public bool Equals(Unit other) /// public override string ToString() + => ToString(null); + + /// + /// Returns a string representation of the unit using the specified format provider. + /// + /// The format provider to use for formatting numeric values in pixel units (DPI). Ignored for other unit types. + /// A string representation of the unit. For pixel units, returns "dpi(value)" format. For other units, returns their short form (e.g., "mm", "cm", "in", "pts"). + public string ToString(IFormatProvider? formatProvider) => Designation switch { - UnitDesignation.Pixels => $"dpi({PerInch})", + UnitDesignation.Pixels => $"dpi({PerInch.ToString(formatProvider)})", var x => x.Shortform() }; From 2b49e78b0de3eed4aadc8b38a907d18a81c1aad7 Mon Sep 17 00:00:00 2001 From: Gerard Gunnewijk Date: Tue, 17 Jun 2025 15:38:56 +0200 Subject: [PATCH 3/3] Increased workflow action versions --- .github/workflows/dotnet-core.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/dotnet-core.yml b/.github/workflows/dotnet-core.yml index 41a1d8b..a8b06ab 100644 --- a/.github/workflows/dotnet-core.yml +++ b/.github/workflows/dotnet-core.yml @@ -45,7 +45,7 @@ jobs: run: dotnet pack -v normal -c Release --no-restore --include-symbols --include-source -p:SymbolPackageFormat=snupkg -p:PackageVersion=1.0.0-pre+$GITHUB_RUN_ID src/$PROJECT_NAME/$PROJECT_NAME.*proj - name: Upload Artifact if: matrix.os == 'ubuntu-latest' - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 with: name: nupkg path: ./artifacts/pkg/Release/${{ env.PROJECT_NAME }}.*.nupkg @@ -55,7 +55,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Download Artifact - uses: actions/download-artifact@v1 + uses: actions/download-artifact@v4 with: name: nupkg - name: Push to GitHub Feed