Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement IParsable<T> #268

Merged
merged 1 commit into from
Jun 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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