diff --git a/Source/aweXpect/Results/IsParsableResult.cs b/Source/aweXpect/Results/IsParsableResult.cs
new file mode 100644
index 000000000..822603d04
--- /dev/null
+++ b/Source/aweXpect/Results/IsParsableResult.cs
@@ -0,0 +1,34 @@
+#if NET8_0_OR_GREATER
+using System;
+using aweXpect.Core;
+
+namespace aweXpect.Results;
+
+///
+/// The result for verifying that the subject is parsable into .
+///
+public class IsParsableResult(
+ ExpectationBuilder expectationBuilder,
+ IThat subject,
+ IFormatProvider? formatProvider)
+ : AndOrResult>(expectationBuilder, subject)
+ where TType : IParsable
+{
+ private readonly ExpectationBuilder _expectationBuilder = expectationBuilder;
+
+ ///
+ /// Gives access to the parsed value.
+ ///
+ public IThat Which
+ => new ThatSubject(_expectationBuilder
+ .ForWhich(d =>
+ {
+ if (TType.TryParse(d, formatProvider, out TType? result))
+ {
+ return result;
+ }
+
+ return default;
+ }, " which "));
+}
+#endif
diff --git a/Source/aweXpect/That/Strings/ThatString.IsParsableInto.cs b/Source/aweXpect/That/Strings/ThatString.IsParsableInto.cs
new file mode 100644
index 000000000..dbf70eccb
--- /dev/null
+++ b/Source/aweXpect/That/Strings/ThatString.IsParsableInto.cs
@@ -0,0 +1,129 @@
+#if NET8_0_OR_GREATER
+using System;
+using aweXpect.Core;
+using aweXpect.Core.Constraints;
+using aweXpect.Helpers;
+using aweXpect.Results;
+
+namespace aweXpect;
+
+public static partial class ThatString
+{
+ ///
+ /// Verifies that the subject is parsable into type .
+ ///
+ ///
+ /// The optional parameter provides culture-specific formatting information
+ /// in the call to .
+ ///
+ public static IsParsableResult IsParsableInto(
+ this IThat source,
+ IFormatProvider? formatProvider = null)
+ where TType : IParsable
+ => new(source.Get().ExpectationBuilder.AddConstraint((it, grammars)
+ => new IsParsableIntoConstraint(it, grammars, formatProvider)),
+ source,
+ formatProvider);
+
+ ///
+ /// Verifies that the subject is not parsable into type .
+ ///
+ ///
+ /// The optional parameter provides culture-specific formatting information
+ /// in the call to .
+ ///
+ public static AndOrResult> IsNotParsableInto(
+ this IThat source,
+ IFormatProvider? formatProvider = null)
+ where TType : IParsable
+ => new(source.Get().ExpectationBuilder.AddConstraint((it, grammars)
+ => new IsParsableIntoConstraint(it, grammars, formatProvider).Invert()),
+ source);
+
+ private sealed class IsParsableIntoConstraint : ConstraintResult.WithValue,
+ IValueConstraint
+ where TType : IParsable
+ {
+ private readonly IFormatProvider? _formatProvider;
+ private readonly string _it;
+ private string? _exceptionMessage;
+
+ public IsParsableIntoConstraint(string it,
+ ExpectationGrammars grammars,
+ IFormatProvider? formatProvider) : base(grammars)
+ {
+ _it = it;
+ _formatProvider = formatProvider;
+ FurtherProcessingStrategy = FurtherProcessingStrategy.IgnoreResult;
+ }
+
+ public ConstraintResult IsMetBy(string? actual)
+ {
+ Actual = actual;
+ if (actual is null)
+ {
+ Outcome = Outcome.Failure;
+ return this;
+ }
+
+ try
+ {
+ _ = TType.Parse(actual, _formatProvider);
+ Outcome = Outcome.Success;
+ }
+ catch (Exception ex)
+ {
+ if (string.IsNullOrEmpty(ex.Message) || ex.Message.Length < 2)
+ {
+ _exceptionMessage = "an unknown error occurred";
+ }
+ else
+ {
+ _exceptionMessage = char.ToLowerInvariant(ex.Message[0]) + ex.Message[1..^1];
+ }
+
+ Outcome = Outcome.Failure;
+ }
+
+ return this;
+ }
+
+ protected override void AppendNormalExpectation(StringBuilder stringBuilder, string? indentation = null)
+ {
+ stringBuilder.Append("is parsable into ");
+ Formatter.Format(stringBuilder, typeof(TType));
+ if (_formatProvider is not null)
+ {
+ stringBuilder.Append(" using ");
+ Formatter.Format(stringBuilder, _formatProvider);
+ }
+ }
+
+ protected override void AppendNormalResult(StringBuilder stringBuilder, string? indentation = null)
+ {
+ if (Actual is null)
+ {
+ stringBuilder.ItWasNull(_it);
+ }
+ else
+ {
+ stringBuilder.Append(_it).Append(" was not because ").Append(_exceptionMessage);
+ }
+ }
+
+ protected override void AppendNegatedExpectation(StringBuilder stringBuilder, string? indentation = null)
+ {
+ stringBuilder.Append("is not parsable into ");
+ Formatter.Format(stringBuilder, typeof(TType));
+ if (_formatProvider is not null)
+ {
+ stringBuilder.Append(" using ");
+ Formatter.Format(stringBuilder, _formatProvider);
+ }
+ }
+
+ protected override void AppendNegatedResult(StringBuilder stringBuilder, string? indentation = null)
+ => stringBuilder.Append(_it).Append(" was");
+ }
+}
+#endif
diff --git a/Tests/aweXpect.Api.Tests/Expected/aweXpect_net8.0.txt b/Tests/aweXpect.Api.Tests/Expected/aweXpect_net8.0.txt
index 90a0859a4..9dbef024a 100644
--- a/Tests/aweXpect.Api.Tests/Expected/aweXpect_net8.0.txt
+++ b/Tests/aweXpect.Api.Tests/Expected/aweXpect_net8.0.txt
@@ -1118,12 +1118,16 @@ namespace aweXpect
public static aweXpect.Results.AndOrResult> IsNotNullOrWhiteSpace(this aweXpect.Core.IThat source) { }
public static aweXpect.Results.StringEqualityTypeResult> IsNotOneOf(this aweXpect.Core.IThat source, System.Collections.Generic.IEnumerable unexpected) { }
public static aweXpect.Results.StringEqualityTypeResult> IsNotOneOf(this aweXpect.Core.IThat source, params string?[] unexpected) { }
+ public static aweXpect.Results.AndOrResult> IsNotParsableInto(this aweXpect.Core.IThat source, System.IFormatProvider? formatProvider = null)
+ where TType : System.IParsable { }
public static aweXpect.Results.AndOrResult> IsNotUpperCased(this aweXpect.Core.IThat source) { }
public static aweXpect.Results.AndOrResult> IsNull(this aweXpect.Core.IThat source) { }
public static aweXpect.Results.AndOrResult> IsNullOrEmpty(this aweXpect.Core.IThat source) { }
public static aweXpect.Results.AndOrResult> IsNullOrWhiteSpace(this aweXpect.Core.IThat source) { }
public static aweXpect.Results.StringEqualityTypeResult> IsOneOf(this aweXpect.Core.IThat source, System.Collections.Generic.IEnumerable expected) { }
public static aweXpect.Results.StringEqualityTypeResult> IsOneOf(this aweXpect.Core.IThat source, params string?[] expected) { }
+ public static aweXpect.Results.IsParsableResult IsParsableInto(this aweXpect.Core.IThat source, System.IFormatProvider? formatProvider = null)
+ where TType : System.IParsable { }
public static aweXpect.Results.AndOrResult> IsUpperCased(this aweXpect.Core.IThat source) { }
public static aweXpect.Results.StringEqualityTypeResult> StartsWith(this aweXpect.Core.IThat source, string expected) { }
}
@@ -1246,6 +1250,12 @@ namespace aweXpect.Results
aweXpect.Results.EventTriggerResult WithParameter(string expression, int? position, System.Func predicate);
}
}
+ public class IsParsableResult : aweXpect.Results.AndOrResult>
+ where TType : System.IParsable
+ {
+ public IsParsableResult(aweXpect.Core.ExpectationBuilder expectationBuilder, aweXpect.Core.IThat subject, System.IFormatProvider? formatProvider) { }
+ public aweXpect.Core.IThat Which { get; }
+ }
public class ObjectCollectionBeContainedInResult : aweXpect.Results.ObjectCollectionMatchResult
{
public ObjectCollectionBeContainedInResult(aweXpect.Core.ExpectationBuilder expectationBuilder, TThat returnValue, aweXpect.Options.ObjectEqualityOptions options, aweXpect.Options.CollectionMatchOptions collectionMatchOptions) { }
diff --git a/Tests/aweXpect.Tests/Strings/ThatString.IsNotParsableInto.Tests.cs b/Tests/aweXpect.Tests/Strings/ThatString.IsNotParsableInto.Tests.cs
new file mode 100644
index 000000000..77aa2115b
--- /dev/null
+++ b/Tests/aweXpect.Tests/Strings/ThatString.IsNotParsableInto.Tests.cs
@@ -0,0 +1,70 @@
+#if NET8_0_OR_GREATER
+using System.Globalization;
+
+namespace aweXpect.Tests;
+
+public sealed partial class ThatString
+{
+ public class IsNotParsableInto
+ {
+ public sealed class Tests
+ {
+ [Fact]
+ public async Task WhenNull_ShouldSucceed()
+ {
+ string? subject = null;
+
+ async Task Act()
+ => await That(subject).IsNotParsableInto();
+
+ await That(Act).DoesNotThrow();
+ }
+
+ [Fact]
+ public async Task WhenStringIsNotParsable_ShouldSucceed()
+ {
+ string subject = "abc";
+
+ async Task Act()
+ => await That(subject).IsNotParsableInto();
+
+ await That(Act).DoesNotThrow();
+ }
+
+ [Fact]
+ public async Task WhenStringIsParsable_ShouldFail()
+ {
+ string subject = "42";
+
+ async Task Act()
+ => await That(subject).IsNotParsableInto();
+
+ await That(Act).Throws()
+ .WithMessage("""
+ Expected that subject
+ is not parsable into int,
+ but it was
+ """);
+ }
+
+ [Theory]
+ [InlineData("12,34", "de-AT")]
+ [InlineData("12.34", "en-US")]
+ public async Task WithFormatProvider_ShouldBeUsed(string subject, string cultureName)
+ {
+ IFormatProvider formatProvider = new CultureInfo(cultureName);
+
+ async Task Act()
+ => await That(subject).IsNotParsableInto(formatProvider);
+
+ await That(Act).Throws()
+ .WithMessage($"""
+ Expected that subject
+ is not parsable into decimal using {cultureName},
+ but it was
+ """);
+ }
+ }
+ }
+}
+#endif
diff --git a/Tests/aweXpect.Tests/Strings/ThatString.IsParsableInto.Tests.cs b/Tests/aweXpect.Tests/Strings/ThatString.IsParsableInto.Tests.cs
new file mode 100644
index 000000000..7fddc0c55
--- /dev/null
+++ b/Tests/aweXpect.Tests/Strings/ThatString.IsParsableInto.Tests.cs
@@ -0,0 +1,147 @@
+#if NET8_0_OR_GREATER
+using System.Globalization;
+
+namespace aweXpect.Tests;
+
+public sealed partial class ThatString
+{
+ public class IsParsableInto
+ {
+ public sealed class Tests
+ {
+ [Fact]
+ public async Task WhenNull_ShouldFail()
+ {
+ string? subject = null;
+
+ async Task Act()
+ => await That(subject).IsParsableInto().Because("null should fail");
+
+ await That(Act).Throws()
+ .WithMessage("""
+ Expected that subject
+ is parsable into int, because null should fail,
+ but it was
+ """);
+ }
+
+ [Fact]
+ public async Task WhenStringIsNotParsable_ShouldFail()
+ {
+ string subject = "abc";
+
+ async Task Act()
+ => await That(subject).IsParsableInto();
+
+ await That(Act).Throws()
+ .WithMessage("""
+ Expected that subject
+ is parsable into int,
+ but it was not because the input string 'abc' was not in a correct format
+ """);
+ }
+
+ [Fact]
+ public async Task WhenStringIsParsable_ShouldSucceed()
+ {
+ string subject = "42";
+
+ async Task Act()
+ => await That(subject).IsParsableInto();
+
+ await That(Act).DoesNotThrow();
+ }
+
+ [Theory]
+ [InlineData("-12.34", "de-AT")]
+ [InlineData("-12,34", "en-US")]
+ public async Task WithFormatProvider_WhenFormatDoesNotMatch_ShouldFail(string subject, string cultureName)
+ {
+ IFormatProvider formatProvider = new CultureInfo(cultureName);
+
+ async Task Act()
+ => await That(subject).IsParsableInto(formatProvider);
+
+ await That(Act).Throws()
+ .WithMessage($"""
+ Expected that subject
+ is parsable into uint using {cultureName},
+ but it was not because the input string '{subject}' was not in a correct format
+ """);
+ }
+
+ [Theory]
+ [InlineData("12,34", "de-AT")]
+ [InlineData("12.34", "en-US")]
+ public async Task WithFormatProvider_WhenFormatMatches_ShouldSucceed(string subject, string cultureName)
+ {
+ IFormatProvider formatProvider = new CultureInfo(cultureName);
+
+ async Task Act()
+ => await That(subject).IsParsableInto(formatProvider);
+
+ await That(Act).DoesNotThrow();
+ }
+ }
+
+ public sealed class WhichTests
+ {
+ [Fact]
+ public async Task WhenNull_ShouldFail()
+ {
+ string? subject = null;
+
+ async Task Act()
+ => await That(subject).IsParsableInto().Which.IsGreaterThan(2).And.IsLessThan(3);
+
+ await That(Act).Throws()
+ .WithMessage("""
+ Expected that subject
+ is parsable into int which is greater than 2 and is less than 3,
+ but it was
+ """);
+ }
+
+ [Fact]
+ public async Task WhenStringIsNotParsable_ShouldFail()
+ {
+ string subject = "abc";
+
+ async Task Act()
+ => await That(subject).IsParsableInto().Which.IsLessThan(10.Seconds());
+
+ await That(Act).Throws()
+ .WithMessage("""
+ Expected that subject
+ is parsable into TimeSpan which is less than 0:10,
+ but it was not because string 'abc' was not recognized as a valid TimeSpan
+ """);
+ }
+
+ [Fact]
+ public async Task WhenStringIsParsable_ShouldSucceed()
+ {
+ string subject = "42";
+
+ async Task Act()
+ => await That(subject).IsParsableInto().Which.IsBetween(41).And(43);
+
+ await That(Act).DoesNotThrow();
+ }
+
+ [Theory]
+ [InlineData("12,34", "de-AT")]
+ [InlineData("12.34", "en-US")]
+ public async Task WithFormatProvider_ShouldBeUsed(string subject, string cultureName)
+ {
+ IFormatProvider formatProvider = new CultureInfo(cultureName);
+
+ async Task Act()
+ => await That(subject).IsParsableInto(formatProvider).Which.IsEqualTo(12.34M);
+
+ await That(Act).DoesNotThrow();
+ }
+ }
+ }
+}
+#endif