Skip to content

Commit

Permalink
Implement IParsable<T> (#268)
Browse files Browse the repository at this point in the history
`IParsable<T>` is useful for generic parsing. This PR adds support for it on `ByteSize`, `Duration`, `Etag`, `Ksuid`, `Money`, and `SwiftCode`.

Consequently:
1. Creating `Etag` from string has been marked as obsolete in favour of parsing.
2. Parsing of `Money` requires more explict currency specification where the symbol is used by multiple currencies, such as `$` which is used by 9 currencies. `Money.Parse(...)` will throw but `Money.TryParse(...)` will return false.
  • Loading branch information
mburumaxwell authored Jun 5, 2024
1 parent b7dd81b commit 2e8832f
Show file tree
Hide file tree
Showing 12 changed files with 429 additions and 252 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ public override Etag Deserialize(BsonDeserializationContext context, BsonDeseria
var bsonType = context.Reader.CurrentBsonType;
return bsonType switch
{
BsonType.String => new Etag(_stringSerializer.Deserialize(context)),
BsonType.String => Etag.Parse(_stringSerializer.Deserialize(context)),
BsonType.Int64 => new Etag((ulong)_int64Serializer.Deserialize(context)),
BsonType.Binary => new Etag(_byteArraySerializer.Deserialize(context)),
_ => throw CreateCannotDeserializeFromBsonTypeException(bsonType),
Expand Down
88 changes: 60 additions & 28 deletions src/Tingle.Extensions.Primitives/ByteSize.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Text.Json.Serialization;
using Tingle.Extensions.Primitives.Converters;
Expand All @@ -11,7 +12,7 @@ namespace Tingle.Extensions.Primitives;
/// <param name="bytes">Number of bytes.</param>
[JsonConverter(typeof(ByteSizeJsonConverter))]
[TypeConverter(typeof(ByteSizeTypeConverter))]
public readonly struct ByteSize(long bytes) : IEquatable<ByteSize>, IComparable<ByteSize>, IConvertible, IFormattable
public readonly struct ByteSize(long bytes) : IEquatable<ByteSize>, IComparable<ByteSize>, IConvertible, IFormattable, IParsable<ByteSize>
{
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
public static readonly ByteSize MinValue = FromBytes(long.MinValue);
Expand Down Expand Up @@ -186,27 +187,27 @@ internal double LargestWholeNumberDecimalValue
public string ToString(string? format) => ToString(format, CultureInfo.CurrentCulture);

/// <inheritdoc/>
public string ToString(string? format, IFormatProvider? formatProvider) => ToString(format, formatProvider, useBinaryByte: false);
public string ToString(string? format, IFormatProvider? provider) => ToString(format, provider, useBinaryByte: false);

/// <summary>Formats the value of the current instance in binary format.</summary>
/// <returns>The value of the current instance in the specified format.</returns>
public string ToBinaryString() => ToString("0.##", CultureInfo.CurrentCulture, useBinaryByte: true);

/// <summary>Formats the value of the current instance in binary format.</summary>
/// <param name="formatProvider">
/// <param name="provider">
/// The provider to use to format the value. -or- A null reference (Nothing in Visual
/// Basic) to obtain the numeric format information from the current locale setting
/// of the operating system.
/// </param>
/// <returns>The value of the current instance in the specified format.</returns>
public string ToBinaryString(IFormatProvider? formatProvider) => ToString("0.##", formatProvider, useBinaryByte: true);
public string ToBinaryString(IFormatProvider? provider) => ToString("0.##", provider, useBinaryByte: true);

/// <summary>Formats the value of the current instance using the specified format.</summary>
/// <param name="format">
/// The format to use. -or- A null reference (Nothing in Visual Basic) to use the
/// default format defined for the type of the System.IFormattable implementation.
/// </param>
/// <param name="formatProvider">
/// <param name="provider">
/// The provider to use to format the value. -or- A null reference (Nothing in Visual
/// Basic) to obtain the numeric format information from the current locale setting
/// of the operating system.
Expand All @@ -215,16 +216,16 @@ internal double LargestWholeNumberDecimalValue
/// Whether to use binary format
/// </param>
/// <returns>The value of the current instance in the specified format.</returns>
public string ToString(string? format, IFormatProvider? formatProvider, bool useBinaryByte)
public string ToString(string? format, IFormatProvider? provider, bool useBinaryByte)
{
format ??= "0.##";
formatProvider ??= CultureInfo.CurrentCulture;
provider ??= CultureInfo.CurrentCulture;

if (!format.Contains('#') && !format.Contains('0'))
format = "0.## " + format;

bool has(string s) => format.Contains(s, StringComparison.CurrentCultureIgnoreCase);
string output(double n) => n.ToString(format, formatProvider);
string output(double n) => n.ToString(format, provider);

// Binary
if (has("PiB")) return output(PebiBytes);
Expand All @@ -245,8 +246,8 @@ public string ToString(string? format, IFormatProvider? formatProvider, bool use
return output(Bytes);

return useBinaryByte
? string.Format("{0} {1}", LargestWholeNumberBinaryValue.ToString(format, formatProvider), LargestWholeNumberBinarySymbol)
: string.Format("{0} {1}", LargestWholeNumberDecimalValue.ToString(format, formatProvider), LargestWholeNumberDecimalSymbol);
? string.Format("{0} {1}", LargestWholeNumberBinaryValue.ToString(format, provider), LargestWholeNumberBinarySymbol)
: string.Format("{0} {1}", LargestWholeNumberDecimalValue.ToString(format, provider), LargestWholeNumberDecimalSymbol);
}

#endregion
Expand Down Expand Up @@ -339,27 +340,27 @@ public string ToString(string? format, IFormatProvider? formatProvider, bool use

/// <summary>Converts a <see cref="string"/> into a <see cref="ByteSize"/> in a specified culture-specific format.</summary>
/// <param name="s">A string containing the value to convert.</param>
/// <param name="formatProvider">An object that supplies culture-specific formatting information about <paramref name="s"/>.</param>
/// <param name="provider">An object that supplies culture-specific formatting information about <paramref name="s"/>.</param>
/// <returns>A <see cref="ByteSize"/> equivalent to the value specified in <paramref name="s"/>.</returns>
/// <exception cref="ArgumentNullException"><paramref name="s"/> is null.</exception>
/// <exception cref="FormatException"><paramref name="s"/> is not in a correct format.</exception>
public static ByteSize Parse(string s, IFormatProvider formatProvider) => Parse(s, NumberStyles.Float | NumberStyles.AllowThousands, formatProvider);
public static ByteSize Parse(string s, IFormatProvider? provider) => Parse(s, NumberStyles.Float | NumberStyles.AllowThousands, provider);

/// <summary>Converts a <see cref="string"/> into a <see cref="ByteSize"/> in a specified style and culture-specific format.</summary>
/// <param name="s">A string containing the value to convert.</param>
/// <param name="style">
/// A bitwise combination of <see cref="NumberStyles"/> values that indicates the permitted format of <paramref name="s"/>.
/// A typical value to specify is <see cref="NumberStyles.Float"/> combined with <see cref="NumberStyles.AllowThousands"/>.
/// </param>
/// <param name="formatProvider">An object that supplies culture-specific formatting information about <paramref name="s"/>.</param>
/// <param name="provider">An object that supplies culture-specific formatting information about <paramref name="s"/>.</param>
/// <returns>A <see cref="ByteSize"/> equivalent to the value specified in <paramref name="s"/>.</returns>
/// <exception cref="ArgumentNullException"><paramref name="s"/> is null.</exception>
/// <exception cref="ArgumentException">
/// <paramref name="style"/> is not a <see cref="NumberStyles"/> value.
/// -or- style includes the <see cref="NumberStyles.AllowHexSpecifier"/> value.
/// </exception>
/// <exception cref="FormatException"><paramref name="s"/> is not in a correct format.</exception>
public static ByteSize Parse(string s, NumberStyles style, IFormatProvider formatProvider)
public static ByteSize Parse(string s, NumberStyles style, IFormatProvider? provider)
{
if (string.IsNullOrWhiteSpace(s))
{
Expand All @@ -371,7 +372,7 @@ public static ByteSize Parse(string s, NumberStyles style, IFormatProvider forma

var found = false;

var numberFormatInfo = NumberFormatInfo.GetInstance(formatProvider);
var numberFormatInfo = NumberFormatInfo.GetInstance(provider);
var decimalSeparator = Convert.ToChar(numberFormatInfo.NumberDecimalSeparator);
var groupSeparator = Convert.ToChar(numberFormatInfo.NumberGroupSeparator);

Expand All @@ -397,7 +398,7 @@ public static ByteSize Parse(string s, NumberStyles style, IFormatProvider forma
string sizePart = s[lastNumber..].Trim();

// Get the numeric part
if (!double.TryParse(numberPart, style, formatProvider, out var number))
if (!double.TryParse(numberPart, style, provider, out var number))
throw new FormatException($"No number found in value '{s}'.");

// Get the magnitude part
Expand Down Expand Up @@ -441,18 +442,49 @@ public static ByteSize Parse(string s, NumberStyles style, IFormatProvider forma
/// any value originally supplied in result will be overwritten.
/// </param>
/// <returns><see langword="true"/> if <paramref name="s"/> was converted successfully; otherwise, <see langword="false"/>.</returns>
public static bool TryParse(string s, out ByteSize result)
public static bool TryParse([NotNullWhen(true)] string? s, out ByteSize result)
{
result = default;
if (s is null) return false;

try
{
result = Parse(s);
return true;
}
catch
catch { }
return false;
}

/// <summary>
/// Converts the <see cref="string"/> into a <see cref="ByteSize"/> in a specified style and culture-specific format.
/// A return value indicates whether the conversion succeeded or failed.
/// </summary>
/// <param name="s">A string containing the value to convert.</param>
/// <param name="provider">
/// An <see cref="IFormatProvider"/> that supplies culture-specific formatting information about <paramref name="s"/>.
/// </param>
/// <param name="result">
/// When this method returns, contains the <see cref="ByteSize"/> equivalent of the <paramref name="s"/> parameter,
/// if the conversion succeeded, or default if the conversion failed.
/// The conversion fails if the <paramref name="s"/> parameter is <see langword="null"/> or <see cref="string.Empty"/>,
/// is not in a valid format, or represents a value less than <see cref="MinValue"/>
/// or greater than <see cref="MaxValue"/>. This parameter is passed uninitialized;
/// any value originally supplied in result will be overwritten.
/// </param>
/// <returns><see langword="true"/> if <paramref name="s"/> was converted successfully; otherwise, <see langword="false"/>.</returns>
public static bool TryParse([NotNullWhen(true)] string? s, IFormatProvider? provider, out ByteSize result)
{
result = default;
if (s is null) return false;

try
{
result = new ByteSize();
return false;
result = Parse(s, provider);
return true;
}
catch { }
return false;
}

/// <summary>
Expand All @@ -464,7 +496,7 @@ public static bool TryParse(string s, out ByteSize result)
/// A bitwise combination of <see cref="NumberStyles"/> values that indicates the permitted format of <paramref name="s"/>.
/// A typical value to specify is <see cref="NumberStyles.Float"/> combined with <see cref="NumberStyles.AllowThousands"/>.
/// </param>
/// <param name="formatProvider">
/// <param name="provider">
/// An <see cref="IFormatProvider"/> that supplies culture-specific formatting information about <paramref name="s"/>.
/// </param>
/// <param name="result">
Expand All @@ -476,18 +508,18 @@ public static bool TryParse(string s, out ByteSize result)
/// any value originally supplied in result will be overwritten.
/// </param>
/// <returns><see langword="true"/> if <paramref name="s"/> was converted successfully; otherwise, <see langword="false"/>.</returns>
public static bool TryParse(string s, NumberStyles style, IFormatProvider formatProvider, out ByteSize result)
public static bool TryParse([NotNullWhen(true)] string? s, NumberStyles style, IFormatProvider? provider, out ByteSize result)
{
result = default;
if (s is null) return false;

try
{
result = Parse(s, style, formatProvider);
result = Parse(s, style, provider);
return true;
}
catch
{
result = new ByteSize();
return false;
}
catch { }
return false;
}

#endregion
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ public override Etag Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSer
}

var str = reader.GetString();
return new Etag(str!);
return Etag.Parse(str!);
}

/// <inheritdoc/>
Expand Down
65 changes: 43 additions & 22 deletions src/Tingle.Extensions.Primitives/Duration.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using System.ComponentModel;
using System;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Text;
using System.Text.Json.Serialization;
Expand All @@ -16,7 +18,7 @@ namespace Tingle.Extensions.Primitives;
/// </remarks>
[JsonConverter(typeof(DurationJsonConverter))]
[TypeConverter(typeof(DurationTypeConverter))]
public readonly struct Duration : IEquatable<Duration>, IConvertible
public readonly struct Duration : IEquatable<Duration>, IConvertible, IParsable<Duration>
{
/// <summary>Represents the zero <see cref="Duration"/> value.</summary>
public static readonly Duration Zero = new(0, 0, 0, 0);
Expand Down Expand Up @@ -174,39 +176,58 @@ void AppendComponent(uint number, char symbol)
#region Parsing

/// <summary>Converts a <see cref="string"/> in ISO8601 format into a <see cref="Duration"/>.</summary>
/// <param name="value">A string containing the value to convert.</param>
/// <returns>A <see cref="Duration"/> equivalent to the value specified in <paramref name="value"/>.</returns>
/// <exception cref="ArgumentNullException"><paramref name="value"/> is null.</exception>
/// <exception cref="FormatException"><paramref name="value"/> is not in a correct format.</exception>
public static Duration Parse(string value)
/// <param name="s">A string containing the value to convert.</param>
/// <returns>A <see cref="Duration"/> equivalent to the value specified in <paramref name="s"/>.</returns>
/// <exception cref="ArgumentNullException"><paramref name="s"/> is null.</exception>
/// <exception cref="FormatException"><paramref name="s"/> is not in a correct format.</exception>
public static Duration Parse(string s) => Parse(s, null);

/// <summary>Converts a <see cref="string"/> in ISO8601 format into a <see cref="Duration"/>.</summary>
/// <param name="s">A string containing the value to convert.</param>
/// <param name="provider">An object that supplies culture-specific formatting information about <paramref name="s"/>.</param>
/// <returns>A <see cref="Duration"/> equivalent to the value specified in <paramref name="s"/>.</returns>
/// <exception cref="ArgumentNullException"><paramref name="s"/> is null.</exception>
/// <exception cref="FormatException"><paramref name="s"/> is not in a correct format.</exception>
public static Duration Parse(string s, IFormatProvider? provider)
{
if (string.IsNullOrWhiteSpace(value))
if (string.IsNullOrWhiteSpace(s))
{
throw new ArgumentException($"'{nameof(value)}' cannot be null or whitespace.", nameof(value));
throw new ArgumentException($"'{nameof(s)}' cannot be null or whitespace.", nameof(s));
}

if (TryParse(value, out var duration))
return duration;

throw new FormatException($"'{value}' is not a valid Duration representation.");
if (TryParse(s, out var duration)) return duration;
throw new FormatException($"'{s}' is not a valid Duration representation.");
}

/// <summary>Converts a <see cref="string"/> in ISO8601 format into a <see cref="Duration"/>.</summary>
/// <param name="value">A string containing the value to convert.</param>
/// <param name="s">A string containing the value to convert.</param>
/// <param name="result">
/// When this method returns, contains the value associated parsed,
/// if successful; otherwise, <see langword="null"/> is returned.
/// This parameter is passed uninitialized.
/// </param>
/// <returns>
/// <see langword="true"/> if <paramref name="s"/> could be parsed; otherwise, false.
/// </returns>
public static bool TryParse([NotNullWhen(true)] string? s, out Duration result) => TryParse(s, null, out result);

/// <summary>Converts a <see cref="string"/> in ISO8601 format into a <see cref="Duration"/>.</summary>
/// <param name="s">A string containing the value to convert.</param>
/// <param name="provider">An object that supplies culture-specific formatting information about <paramref name="s"/>.</param>
/// <param name="result">
/// When this method returns, contains the value associated parsed,
/// if successful; otherwise, <see langword="null"/> is returned.
/// This parameter is passed uninitialized.
/// </param>
/// <returns>
/// <see langword="true"/> if <paramref name="value"/> could be parsed; otherwise, false.
/// <see langword="true"/> if <paramref name="s"/> could be parsed; otherwise, false.
/// </returns>
public static bool TryParse(string value, out Duration result)
public static bool TryParse([NotNullWhen(true)] string? s, IFormatProvider? provider, out Duration result)
{
result = default;
if (value == null) return false;
if (value.Length < 3) return false;
if (value[0] != DurationChars.Prefix) return false;
if (s == null) return false;
if (s.Length < 3) return false;
if (s[0] != DurationChars.Prefix) return false;

uint years = 0, months = 0, weeks = 0, days = 0, hours = 0, minutes = 0, seconds = 0;

Expand All @@ -215,9 +236,9 @@ public static bool TryParse(string value, out Duration result)
int numberStart = -1;
var isTimeSpecified = false;

while (position < value.Length)
while (position < s.Length)
{
char c = value[position];
char c = s[position];
if (c == DurationChars.Time)
{
isTimeSpecified = true;
Expand All @@ -228,7 +249,7 @@ public static bool TryParse(string value, out Duration result)
if (numberStart < 0 || numberStart >= position)
return false; // No number preceding letter

var numberString = value[numberStart..position];
var numberString = s[numberStart..position];
if (!uint.TryParse(numberString, out uint n))
return false; // Not a valid number

Expand Down
Loading

0 comments on commit 2e8832f

Please sign in to comment.