diff --git a/Code/Light.GuardClauses.AllProjects.sln.DotSettings b/Code/Light.GuardClauses.AllProjects.sln.DotSettings
index 2c4519b4..ac545e29 100644
--- a/Code/Light.GuardClauses.AllProjects.sln.DotSettings
+++ b/Code/Light.GuardClauses.AllProjects.sln.DotSettings
@@ -2,16 +2,39 @@
True
None
False
- HINT
+ HINT
+ SUGGESTION
+ HINT
+ SUGGESTION
+ SUGGESTION
+ HINT
+ WARNING
+ WARNING
+ DO_NOT_SHOW
+ DO_NOT_SHOW
HINT
+ WARNING
+ DO_NOT_SHOW
HINT
WARNING
+ True
<?xml version="1.0" encoding="utf-16"?><Profile name="Kenny's Kleanup"><CSReorderTypeMembers>True</CSReorderTypeMembers><CSUpdateFileHeader>True</CSUpdateFileHeader><CSEnforceVarKeywordUsageSettings>True</CSEnforceVarKeywordUsageSettings><CSMakeFieldReadonly>True</CSMakeFieldReadonly><CSArrangeQualifiers>True</CSArrangeQualifiers><CSOptimizeUsings><OptimizeUsings>True</OptimizeUsings><EmbraceInRegion>False</EmbraceInRegion><RegionName></RegionName></CSOptimizeUsings><CSShortenReferences>True</CSShortenReferences><CSReformatCode>True</CSReformatCode><CSharpFormatDocComments>True</CSharpFormatDocComments><CppReformatCode>True</CppReformatCode><XMLReformatCode>True</XMLReformatCode><CssReformatCode>True</CssReformatCode><CSCodeStyleAttributes ArrangeTypeAccessModifier="True" ArrangeTypeMemberAccessModifier="True" SortModifiers="True" RemoveRedundantParentheses="False" AddMissingParentheses="False" ArrangeBraces="False" ArrangeAttributes="False" ArrangeArgumentsStyle="False" ArrangeCodeBodyStyle="False" ArrangeVarStyle="False" /><CSMakeAutoPropertyGetOnly>True</CSMakeAutoPropertyGetOnly><RemoveCodeRedundancies>True</RemoveCodeRedundancies><HtmlReformatCode>True</HtmlReformatCode><JsInsertSemicolon>True</JsInsertSemicolon><CorrectVariableKindsDescriptor>True</CorrectVariableKindsDescriptor><JsReformatCode>True</JsReformatCode><JsFormatDocComments>True</JsFormatDocComments><FormatAttributeQuoteDescriptor>True</FormatAttributeQuoteDescriptor></Profile>
- C# Cleanup
- Kenny's Kleanup
+ Built-in: Full Cleanup
+ Built-in: Full Cleanup
+ False
+ Required
+ Required
+ Required
+ Required
+ ExpressionBody
+ ExpressionBody
+ ExpressionBody
+ True
+ False
True
True
+ True
True
@@ -22,23 +45,69 @@
True
True
True
+ True
TOGETHER_SAME_LINE
+ True
+ True
+ True
+ NO_INDENT
True
True
+ True
+ True
+ False
True
+ True
+ False
+ True
+ 90
+ 1
+ 10000
+ COMPACT
True
+ True
True
True
NEVER
- NEVER
- NEVER
- NEVER
+
+
+
+ NEVER
+ False
+ ALWAYS
+ IF_OWNER_IS_SINGLE_LINE
+ NEVER
DO_NOT_CHANGE
+ True
+ True
True
- 500
- False
+ False
+ True
+ True
+ CHOP_IF_LONG
+ CHOP_IF_LONG
+ False
+ True
+ True
+ True
+ True
+ True
+ True
+ False
+ CHOP_IF_LONG
+ CHOP_IF_LONG
+ CHOP_IF_LONG
+
+
+ CHOP_IF_LONG
+ CHOP_ALWAYS
+ CHOP_IF_LONG
RemoveIndent
RemoveIndent
+ False
+ ByFirstAttr
+ 150
+ False
False
False
False
@@ -192,16 +261,21 @@
</TypePattern>
</Patterns>
False
+ False
False
True
Replace
Replace
False
+ True
True
False
False
True
False
+ False
+ True
+ False
<Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
<Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
<Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" />
@@ -256,12 +330,4 @@
True
True
True
- True
- True
- True
- True
- True
- True
- True
- True
- True
\ No newline at end of file
+ True
\ No newline at end of file
diff --git a/Code/Light.GuardClauses.Performance/CollectionAssertions/SpanMustHaveLengthBenchmark.cs b/Code/Light.GuardClauses.Performance/CollectionAssertions/SpanMustHaveLengthBenchmark.cs
index 947efbf4..03988128 100644
--- a/Code/Light.GuardClauses.Performance/CollectionAssertions/SpanMustHaveLengthBenchmark.cs
+++ b/Code/Light.GuardClauses.Performance/CollectionAssertions/SpanMustHaveLengthBenchmark.cs
@@ -2,6 +2,7 @@
using System.Runtime.CompilerServices;
using System.Text;
using BenchmarkDotNet.Attributes;
+using Light.GuardClauses.ExceptionFactory;
using Light.GuardClauses.Exceptions;
namespace Light.GuardClauses.Performance.CollectionAssertions
@@ -47,7 +48,7 @@ public static class SpanMustHaveLengthExtensions
public static Span MustHaveLengthCopyByValue(this Span parameter, int length, string parameterName = null, string message = null)
{
if (parameter.Length != length)
- Throw.InvalidSpanLength(parameter, length, parameterName, message);
+ Throw.InvalidSpanLength((ReadOnlySpan) parameter, length, parameterName, message);
return parameter;
}
@@ -55,7 +56,7 @@ public static Span MustHaveLengthCopyByValue(this Span parameter, int l
public static Span MustHaveLengthInParameter(in this Span parameter, int length, string parameterName = null, string message = null)
{
if (parameter.Length != length)
- Throw.InvalidSpanLength(parameter, length, parameterName, message);
+ Throw.InvalidSpanLength((ReadOnlySpan) parameter, length, parameterName, message);
return parameter;
}
@@ -63,7 +64,7 @@ public static Span MustHaveLengthInParameter(in this Span parameter, in
public static ref Span MustHaveLengthInOut(ref this Span parameter, int length, string parameterName = null, string message = null)
{
if (parameter.Length != length)
- Throw.InvalidSpanLength(parameter, length, parameterName, message);
+ Throw.InvalidSpanLength((ReadOnlySpan) parameter, length, parameterName, message);
return ref parameter;
}
}
diff --git a/Code/Light.GuardClauses.Performance/CommonAssertions/InvalidArgumentBenchmark.cs b/Code/Light.GuardClauses.Performance/CommonAssertions/InvalidArgumentBenchmark.cs
index dbc6241c..3e922c34 100644
--- a/Code/Light.GuardClauses.Performance/CommonAssertions/InvalidArgumentBenchmark.cs
+++ b/Code/Light.GuardClauses.Performance/CommonAssertions/InvalidArgumentBenchmark.cs
@@ -1,5 +1,6 @@
using System;
using BenchmarkDotNet.Attributes;
+using Light.GuardClauses.ExceptionFactory;
namespace Light.GuardClauses.Performance.CommonAssertions
{
@@ -34,7 +35,7 @@ public bool LightGuardClausesCustomException()
public bool LightGuardClausesCustomExceptionManualInlining()
{
if (Condition)
- Exceptions.Throw.CustomException(() => new ArgumentException(Message, ParameterName));
+ Throw.CustomException(() => new ArgumentException(Message, ParameterName));
return Condition;
}
diff --git a/Code/Light.GuardClauses.Performance/CommonAssertions/MustHaveValueBenchmark.cs b/Code/Light.GuardClauses.Performance/CommonAssertions/MustHaveValueBenchmark.cs
index e42ec29e..1458a092 100644
--- a/Code/Light.GuardClauses.Performance/CommonAssertions/MustHaveValueBenchmark.cs
+++ b/Code/Light.GuardClauses.Performance/CommonAssertions/MustHaveValueBenchmark.cs
@@ -1,6 +1,7 @@
using System;
using System.Runtime.CompilerServices;
using BenchmarkDotNet.Attributes;
+using Light.GuardClauses.ExceptionFactory;
using Light.GuardClauses.Exceptions;
namespace Light.GuardClauses.Performance.CommonAssertions
diff --git a/Code/Light.GuardClauses.Performance/CommonAssertions/MustNotBeNullReferenceBenchmark.cs b/Code/Light.GuardClauses.Performance/CommonAssertions/MustNotBeNullReferenceBenchmark.cs
index e6c2a8a6..ea6aa9ed 100644
--- a/Code/Light.GuardClauses.Performance/CommonAssertions/MustNotBeNullReferenceBenchmark.cs
+++ b/Code/Light.GuardClauses.Performance/CommonAssertions/MustNotBeNullReferenceBenchmark.cs
@@ -1,7 +1,7 @@
using System;
using System.Runtime.CompilerServices;
using BenchmarkDotNet.Attributes;
-using Light.GuardClauses.Exceptions;
+using Light.GuardClauses.ExceptionFactory;
namespace Light.GuardClauses.Performance.CommonAssertions
{
@@ -56,7 +56,7 @@ public static T MustNotBeNullReferenceV1(this T parameter, string parameterNa
return parameter;
Throw.ArgumentNull(parameterName, message);
- return default(T);
+ return default;
}
return parameter;
@@ -71,7 +71,7 @@ public static T MustNotBeNullReferenceV2(this T parameter, string parameterNa
return parameter;
Throw.ArgumentNull(parameterName, message);
- return default(T);
+ return default;
}
}
}
\ No newline at end of file
diff --git a/Code/Light.GuardClauses.Performance/ComparableAssertions/MustBeApproximatelyBenchmark.cs b/Code/Light.GuardClauses.Performance/ComparableAssertions/MustBeApproximatelyBenchmark.cs
new file mode 100644
index 00000000..57364eba
--- /dev/null
+++ b/Code/Light.GuardClauses.Performance/ComparableAssertions/MustBeApproximatelyBenchmark.cs
@@ -0,0 +1,25 @@
+using System;
+using BenchmarkDotNet.Attributes;
+
+namespace Light.GuardClauses.Performance.ComparableAssertions;
+
+[MemoryDiagnoser]
+// ReSharper disable once ClassCanBeSealed.Global -- Benchmark.NET derives from this class with dynamically created code
+public class MustBeApproximatelyBenchmark
+{
+ [Benchmark]
+ public double MustBeApproximately() => 5.100001.MustBeApproximately(5.100000, 0.0001);
+
+ [Benchmark(Baseline = true)]
+ public double Imperative() => Imperative(5.100001, 5.100000, 0.0001);
+
+ private static double Imperative(double parameter, double other, double tolerance)
+ {
+ if (Math.Abs(parameter - other) > tolerance)
+ {
+ throw new ArgumentOutOfRangeException(nameof(parameter), $"Value is not approximately {other}");
+ }
+
+ return parameter;
+ }
+}
diff --git a/Code/Light.GuardClauses.Performance/ComparableAssertions/MustNotBeApproximatelyBenchmark.cs b/Code/Light.GuardClauses.Performance/ComparableAssertions/MustNotBeApproximatelyBenchmark.cs
new file mode 100644
index 00000000..a65d4b84
--- /dev/null
+++ b/Code/Light.GuardClauses.Performance/ComparableAssertions/MustNotBeApproximatelyBenchmark.cs
@@ -0,0 +1,25 @@
+using System;
+using BenchmarkDotNet.Attributes;
+
+namespace Light.GuardClauses.Performance.ComparableAssertions;
+
+[MemoryDiagnoser]
+// ReSharper disable once ClassCanBeSealed.Global -- Benchmark.NET derives from this class with dynamically created code
+public class MustNotBeApproximatelyBenchmark
+{
+ [Benchmark]
+ public double MustNotBeApproximately() => 5.2.MustNotBeApproximately(5.1, 0.5);
+
+ [Benchmark(Baseline = true)]
+ public double Imperative() => Imperative(5.2, 5.1, 0.5);
+
+ private static double Imperative(double parameter, double other, double tolerance)
+ {
+ if (Math.Abs(parameter - other) < tolerance)
+ {
+ throw new ArgumentOutOfRangeException(nameof(parameter), $"Value is approximately {other}");
+ }
+
+ return parameter;
+ }
+}
\ No newline at end of file
diff --git a/Code/Light.GuardClauses.Performance/Light.GuardClauses.Performance.csproj b/Code/Light.GuardClauses.Performance/Light.GuardClauses.Performance.csproj
index 7ffe52c8..cbf14189 100644
--- a/Code/Light.GuardClauses.Performance/Light.GuardClauses.Performance.csproj
+++ b/Code/Light.GuardClauses.Performance/Light.GuardClauses.Performance.csproj
@@ -8,7 +8,7 @@
-
+
\ No newline at end of file
diff --git a/Code/Light.GuardClauses.Performance/Program.cs b/Code/Light.GuardClauses.Performance/Program.cs
index 6bdd8259..b17928a0 100644
--- a/Code/Light.GuardClauses.Performance/Program.cs
+++ b/Code/Light.GuardClauses.Performance/Program.cs
@@ -4,18 +4,17 @@
using BenchmarkDotNet.Jobs;
using BenchmarkDotNet.Running;
-namespace Light.GuardClauses.Performance
+namespace Light.GuardClauses.Performance;
+
+public static class Program
{
- public static class Program
- {
- private static IConfig DefaultConfiguration =>
- DefaultConfig
- .Instance
- .AddJob(Job.Default.WithRuntime(CoreRuntime.Core70))
- .AddJob(Job.Default.WithRuntime(ClrRuntime.Net48))
- .AddDiagnoser(MemoryDiagnoser.Default, new DisassemblyDiagnoser(new DisassemblyDiagnoserConfig()));
+ private static IConfig DefaultConfiguration =>
+ DefaultConfig
+ .Instance
+ .AddJob(Job.Default.WithRuntime(CoreRuntime.Core80))
+ .AddJob(Job.Default.WithRuntime(ClrRuntime.Net48))
+ .AddDiagnoser(MemoryDiagnoser.Default, new DisassemblyDiagnoser(new DisassemblyDiagnoserConfig()));
- public static void Main(string[] arguments) =>
- BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(arguments, DefaultConfiguration);
- }
+ public static void Main(string[] arguments) =>
+ BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(arguments, DefaultConfiguration);
}
\ No newline at end of file
diff --git a/Code/Light.GuardClauses.Source/Light.GuardClauses.Source.csproj b/Code/Light.GuardClauses.Source/Light.GuardClauses.Source.csproj
index 229eeab9..0f9b0900 100644
--- a/Code/Light.GuardClauses.Source/Light.GuardClauses.Source.csproj
+++ b/Code/Light.GuardClauses.Source/Light.GuardClauses.Source.csproj
@@ -5,13 +5,13 @@
12.0
Kenny Pflug
Kenny Pflug
- Copyright © Kenny Pflug 2016 - 2023
+ Copyright © Kenny Pflug 2016, 2025
true
-
-
+
+
\ No newline at end of file
diff --git a/Code/Light.GuardClauses.SourceCodeTransformation/Light.GuardClauses.SourceCodeTransformation.csproj b/Code/Light.GuardClauses.SourceCodeTransformation/Light.GuardClauses.SourceCodeTransformation.csproj
index f9959aed..371827b8 100644
--- a/Code/Light.GuardClauses.SourceCodeTransformation/Light.GuardClauses.SourceCodeTransformation.csproj
+++ b/Code/Light.GuardClauses.SourceCodeTransformation/Light.GuardClauses.SourceCodeTransformation.csproj
@@ -13,7 +13,7 @@
-
+
diff --git a/Code/Light.GuardClauses.SourceCodeTransformation/SourceFileMerger.cs b/Code/Light.GuardClauses.SourceCodeTransformation/SourceFileMerger.cs
index 6ceb25d7..a5af7e00 100644
--- a/Code/Light.GuardClauses.SourceCodeTransformation/SourceFileMerger.cs
+++ b/Code/Light.GuardClauses.SourceCodeTransformation/SourceFileMerger.cs
@@ -24,17 +24,20 @@ public static void CreateSingleSourceFile(SourceFileMergeOptions options)
{
Console.WriteLine("Appending version header...");
stringBuilder.AppendLine("/* ------------------------------")
- .AppendLine($" Light.GuardClauses {typeof(SourceFileMerger).Assembly.GetName().Version!.ToString(3)}")
+ .AppendLine(
+ $" Light.GuardClauses {typeof(SourceFileMerger).Assembly.GetName().Version!.ToString(3)}"
+ )
.AppendLine(" ------------------------------")
.AppendLine();
}
Console.WriteLine("Creating default file layout...");
stringBuilder.AppendLineIf(!options.IncludeVersionComment, "/*")
- .AppendLine($@"License information for Light.GuardClauses
+ .AppendLine(
+ $@"License information for Light.GuardClauses
The MIT License (MIT)
-Copyright (c) 2016, 2024 Kenny Pflug mailto:kenny.pflug@live.de
+Copyright (c) 2016, 2025 Kenny Pflug mailto:kenny.pflug@live.de
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the ""Software""), to deal
@@ -68,6 +71,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
using System.Text;
using System.Text.RegularExpressions;
{(options.IncludeJetBrainsAnnotationsUsing ? "using JetBrains.Annotations;" + Environment.NewLine : string.Empty)}using {options.BaseNamespace}.Exceptions;
+using {options.BaseNamespace}.ExceptionFactory;
using {options.BaseNamespace}.FrameworkExtensions;
{(options.IncludeJetBrainsAnnotationsUsing ? "using NotNullAttribute = System.Diagnostics.CodeAnalysis.NotNullAttribute;" : "")}
@@ -82,12 +86,18 @@ namespace {options.BaseNamespace}.Exceptions
{{
}}
+namespace {options.BaseNamespace}.ExceptionFactory
+{{
+}}
+
namespace {options.BaseNamespace}.FrameworkExtensions
{{
-}}");
+}}"
+ );
if (options.IncludeJetBrainsAnnotations)
{
- stringBuilder.AppendLine().AppendLine(@"/*
+ stringBuilder.AppendLine().AppendLine(
+ @"/*
License information for JetBrains.Annotations
MIT License
@@ -113,12 +123,14 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
namespace JetBrains.Annotations
{
-}");
+}"
+ );
}
if (options.IncludeCodeAnalysisNullableAttributes)
{
- stringBuilder.AppendLine().AppendLine(@"
+ stringBuilder.AppendLine().AppendLine(
+ @"
namespace System.Diagnostics.CodeAnalysis
{
///
@@ -293,12 +305,14 @@ public NotNullWhenAttribute(bool returnValue)
ReturnValue = returnValue;
}
}
-}");
+}"
+ );
}
if (options.IncludeCallerArgumentExpressionAttribute)
{
- stringBuilder.AppendLine().AppendLine(@"
+ stringBuilder.AppendLine().AppendLine(
+ @"
namespace System.Runtime.CompilerServices
{
[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)]
@@ -311,10 +325,11 @@ public CallerArgumentExpressionAttribute(string parameterName)
public string ParameterName { get; }
}
-}");
+}"
+ );
}
- var csharpParseOptions = new CSharpParseOptions(LanguageVersion.CSharp10);
+ var csharpParseOptions = new CSharpParseOptions(LanguageVersion.CSharp12);
var targetSyntaxTree = CSharpSyntaxTree.ParseText(stringBuilder.ToString(), csharpParseOptions);
var targetRoot = (CompilationUnitSyntax) targetSyntaxTree.GetRoot();
@@ -323,63 +338,131 @@ public CallerArgumentExpressionAttribute(string parameterName)
.OfType()
.ToList();
var defaultNamespace = namespaces.First(@namespace => @namespace.Name.ToString() == $"{options.BaseNamespace}");
- var exceptionsNamespace = namespaces.First(@namespace => @namespace.Name.ToString() == $"{options.BaseNamespace}.Exceptions");
- var extensionsNamespace = namespaces.First(@namespace => @namespace.Name.ToString() == $"{options.BaseNamespace}.FrameworkExtensions");
- var jetBrainsNamespace = namespaces.FirstOrDefault(@namespace => @namespace.Name.ToString() == "JetBrains.Annotations");
+ var exceptionsNamespace =
+ namespaces.First(@namespace => @namespace.Name.ToString() == $"{options.BaseNamespace}.Exceptions");
+ var extensionsNamespace = namespaces.First(
+ @namespace => @namespace.Name.ToString() == $"{options.BaseNamespace}.FrameworkExtensions"
+ );
+ var exceptionFactoryNamespace = namespaces.First(
+ @namespace => @namespace.Name.ToString() == $"{options.BaseNamespace}.ExceptionFactory"
+ );
+ var jetBrainsNamespace =
+ namespaces.FirstOrDefault(@namespace => @namespace.Name.ToString() == "JetBrains.Annotations");
var replacedNodes = new Dictionary
{
[defaultNamespace] = defaultNamespace,
[exceptionsNamespace] = exceptionsNamespace,
- [extensionsNamespace] = extensionsNamespace
+ [extensionsNamespace] = extensionsNamespace,
+ [exceptionFactoryNamespace] = exceptionFactoryNamespace,
};
if (options.IncludeJetBrainsAnnotations && jetBrainsNamespace != null)
+ {
replacedNodes.Add(jetBrainsNamespace, jetBrainsNamespace);
+ }
var allSourceFiles = new DirectoryInfo(options.SourceFolder).GetFiles("*.cs", SearchOption.AllDirectories)
- .Where(f => !f.FullName.Contains("obj") &&
- !f.FullName.Contains("bin"))
+ .Where(
+ f => !f.FullName.Contains("obj") &&
+ !f.FullName.Contains("bin")
+ )
.ToDictionary(f => f.Name);
- // Start with Check.CommonAssertions before all other files to prepare the Check class
- Console.WriteLine("Merging CommonAssertions of the Check class...");
- var currentFile = allSourceFiles["Check.CommonAssertions.cs"];
-
+ // Start with Check.cs before all other files to prepare the Check class
+ Console.WriteLine("Preparing Check class...");
+ var currentFile = allSourceFiles["Check.cs"];
var sourceSyntaxTree = CSharpSyntaxTree.ParseText(currentFile.ReadContent(), csharpParseOptions);
var checkClassDeclaration = (ClassDeclarationSyntax) sourceSyntaxTree.GetRoot()
.DescendantNodes()
- .First(node => node.IsKind(SyntaxKind.ClassDeclaration));
- checkClassDeclaration = checkClassDeclaration.WithModifiers(checkClassDeclaration.Modifiers.Remove(checkClassDeclaration.Modifiers.First(token => token.IsKind(SyntaxKind.PartialKeyword))));
+ .First(
+ node => node.IsKind(
+ SyntaxKind.ClassDeclaration
+ )
+ );
+ checkClassDeclaration = checkClassDeclaration.WithModifiers(
+ checkClassDeclaration.Modifiers.Remove(
+ checkClassDeclaration.Modifiers.First(token => token.IsKind(SyntaxKind.PartialKeyword))
+ )
+ );
+
+ // Do the same thing for the Throw class
+ Console.WriteLine("Preparing Throw class...");
+ currentFile = allSourceFiles["Throw.cs"];
+ sourceSyntaxTree = CSharpSyntaxTree.ParseText(currentFile.ReadContent(), csharpParseOptions);
+ var throwClassDeclaration = (ClassDeclarationSyntax) sourceSyntaxTree.GetRoot()
+ .DescendantNodes()
+ .First(
+ node => node.IsKind(
+ SyntaxKind.ClassDeclaration
+ )
+ );
+ throwClassDeclaration = throwClassDeclaration.WithModifiers(
+ throwClassDeclaration.Modifiers.Remove(
+ throwClassDeclaration.Modifiers.First(token => token.IsKind(SyntaxKind.PartialKeyword))
+ )
+ );
// Process all other files
Console.WriteLine("Merging remaining files...");
foreach (var fileName in allSourceFiles.Keys)
{
+ if (fileName == "SpanDelegates.cs")
+ {
+
+ }
+
if (!CheckIfFileShouldBeProcessed(options, fileName))
+ {
continue;
+ }
currentFile = allSourceFiles[fileName];
sourceSyntaxTree = CSharpSyntaxTree.ParseText(currentFile.ReadContent(), csharpParseOptions);
- var originalNamespace = DetermineOriginalNamespace(options,
- defaultNamespace,
- currentFile,
- extensionsNamespace,
- exceptionsNamespace,
- jetBrainsNamespace);
+ var originalNamespace = DetermineOriginalNamespace(
+ options,
+ defaultNamespace,
+ currentFile,
+ extensionsNamespace,
+ exceptionsNamespace,
+ exceptionFactoryNamespace,
+ jetBrainsNamespace
+ );
// If the file contains assertions, add it to the existing Check class declaration
if (originalNamespace == defaultNamespace && currentFile.Name.StartsWith("Check."))
{
var classDeclaration = (ClassDeclarationSyntax) sourceSyntaxTree.GetRoot()
.DescendantNodes()
- .First(node => node.IsKind(SyntaxKind.ClassDeclaration));
- checkClassDeclaration = checkClassDeclaration.WithMembers(checkClassDeclaration.Members.AddRange(classDeclaration.Members));
+ .First(
+ node => node.IsKind(
+ SyntaxKind.ClassDeclaration
+ )
+ );
+ checkClassDeclaration =
+ checkClassDeclaration.WithMembers(checkClassDeclaration.Members.AddRange(classDeclaration.Members));
+ continue;
+ }
+
+ // Do something similar for the Throw class
+ if (originalNamespace == exceptionFactoryNamespace && currentFile.Name.StartsWith("Throw."))
+ {
+ var classDeclaration = (ClassDeclarationSyntax) sourceSyntaxTree.GetRoot()
+ .DescendantNodes()
+ .First(
+ node => node.IsKind(
+ SyntaxKind.ClassDeclaration
+ )
+ );
+ throwClassDeclaration =
+ throwClassDeclaration.WithMembers(throwClassDeclaration.Members.AddRange(classDeclaration.Members));
continue;
}
// Else just get the members of the first namespace and add them to the corresponding one
var sourceCompilationUnit = (CompilationUnitSyntax) sourceSyntaxTree.GetRoot();
if (sourceCompilationUnit.Members.IsNullOrEmpty())
+ {
continue;
+ }
var membersToAdd = ((FileScopedNamespaceDeclarationSyntax) sourceCompilationUnit.Members[0]).Members;
@@ -387,15 +470,25 @@ public CallerArgumentExpressionAttribute(string parameterName)
replacedNodes[originalNamespace] =
currentlyEditedNamespace
.WithMembers(
- currentlyEditedNamespace.Members.AddRange(membersToAdd));
+ currentlyEditedNamespace.Members.AddRange(membersToAdd)
+ );
}
// After the Check class declaration is finished, insert it into the default namespace
var currentDefaultNamespace = replacedNodes[defaultNamespace];
- replacedNodes[defaultNamespace] = currentDefaultNamespace.WithMembers(currentDefaultNamespace.Members.Insert(0, checkClassDeclaration));
+ replacedNodes[defaultNamespace] =
+ currentDefaultNamespace.WithMembers(currentDefaultNamespace.Members.Insert(0, checkClassDeclaration));
+
+ // Do the same thing for the Throw class
+ var currentExceptionFactoryNamespace = replacedNodes[exceptionFactoryNamespace];
+ replacedNodes[exceptionFactoryNamespace] =
+ currentExceptionFactoryNamespace.WithMembers(
+ currentExceptionFactoryNamespace.Members.Insert(0, throwClassDeclaration)
+ );
// Update the target compilation unit
- targetRoot = targetRoot.ReplaceNodes(replacedNodes.Keys, (originalNode, _) => replacedNodes[originalNode]).NormalizeWhitespace();
+ targetRoot = targetRoot.ReplaceNodes(replacedNodes.Keys, (originalNode, _) => replacedNodes[originalNode])
+ .NormalizeWhitespace();
// Make types internal if necessary
if (options.ChangePublicTypesToInternalTypes)
@@ -403,38 +496,57 @@ public CallerArgumentExpressionAttribute(string parameterName)
Console.WriteLine("Types are changed from public to internal...");
var changedTypeDeclarations = new Dictionary();
- foreach (var typeDeclaration in targetRoot.DescendantNodes().Where(node => node.IsKind(SyntaxKind.ClassDeclaration) ||
- node.IsKind(SyntaxKind.StructDeclaration) ||
- node.IsKind(SyntaxKind.EnumDeclaration) ||
- node.IsKind(SyntaxKind.DelegateDeclaration)))
+ foreach (var typeDeclaration in targetRoot.DescendantNodes().Where(
+ node => node.IsKind(SyntaxKind.ClassDeclaration) ||
+ node.IsKind(SyntaxKind.StructDeclaration) ||
+ node.IsKind(SyntaxKind.EnumDeclaration) ||
+ node.IsKind(SyntaxKind.DelegateDeclaration)
+ ))
{
if (typeDeclaration is BaseTypeDeclarationSyntax typeDeclarationSyntax)
{
var publicModifier = typeDeclarationSyntax.Modifiers[0];
var adjustedModifiers = typeDeclarationSyntax.Modifiers
.RemoveAt(0)
- .Insert(0, Token(SyntaxKind.InternalKeyword).WithTriviaFrom(publicModifier));
+ .Insert(
+ 0,
+ Token(SyntaxKind.InternalKeyword)
+ .WithTriviaFrom(publicModifier)
+ );
if (typeDeclarationSyntax is ClassDeclarationSyntax classDeclaration)
+ {
changedTypeDeclarations[classDeclaration] = classDeclaration.WithModifiers(adjustedModifiers);
+ }
else if (typeDeclarationSyntax is StructDeclarationSyntax structDeclaration)
+ {
changedTypeDeclarations[structDeclaration] = structDeclaration.WithModifiers(adjustedModifiers);
+ }
else if (typeDeclarationSyntax is EnumDeclarationSyntax enumDeclaration)
+ {
changedTypeDeclarations[enumDeclaration] = enumDeclaration.WithModifiers(adjustedModifiers);
+ }
}
else if (typeDeclaration is DelegateDeclarationSyntax delegateDeclaration)
{
var publicModifier = delegateDeclaration.Modifiers[0];
var adjustedModifiers = delegateDeclaration.Modifiers
.RemoveAt(0)
- .Insert(0, Token(SyntaxKind.InternalKeyword).WithTriviaFrom(publicModifier));
+ .Insert(
+ 0,
+ Token(SyntaxKind.InternalKeyword)
+ .WithTriviaFrom(publicModifier)
+ );
changedTypeDeclarations[delegateDeclaration] = delegateDeclaration.WithModifiers(adjustedModifiers);
}
}
- targetRoot = targetRoot.ReplaceNodes(changedTypeDeclarations.Keys, (originalNode, _) => changedTypeDeclarations[originalNode]);
+ targetRoot = targetRoot.ReplaceNodes(
+ changedTypeDeclarations.Keys,
+ (originalNode, _) => changedTypeDeclarations[originalNode]
+ );
}
// Remove assertion overloads that incorporate an exception factory if necessary
@@ -443,23 +555,49 @@ public CallerArgumentExpressionAttribute(string parameterName)
Console.WriteLine("Removing overloads with exception factory...");
var checkClass = (ClassDeclarationSyntax) targetRoot.DescendantNodes()
- .First(node => node.IsKind(SyntaxKind.ClassDeclaration) &&
- node is ClassDeclarationSyntax { Identifier.Text: "Check" });
+ .First(
+ node => node.IsKind(SyntaxKind.ClassDeclaration) &&
+ node is ClassDeclarationSyntax
+ {
+ Identifier.Text: "Check",
+ }
+ );
var membersWithoutExceptionFactory =
checkClass.Members
- .Where(member => member is not MethodDeclarationSyntax method || method.ParameterList.Parameters.All(parameter => parameter.Identifier.Text != "exceptionFactory"));
- targetRoot = targetRoot.ReplaceNode(checkClass, checkClass.WithMembers(new SyntaxList(membersWithoutExceptionFactory)));
+ .Where(
+ member => member is not MethodDeclarationSyntax method ||
+ method.ParameterList.Parameters.All(
+ parameter => parameter.Identifier.Text != "exceptionFactory"
+ )
+ );
+ targetRoot = targetRoot.ReplaceNode(
+ checkClass,
+ checkClass.WithMembers(new (membersWithoutExceptionFactory))
+ );
// Remove members from Throw class that use exception factories
var throwClass = (ClassDeclarationSyntax) targetRoot.DescendantNodes()
- .First(node => node.IsKind(SyntaxKind.ClassDeclaration) &&
- node is ClassDeclarationSyntax { Identifier.Text: "Throw" });
+ .First(
+ node => node.IsKind(SyntaxKind.ClassDeclaration) &&
+ node is ClassDeclarationSyntax
+ {
+ Identifier.Text: "Throw",
+ }
+ );
membersWithoutExceptionFactory =
throwClass.Members
- .Where(member => member is not MethodDeclarationSyntax method || method.ParameterList.Parameters.All(parameter => parameter.Identifier.Text != "exceptionFactory"));
- targetRoot = targetRoot.ReplaceNode(throwClass, throwClass.WithMembers(new SyntaxList(membersWithoutExceptionFactory)));
+ .Where(
+ member => member is not MethodDeclarationSyntax method ||
+ method.ParameterList.Parameters.All(
+ parameter => parameter.Identifier.Text != "exceptionFactory"
+ )
+ );
+ targetRoot = targetRoot.ReplaceNode(
+ throwClass,
+ throwClass.WithMembers(new (membersWithoutExceptionFactory))
+ );
}
var targetFileContent = targetRoot.ToFullString();
@@ -473,35 +611,39 @@ public CallerArgumentExpressionAttribute(string parameterName)
}
private static bool CheckIfFileShouldBeProcessed(SourceFileMergeOptions options, string fileName) =>
- fileName != "Check.CommonAssertions.cs" &&
+ fileName != "Check.cs" &&
+ fileName != "Throw.cs" &&
fileName != "CallerArgumentExpressionAttribute.cs" &&
(fileName != "ReSharperAnnotations.cs" || options.IncludeJetBrainsAnnotations) &&
(fileName != "ValidatedNotNullAttribute.cs" || options.IncludeValidatedNotNullAttribute);
- private static NamespaceDeclarationSyntax DetermineOriginalNamespace(SourceFileMergeOptions options,
- NamespaceDeclarationSyntax defaultNamespace,
- FileInfo currentFile,
- NamespaceDeclarationSyntax extensionsNamespace,
- NamespaceDeclarationSyntax exceptionsNamespace,
- NamespaceDeclarationSyntax? jetBrainsNamespace)
+ private static NamespaceDeclarationSyntax DetermineOriginalNamespace(
+ SourceFileMergeOptions options,
+ NamespaceDeclarationSyntax defaultNamespace,
+ FileInfo currentFile,
+ NamespaceDeclarationSyntax extensionsNamespace,
+ NamespaceDeclarationSyntax exceptionsNamespace,
+ NamespaceDeclarationSyntax exceptionFactoryNamespace,
+ NamespaceDeclarationSyntax? jetBrainsNamespace
+ )
{
var originalNamespace = defaultNamespace;
switch (currentFile.Directory?.Name)
{
- case "FrameworkExtensions":
- originalNamespace = extensionsNamespace;
- break;
- case "Exceptions":
- originalNamespace = exceptionsNamespace;
- break;
+ case "FrameworkExtensions": originalNamespace = extensionsNamespace; break;
+ case "Exceptions": originalNamespace = exceptionsNamespace; break;
+ case "ExceptionFactory": originalNamespace = exceptionFactoryNamespace; break;
default:
{
if (options.IncludeJetBrainsAnnotations && currentFile.Name == "ReSharperAnnotations.cs")
+ {
originalNamespace = jetBrainsNamespace!;
+ }
+
break;
}
}
return originalNamespace;
}
-}
\ No newline at end of file
+}
diff --git a/Code/Light.GuardClauses.Tests/CommonAssertions/IsApproximatelyTests.cs b/Code/Light.GuardClauses.Tests/CommonAssertions/IsApproximatelyTests.cs
index c0abf2d3..70db5a8d 100644
--- a/Code/Light.GuardClauses.Tests/CommonAssertions/IsApproximatelyTests.cs
+++ b/Code/Light.GuardClauses.Tests/CommonAssertions/IsApproximatelyTests.cs
@@ -17,7 +17,8 @@ public static void DoubleWithDefaultTolerance(double first, double second, bool
[InlineData(1.1, 1.3, 0.5, true)]
[InlineData(100.55, 100.555, 0.00001, false)]
[InlineData(5.0, 14.999999, 10.0, true)]
- [InlineData(5.0, 15.0, 10.0, false)]
+ [InlineData(5.0, 15.0, 10.0, true)]
+ [InlineData(4.9998, 15.0, 10.0, false)]
[InlineData(5.0, 15.0001, 10.0, false)]
public static void DoubleWithCustomTolerance(double first, double second, double tolerance, bool expected) =>
first.IsApproximately(second, tolerance).Should().Be(expected);
@@ -34,8 +35,69 @@ public static void FloatWithDefaultTolerance(float first, float second, bool exp
[InlineData(1.1f, 1.3f, 0.5f, true)]
[InlineData(100.55f, 100.555f, 0.00001f, false)]
[InlineData(5.0f, 14.999999f, 10.0f, true)]
- [InlineData(5.0f, 15.0f, 10.0f, false)]
+ [InlineData(5.0f, 15.0f, 10.0f, true)]
+ [InlineData(4.99f, 15.0f, 10.0f, false)]
[InlineData(5.0f, 15.0001f, 10.0f, false)]
public static void FloatWithCustomTolerance(float first, float second, float tolerance, bool expected) =>
first.IsApproximately(second, tolerance).Should().Be(expected);
-}
\ No newline at end of file
+
+#if NET8_0
+ [Theory]
+ [InlineData(1.1, 1.3, 0.5, true)]
+ [InlineData(100.55, 100.555, 0.00001, false)]
+ [InlineData(5.0, 14.999999, 10.0, true)]
+ [InlineData(5.0, 15.0, 10.0, true)]
+ [InlineData(5.0, 15.000001, 10.0, false)]
+ [InlineData(5.0, 15.0001, 10.0, false)]
+ public static void GenericDoubleWithCustomTolerance(double first, double second, double tolerance, bool expected) =>
+ first.IsApproximately(second, tolerance).Should().Be(expected);
+
+ [Theory]
+ [InlineData(1.1f, 1.3f, 0.5f, true)]
+ [InlineData(100.55f, 100.555f, 0.00001f, false)]
+ [InlineData(5.0f, 14.999999f, 10.0f, true)]
+ [InlineData(5.0f, 15.0f, 10.0f, true)]
+ [InlineData(5.0f, 15.01f, 10.0f, false)]
+ [InlineData(5.0f, 15.0001f, 10.0f, false)]
+ public static void GenericFloatWithCustomTolerance(float first, float second, float tolerance, bool expected) =>
+ first.IsApproximately(second, tolerance).Should().Be(expected);
+
+ [Theory]
+ [InlineData(5, 10, 10, true)]
+ [InlineData(5, 15, 10, true)]
+ [InlineData(4, 15, 10, false)]
+ [InlineData(-5, 5, 12, true)]
+ [InlineData(-100, 100, 199, false)]
+ [InlineData(42, 42, 1, true)]
+ public static void GenericIntWithCustomTolerance(int first, int second, int tolerance, bool expected) =>
+ first.IsApproximately(second, tolerance).Should().Be(expected);
+
+ [Theory]
+ [InlineData(5L, 10L, 10L, true)]
+ [InlineData(5L, 15L, 10L, true)]
+ [InlineData(5L, 16L, 10L, false)]
+ [InlineData(-5L, 5L, 12L, true)]
+ [InlineData(-100L, 100L, 199L, false)]
+ [InlineData(42L, 42L, 1L, true)]
+ public static void GenericLongWithCustomTolerance(long first, long second, long tolerance, bool expected) =>
+ first.IsApproximately(second, tolerance).Should().Be(expected);
+
+ [Theory]
+ [MemberData(nameof(DecimalTestData))]
+ public static void GenericDecimalWithCustomTolerance(
+ decimal first,
+ decimal second,
+ decimal tolerance,
+ bool expected
+ ) =>
+ first.IsApproximately(second, tolerance).Should().Be(expected);
+
+ public static TheoryData DecimalTestData() => new ()
+ {
+ { 1.1m, 1.3m, 0.5m, true },
+ { 100.55m, 100.555m, 0.00001m, false },
+ { 5.0m, 14.999999m, 10.0m, true },
+ { 5.0m, 15.0m, 9.99m, false },
+ };
+#endif
+}
diff --git a/Code/Light.GuardClauses.Tests/CommonAssertions/IsGreaterThanOrApproximatelyTests.cs b/Code/Light.GuardClauses.Tests/CommonAssertions/IsGreaterThanOrApproximatelyTests.cs
index d7d59d5c..96ccedcb 100644
--- a/Code/Light.GuardClauses.Tests/CommonAssertions/IsGreaterThanOrApproximatelyTests.cs
+++ b/Code/Light.GuardClauses.Tests/CommonAssertions/IsGreaterThanOrApproximatelyTests.cs
@@ -36,4 +36,59 @@ public static void FloatWithDefaultTolerance(float first, float second, bool exp
[InlineData(1.0f, 2.0f, 0.1f, false)]
public static void FloatWIthCustomTolerance(float first, float second, float tolerance, bool expected) =>
first.IsGreaterThanOrApproximately(second, tolerance).Should().Be(expected);
-}
\ No newline at end of file
+
+#if NET8_0
+ [Theory]
+ [InlineData(15.91, 15.9, 0.1, true)]
+ [InlineData(24.449, 24.45, 0.0001, false)]
+ [InlineData(-3.12, -3.2, 0.001, true)]
+ [InlineData(2.369, 2.37, 0.0005, false)]
+ [InlineData(15.0, 14.0, 0.1, true)] // Greater than case
+ [InlineData(14.95, 15.0, 0.1, true)] // Approximately equal case
+ public static void GenericDoubleWithCustomTolerance(double first, double second, double tolerance, bool expected) =>
+ first.IsGreaterThanOrApproximately(second, tolerance).Should().Be(expected);
+
+ [Theory]
+ [InlineData(2.0f, 1.0f, 0.1f, true)]
+ [InlineData(1.0f, 1.0f, 0.1f, true)]
+ [InlineData(1.0f, 1.1f, 0.01f, false)]
+ [InlineData(1.0f, 2.0f, 0.1f, false)]
+ [InlineData(2.1f, 2.0f, 0.01f, true)] // Greater than case
+ public static void GenericFloatWithCustomTolerance(float first, float second, float tolerance, bool expected) =>
+ first.IsGreaterThanOrApproximately(second, tolerance).Should().Be(expected);
+
+ [Theory]
+ [InlineData(10, 5, 1, true)] // Greater than case
+ [InlineData(5, 5, 1, true)] // Equal case
+ [InlineData(5, 6, 1, true)] // Approximately equal case
+ [InlineData(5, 7, 1, false)] // Not greater than or approximately equal case
+ public static void GenericIntWithCustomTolerance(int first, int second, int tolerance, bool expected) =>
+ first.IsGreaterThanOrApproximately(second, tolerance).Should().Be(expected);
+
+ [Theory]
+ [InlineData(10L, 5L, 1L, true)] // Greater than case
+ [InlineData(5L, 5L, 1L, true)] // Equal case
+ [InlineData(5L, 6L, 1L, true)] // Approximately equal case
+ [InlineData(5L, 7L, 1L, false)] // Not greater than or approximately equal case
+ public static void GenericLongWithCustomTolerance(long first, long second, long tolerance, bool expected) =>
+ first.IsGreaterThanOrApproximately(second, tolerance).Should().Be(expected);
+
+ [Theory]
+ [MemberData(nameof(DecimalTestData))]
+ public static void GenericDecimalWithCustomTolerance(
+ decimal first,
+ decimal second,
+ decimal tolerance,
+ bool expected
+ ) =>
+ first.IsGreaterThanOrApproximately(second, tolerance).Should().Be(expected);
+
+ public static TheoryData DecimalTestData() => new ()
+ {
+ { 1.3m, 1.1m, 0.1m, true }, // Greater than case
+ { 1.1m, 1.1m, 0.1m, true }, // Equal case
+ { 1.0m, 1.1m, 0.2m, true }, // Approximately equal case
+ { 1.0m, 1.3m, 0.1m, false }, // Not greater than or approximately equal case
+ };
+#endif
+}
diff --git a/Code/Light.GuardClauses.Tests/CommonAssertions/IsLessThanOrApproximatelyTests.cs b/Code/Light.GuardClauses.Tests/CommonAssertions/IsLessThanOrApproximatelyTests.cs
index c710ec22..6e774b83 100644
--- a/Code/Light.GuardClauses.Tests/CommonAssertions/IsLessThanOrApproximatelyTests.cs
+++ b/Code/Light.GuardClauses.Tests/CommonAssertions/IsLessThanOrApproximatelyTests.cs
@@ -36,4 +36,56 @@ public static void FloatWithDefaultTolerance(float first, float second, bool exp
[InlineData(0f, -1f, 0.9f, false)]
public static void FloatWithCustomTolerance(float first, float second, float tolerance, bool expected) =>
first.IsLessThanOrApproximately(second, tolerance).Should().Be(expected);
+
+#if NET8_0
+ [Theory]
+ [InlineData(13.25, 13.5, 0.1, true)] // Less than case
+ [InlineData(13.5, 13.5, 0.1, true)] // Equal case
+ [InlineData(13.55, 13.5, 0.1, true)] // Approximately equal case
+ [InlineData(13.7, 13.5, 0.1, false)] // Not less than or approximately equal case
+ public static void GenericDoubleWithCustomTolerance(double first, double second, double tolerance, bool expected) =>
+ first.IsLessThanOrApproximately(second, tolerance).Should().Be(expected);
+
+ [Theory]
+ [InlineData(13.25f, 13.5f, 0.1f, true)] // Less than case
+ [InlineData(13.5f, 13.5f, 0.1f, true)] // Equal case
+ [InlineData(13.55f, 13.5f, 0.1f, true)] // Approximately equal case
+ [InlineData(13.7f, 13.5f, 0.1f, false)] // Not less than or approximately equal case
+ public static void GenericFloatWithCustomTolerance(float first, float second, float tolerance, bool expected) =>
+ first.IsLessThanOrApproximately(second, tolerance).Should().Be(expected);
+
+ [Theory]
+ [InlineData(5, 10, 1, true)] // Less than case
+ [InlineData(5, 5, 1, true)] // Equal case
+ [InlineData(6, 5, 1, true)] // Approximately equal case
+ [InlineData(7, 5, 1, false)] // Not less than or approximately equal case
+ public static void GenericIntWithCustomTolerance(int first, int second, int tolerance, bool expected) =>
+ first.IsLessThanOrApproximately(second, tolerance).Should().Be(expected);
+
+ [Theory]
+ [InlineData(5L, 10L, 1L, true)] // Less than case
+ [InlineData(5L, 5L, 1L, true)] // Equal case
+ [InlineData(6L, 5L, 1L, true)] // Approximately equal case
+ [InlineData(7L, 5L, 1L, false)] // Not less than or approximately equal case
+ public static void GenericLongWithCustomTolerance(long first, long second, long tolerance, bool expected) =>
+ first.IsLessThanOrApproximately(second, tolerance).Should().Be(expected);
+
+ [Theory]
+ [MemberData(nameof(DecimalTestData))]
+ public static void GenericDecimalWithCustomTolerance(
+ decimal first,
+ decimal second,
+ decimal tolerance,
+ bool expected
+ ) =>
+ first.IsLessThanOrApproximately(second, tolerance).Should().Be(expected);
+
+ public static TheoryData DecimalTestData() => new ()
+ {
+ { 1.0m, 1.2m, 0.1m, true }, // Less than case
+ { 1.1m, 1.1m, 0.1m, true }, // Equal case
+ { 1.2m, 1.1m, 0.1m, true }, // Approximately equal case
+ { 1.3m, 1.1m, 0.1m, false }, // Not less than or approximately equal case
+ };
+#endif
}
\ No newline at end of file
diff --git a/Code/Light.GuardClauses.Tests/ComparableAssertions/MustBeApproximatelyTests.cs b/Code/Light.GuardClauses.Tests/ComparableAssertions/MustBeApproximatelyTests.cs
new file mode 100644
index 00000000..32046559
--- /dev/null
+++ b/Code/Light.GuardClauses.Tests/ComparableAssertions/MustBeApproximatelyTests.cs
@@ -0,0 +1,236 @@
+using System;
+using FluentAssertions;
+using Xunit;
+
+namespace Light.GuardClauses.Tests.ComparableAssertions;
+
+public static class MustBeApproximatelyTests
+{
+ [Theory]
+ [InlineData(5.1, 5.0, 0.2)]
+ [InlineData(10.3, 10.3, 0.01)]
+ [InlineData(3.14159, 3.14, 0.002)]
+ [InlineData(-42.0, -42.0001, 0.001)]
+ public static void ValuesApproximatelyEqual_Double(double value, double other, double tolerance) =>
+ value.MustBeApproximately(other, tolerance).Should().Be(value);
+
+ [Theory]
+ [InlineData(5.1f, 5.0f, 0.2f)]
+ [InlineData(10.3f, 10.3f, 0.01f)]
+ [InlineData(3.14159f, 3.14f, 0.002f)]
+ [InlineData(-42.0f, -42.0001f, 0.001f)]
+ public static void ValuesApproximatelyEqual_Float(float value, float other, float tolerance) =>
+ value.MustBeApproximately(other, tolerance).Should().Be(value);
+
+ [Theory]
+ [InlineData(5.0, 5.3, 0.1)]
+ [InlineData(100.0, 99.8, 0.1)]
+ [InlineData(-20.0, -20.2, 0.1)]
+ [InlineData(0.0001, 0.0002, 0.00005)]
+ public static void ValuesNotApproximatelyEqual_Double(double value, double other, double tolerance)
+ {
+ var act = () => value.MustBeApproximately(other, tolerance, nameof(value));
+
+ var exceptionAssertion = act.Should().Throw().Which;
+ exceptionAssertion.Message.Should().Contain(
+ $"{nameof(value)} must be approximately equal to {other} with a tolerance of {tolerance}, but it actually is {value}."
+ );
+ exceptionAssertion.ParamName.Should().BeSameAs(nameof(value));
+ }
+
+ [Theory]
+ [InlineData(5.0f, 5.3f, 0.1f)]
+ [InlineData(100.0f, 99.8f, 0.1f)]
+ [InlineData(-20.0f, -20.2f, 0.1f)]
+ [InlineData(0.0001f, 0.0002f, 0.00005f)]
+ public static void ValuesNotApproximatelyEqual_Float(float value, float other, float tolerance)
+ {
+ var act = () => value.MustBeApproximately(other, tolerance, nameof(value));
+
+ var exceptionAssertion = act.Should().Throw().Which;
+ exceptionAssertion.Message.Should().Contain(
+ $"{nameof(value)} must be approximately equal to {other} with a tolerance of {tolerance}, but it actually is {value}."
+ );
+ exceptionAssertion.ParamName.Should().BeSameAs(nameof(value));
+ }
+
+ [Fact]
+ public static void DefaultTolerance_Double()
+ {
+ // Should pass - difference is 0.00005 which is less than default tolerance 0.0001
+ const double value = 1.00005;
+ value.MustBeApproximately(1.0).Should().Be(value);
+
+ // Should throw - difference is 0.0002 which is greater than default tolerance 0.0001
+ Action act = () => 1.0002.MustBeApproximately(1.0, "parameter");
+ act.Should().Throw()
+ .WithParameterName("parameter");
+ }
+
+ [Fact]
+ public static void DefaultTolerance_Float()
+ {
+ // Should pass - difference is 0.00005f which is less than default tolerance 0.0001f
+ const float value = 1.00005f;
+ value.MustBeApproximately(1.0f).Should().Be(value);
+
+ // Should throw - difference is 0.0002f which is greater than default tolerance 0.0001f
+ Action act = () => 1.0002f.MustBeApproximately(1.0f, "parameter");
+ act.Should().Throw()
+ .WithParameterName("parameter");
+ }
+
+ [Fact]
+ public static void CustomException_Double() =>
+ Test.CustomException(
+ 5.0,
+ 5.3,
+ (x, y, exceptionFactory) => x.MustBeApproximately(y, exceptionFactory)
+ );
+
+ [Fact]
+ public static void CustomExceptionWithTolerance_Double() =>
+ Test.CustomException(
+ 5.0,
+ 5.5,
+ 0.1,
+ (x, y, z, exceptionFactory) => x.MustBeApproximately(y, z, exceptionFactory)
+ );
+
+ [Fact]
+ public static void CustomException_Float() =>
+ Test.CustomException(
+ 5.0f,
+ 5.3f,
+ (x, y, exceptionFactory) => x.MustBeApproximately(y, exceptionFactory)
+ );
+
+ [Fact]
+ public static void CustomExceptionWithTolerance_Float() =>
+ Test.CustomException(
+ 5.0f,
+ 5.5f,
+ 0.1f,
+ (x, y, z, exceptionFactory) => x.MustBeApproximately(y, z, exceptionFactory)
+ );
+
+ [Fact]
+ public static void NoCustomExceptionThrown_Double() =>
+ 5.0.MustBeApproximately(5.05, 0.1, (_, _, _) => null).Should().Be(5.0);
+
+ [Fact]
+ public static void NoCustomExceptionThrown_Float() =>
+ 5.0f.MustBeApproximately(5.05f, 0.1f, (_, _, _) => null).Should().Be(5.0f);
+
+ [Fact]
+ public static void CustomMessage_Double() =>
+ Test.CustomMessage(
+ message => 100.0.MustBeApproximately(101.0, 0.5, message: message)
+ );
+
+ [Fact]
+ public static void CustomMessage_Float() =>
+ Test.CustomMessage(
+ message => 100.0f.MustBeApproximately(101.0f, 0.5f, message: message)
+ );
+
+ [Fact]
+ public static void CallerArgumentExpression_Double()
+ {
+ const double seventyEightO1 = 78.1;
+
+ var act = () => seventyEightO1.MustBeApproximately(3.0);
+
+ act.Should().Throw()
+ .WithParameterName(nameof(seventyEightO1));
+ }
+
+ [Fact]
+ public static void CallerArgumentExpressionWithTolerance_Double()
+ {
+ const double pi = 3.14159;
+
+ var act = () => pi.MustBeApproximately(3.0, 0.1);
+
+ act.Should().Throw()
+ .WithParameterName(nameof(pi));
+ }
+
+ [Fact]
+ public static void CallerArgumentExpression_Float()
+ {
+ const float seventyEightO1 = 78.1f;
+
+ var act = () => seventyEightO1.MustBeApproximately(3.0f);
+
+ act.Should().Throw()
+ .WithParameterName(nameof(seventyEightO1));
+ }
+
+ [Fact]
+ public static void CallerArgumentExpressionWithTolerance_Float()
+ {
+ const float pi = 3.14159f;
+
+ var act = () => pi.MustBeApproximately(3.0f, 0.1f);
+
+ act.Should().Throw()
+ .WithParameterName(nameof(pi));
+ }
+
+#if NET8_0
+ [Theory]
+ [InlineData(5.1, 5.0, 0.2)]
+ [InlineData(10.3, 10.3, 0.01)]
+ [InlineData(3.14159, 3.14, 0.002)]
+ [InlineData(-42.0, -42.0001, 0.001)]
+ public static void ValuesApproximatelyEqual_Generic(double value, double other, double tolerance) =>
+ value.MustBeApproximately(other, tolerance).Should().Be(value);
+
+ [Theory]
+ [InlineData(5.0, 5.3, 0.1)]
+ [InlineData(100.0, 99.8, 0.1)]
+ [InlineData(-20.0, -20.2, 0.1)]
+ [InlineData(0.0001, 0.0002, 0.00005)]
+ public static void ValuesNotApproximatelyEqual_Generic(double value, double other, double tolerance)
+ {
+ var act = () => value.MustBeApproximately(other, tolerance, nameof(value));
+
+ var exceptionAssertion = act.Should().Throw().Which;
+ exceptionAssertion.Message.Should().Contain(
+ $"{nameof(value)} must be approximately equal to {other} with a tolerance of {tolerance}, but it actually is {value}."
+ );
+ exceptionAssertion.ParamName.Should().BeSameAs(nameof(value));
+ }
+
+ [Fact]
+ public static void CustomExceptionWithTolerance_Generic() =>
+ Test.CustomException(
+ 5.0,
+ 5.3,
+ 0.2,
+ (x, y, t, exceptionFactory) => x.MustBeApproximately(y, t, exceptionFactory)
+ );
+
+ [Fact]
+ public static void NoCustomExceptionThrown_Generic() =>
+ 5.0.MustBeApproximately(5.05, 0.1, (_, _, _) => null).Should().Be(5.0);
+
+ [Fact]
+ public static void CustomMessage_Generic() =>
+ Test.CustomMessage(
+ message => 100.0.MustBeApproximately(101.0, 0.5, message: message)
+ );
+
+ [Fact]
+ public static void CallerArgumentExpressionWithTolerance_Generic()
+ {
+ const double seventyEightO1 = 78.1;
+
+ var act = () => seventyEightO1.MustBeApproximately(3.0, 10.0);
+
+ act.Should().Throw()
+ .WithParameterName(nameof(seventyEightO1));
+ }
+#endif
+}
diff --git a/Code/Light.GuardClauses.Tests/ComparableAssertions/MustBeGreaterThanOrApproximatelyTests.cs b/Code/Light.GuardClauses.Tests/ComparableAssertions/MustBeGreaterThanOrApproximatelyTests.cs
new file mode 100644
index 00000000..ff19f604
--- /dev/null
+++ b/Code/Light.GuardClauses.Tests/ComparableAssertions/MustBeGreaterThanOrApproximatelyTests.cs
@@ -0,0 +1,295 @@
+using System;
+using FluentAssertions;
+using Xunit;
+
+namespace Light.GuardClauses.Tests.ComparableAssertions;
+
+public static class CheckMustBeGreaterThanOrApproximatelyTests
+{
+ [Theory]
+ [InlineData(17.4, 17.3)]
+ [InlineData(19.9999999, 20.0)]
+ [InlineData(-5.49998, -5.5)]
+ [InlineData(0.0001, 0.0001)]
+ public static void EqualOrGreater_Double(double first, double second) =>
+ first.MustBeGreaterThanOrApproximately(second).Should().Be(first);
+
+ [Theory]
+ [InlineData(15.91, 15.9, 0.1)]
+ [InlineData(24.49999, 24.45, 0.0001)]
+ [InlineData(-3.12, -3.2, 0.001)]
+ [InlineData(2.369, 2.37, 0.05)]
+ public static void EqualOrGreaterWithTolerance_Double(double first, double second, double tolerance) =>
+ first.MustBeGreaterThanOrApproximately(second, tolerance).Should().Be(first);
+
+ [Theory]
+ [InlineData(100.225f, 100.2f)]
+ [InlineData(-5.9f, -5.900005f)]
+ [InlineData(0f, -0.02f)]
+ [InlineData(-0.00001f, 0f)]
+ public static void EqualOrGreater_Float(float first, float second) =>
+ first.MustBeGreaterThanOrApproximately(second).Should().Be(first);
+
+ [Theory]
+ [InlineData(2.0f, 1.0f, 0.1f)]
+ [InlineData(1.0f, 1.0f, 0.1f)]
+ [InlineData(1.01f, 1.1f, 0.1f)]
+ [InlineData(1.0f, 2.0f, 1.0f)]
+ public static void EqualOrGreaterWithTolerance_Float(float first, float second, float tolerance) =>
+ first.MustBeGreaterThanOrApproximately(second, tolerance).Should().Be(first);
+
+ [Theory]
+ [InlineData(5.0, 5.3, 0.1)]
+ [InlineData(100.0, 100.5, 0.1)]
+ [InlineData(-20.0, -19.8, 0.1)]
+ [InlineData(0.0001, 0.0003, 0.00005)]
+ public static void NotGreaterThanOrApproximately_Double(double value, double other, double tolerance)
+ {
+ var act = () => value.MustBeGreaterThanOrApproximately(other, tolerance, nameof(value));
+
+ var exceptionAssertion = act.Should().Throw().Which;
+ exceptionAssertion.Message.Should().Contain(
+ $"{nameof(value)} must be greater than or approximately equal to {other} with a tolerance of {tolerance}, but it actually is {value}."
+ );
+ exceptionAssertion.ParamName.Should().BeSameAs(nameof(value));
+ }
+
+ [Theory]
+ [InlineData(5.0f, 5.3f, 0.1f)]
+ [InlineData(100.0f, 100.5f, 0.1f)]
+ [InlineData(-20.0f, -19.8f, 0.1f)]
+ [InlineData(0.0001f, 0.0003f, 0.00005f)]
+ public static void NotGreaterThanOrApproximately_Float(float value, float other, float tolerance)
+ {
+ var act = () => value.MustBeGreaterThanOrApproximately(other, tolerance, nameof(value));
+
+ var exceptionAssertion = act.Should().Throw().Which;
+ exceptionAssertion.Message.Should().Contain(
+ $"{nameof(value)} must be greater than or approximately equal to {other} with a tolerance of {tolerance}, but it actually is {value}."
+ );
+ exceptionAssertion.ParamName.Should().BeSameAs(nameof(value));
+ }
+
+ [Fact]
+ public static void DefaultTolerance_Double()
+ {
+ // Should pass - difference is 0.00005 which is less than default tolerance 0.0001
+ const double value = 1.00005;
+ value.MustBeGreaterThanOrApproximately(1.0).Should().Be(value);
+
+ // Should throw - difference is 0.0002 which is greater than default tolerance 0.0001
+ Action act = () => 0.9998.MustBeGreaterThanOrApproximately(1.0, "parameter");
+ act.Should().Throw()
+ .WithParameterName("parameter");
+ }
+
+ [Fact]
+ public static void DefaultTolerance_Float()
+ {
+ // Should pass - difference is 0.00005f which is less than default tolerance 0.0001f
+ const float value = 1.00005f;
+ value.MustBeGreaterThanOrApproximately(1.0f).Should().Be(value);
+
+ // Should throw - difference is 0.0002f which is greater than default tolerance 0.0001f
+ Action act = () => 0.9998f.MustBeGreaterThanOrApproximately(1.0f, "parameter");
+ act.Should().Throw()
+ .WithParameterName("parameter");
+ }
+
+ [Fact]
+ public static void CustomException_Double() =>
+ Test.CustomException(
+ 5.0,
+ 5.3,
+ (x, y, exceptionFactory) => x.MustBeGreaterThanOrApproximately(y, exceptionFactory)
+ );
+
+ [Fact]
+ public static void CustomExceptionWithTolerance_Double() =>
+ Test.CustomException(
+ 5.0,
+ 5.5,
+ 0.1,
+ (x, y, z, exceptionFactory) => x.MustBeGreaterThanOrApproximately(y, z, exceptionFactory)
+ );
+
+ [Fact]
+ public static void CustomException_Float() =>
+ Test.CustomException(
+ 5.0f,
+ 5.3f,
+ (x, y, exceptionFactory) => x.MustBeGreaterThanOrApproximately(y, exceptionFactory)
+ );
+
+ [Fact]
+ public static void CustomExceptionWithTolerance_Float() =>
+ Test.CustomException(
+ 5.0f,
+ 5.5f,
+ 0.1f,
+ (x, y, z, exceptionFactory) => x.MustBeGreaterThanOrApproximately(y, z, exceptionFactory)
+ );
+
+ [Fact]
+ public static void NoCustomExceptionThrown_Double() =>
+ 5.2.MustBeGreaterThanOrApproximately(5.1, (_, _) => null).Should().Be(5.2);
+
+ [Fact]
+ public static void NoCustomExceptionThrownWithTolerance_Double() =>
+ 5.2.MustBeGreaterThanOrApproximately(5.0, 0.1, (_, _, _) => null).Should().Be(5.2);
+
+ [Fact]
+ public static void NoCustomExceptionThrown_Float() =>
+ 5.2f.MustBeGreaterThanOrApproximately(5.0f, (_, _) => null).Should().Be(5.2f);
+
+ [Fact]
+ public static void NoCustomExceptionThrownWithTolerance_Float() =>
+ 5.2f.MustBeGreaterThanOrApproximately(5.0f, 0.1f, (_, _, _) => null).Should().Be(5.2f);
+
+ [Fact]
+ public static void CustomMessage_Double() =>
+ Test.CustomMessage(
+ message => 100.0.MustBeGreaterThanOrApproximately(101.0, 0.5, message: message)
+ );
+
+ [Fact]
+ public static void CustomMessage_Float() =>
+ Test.CustomMessage(
+ message => 100.0f.MustBeGreaterThanOrApproximately(101.0f, 0.5f, message: message)
+ );
+
+ [Fact]
+ public static void CallerArgumentExpression_Double()
+ {
+ const double seventyEightO1 = 78.1;
+
+ var act = () => seventyEightO1.MustBeGreaterThanOrApproximately(79.0);
+
+ act.Should().Throw()
+ .WithParameterName(nameof(seventyEightO1));
+ }
+
+ [Fact]
+ public static void CallerArgumentExpressionWithTolerance_Double()
+ {
+ const double pi = 3.14159;
+
+ var act = () => pi.MustBeGreaterThanOrApproximately(3.5, 0.1);
+
+ act.Should().Throw()
+ .WithParameterName(nameof(pi));
+ }
+
+ [Fact]
+ public static void CallerArgumentExpression_Float()
+ {
+ const float seventyEightO1 = 78.1f;
+
+ var act = () => seventyEightO1.MustBeGreaterThanOrApproximately(79.0f);
+
+ act.Should().Throw()
+ .WithParameterName(nameof(seventyEightO1));
+ }
+
+ [Fact]
+ public static void CallerArgumentExpressionWithTolerance_Float()
+ {
+ const float pi = 3.14159f;
+
+ var act = () => pi.MustBeGreaterThanOrApproximately(3.5f, 0.1f);
+
+ act.Should().Throw()
+ .WithParameterName(nameof(pi));
+ }
+
+#if NET8_0
+ [Theory]
+ [InlineData(15.91, 15.9, 0.1)]
+ [InlineData(24.4999, 24.45, 0.0001)]
+ [InlineData(-3.12, -3.2, 0.001)]
+ [InlineData(2.369, 2.37, 0.05)]
+ [InlineData(15.0, 14.0, 0.1)] // Greater than case
+ [InlineData(14.95, 15.0, 0.1)] // Approximately equal case
+ public static void EqualOrGreaterWithCustomTolerance_GenericDouble(double first, double second, double tolerance) =>
+ first.MustBeGreaterThanOrApproximately(second, tolerance).Should().Be(first);
+
+ [Theory]
+ [InlineData(10, 5, 1)] // Greater than case
+ [InlineData(5, 5, 1)] // Equal case
+ [InlineData(5, 6, 1)] // Approximately equal case
+ [InlineData(5, 7, 2)] // Not greater than or approximately equal case
+ public static void EqualOrGreaterWithCustomTolerance_GenericInt32(int first, int second, int tolerance) =>
+ first.MustBeGreaterThanOrApproximately(second, tolerance).Should().Be(first);
+
+ [Theory]
+ [InlineData(10L, 5L, 1L)] // Greater than case
+ [InlineData(5L, 5L, 1L)] // Equal case
+ [InlineData(5L, 6L, 1L)] // Approximately equal case
+ [InlineData(4L, 7L, 3L)] // Not greater than or approximately equal case
+ public static void EqualOrGreaterWithCustomTolerance_GenericInt64(long first, long second, long tolerance) =>
+ first.MustBeGreaterThanOrApproximately(second, tolerance).Should().Be(first);
+
+ [Theory]
+ [MemberData(nameof(DecimalTestData))]
+ public static void GenericDecimalWithCustomTolerance(
+ decimal first,
+ decimal second,
+ decimal tolerance
+ ) =>
+ first.MustBeGreaterThanOrApproximately(second, tolerance).Should().Be(first);
+
+ public static TheoryData DecimalTestData() => new ()
+ {
+ { 1.3m, 1.1m, 0.1m }, // Greater than case
+ { 1.1m, 1.1m, 0.1m }, // Equal case
+ { 1.0m, 1.1m, 0.2m }, // Approximately equal case
+ { 1.292m, 1.3m, 0.1m }, // Not greater than or approximately equal case
+ };
+
+ [Theory]
+ [InlineData(5.0, 5.3, 0.1)]
+ [InlineData(100.0, 100.5, 0.1)]
+ [InlineData(-20.0, -19.8, 0.1)]
+ [InlineData(0.0001, 0.0003, 0.00005)]
+ public static void NotGreaterThanOrApproximately_Generic(double value, double other, double tolerance)
+ {
+ var act = () => value.MustBeGreaterThanOrApproximately(other, tolerance, nameof(value));
+
+ var exceptionAssertion = act.Should().Throw().Which;
+ exceptionAssertion.Message.Should().Contain(
+ $"{nameof(value)} must be greater than or approximately equal to {other} with a tolerance of {tolerance}, but it actually is {value}."
+ );
+ exceptionAssertion.ParamName.Should().BeSameAs(nameof(value));
+ }
+
+ [Fact]
+ public static void CustomExceptionWithTolerance_Generic() =>
+ Test.CustomException(
+ 5.0,
+ 5.5,
+ 0.1,
+ (x, y, t, exceptionFactory) => x.MustBeGreaterThanOrApproximately(y, t, exceptionFactory)
+ );
+
+ [Fact]
+ public static void NoCustomExceptionThrown_Generic() =>
+ 5.2.MustBeGreaterThanOrApproximately(5.0, 0.1, (_, _, _) => null).Should().Be(5.2);
+
+ [Fact]
+ public static void CustomMessage_Generic() =>
+ Test.CustomMessage(
+ message => 100.0.MustBeGreaterThanOrApproximately(101.0, 0.5, message: message)
+ );
+
+ [Fact]
+ public static void CallerArgumentExpressionWithTolerance_Generic()
+ {
+ const double seventyEightO1 = 78.1;
+
+ var act = () => seventyEightO1.MustBeGreaterThanOrApproximately(79.0, 0.5);
+
+ act.Should().Throw()
+ .WithParameterName(nameof(seventyEightO1));
+ }
+#endif
+}
diff --git a/Code/Light.GuardClauses.Tests/ComparableAssertions/MustBeLessThanOrApproximatelyTests.cs b/Code/Light.GuardClauses.Tests/ComparableAssertions/MustBeLessThanOrApproximatelyTests.cs
new file mode 100644
index 00000000..9eace403
--- /dev/null
+++ b/Code/Light.GuardClauses.Tests/ComparableAssertions/MustBeLessThanOrApproximatelyTests.cs
@@ -0,0 +1,295 @@
+using System;
+using FluentAssertions;
+using Xunit;
+
+namespace Light.GuardClauses.Tests.ComparableAssertions;
+
+public static class MustBeLessThanOrApproximatelyTests
+{
+ [Theory]
+ [InlineData(17.3, 17.4)]
+ [InlineData(19.9999999, 20.0)]
+ [InlineData(-5.5, -5.49998)]
+ [InlineData(0.0001, 0.0001)]
+ public static void EqualOrLess_Double(double first, double second) =>
+ first.MustBeLessThanOrApproximately(second).Should().Be(first);
+
+ [Theory]
+ [InlineData(15.9, 15.91, 0.1)]
+ [InlineData(24.45, 24.49999, 0.0001)]
+ [InlineData(-3.2, -3.12, 0.001)]
+ [InlineData(2.37, 2.369, 0.05)]
+ public static void EqualOrLessWithTolerance_Double(double first, double second, double tolerance) =>
+ first.MustBeLessThanOrApproximately(second, tolerance).Should().Be(first);
+
+ [Theory]
+ [InlineData(100.2f, 100.225f)]
+ [InlineData(-5.900005f, -5.9f)]
+ [InlineData(-0.02f, 0f)]
+ [InlineData(0f, 0.00001f)]
+ public static void EqualOrLess_Float(float first, float second) =>
+ first.MustBeLessThanOrApproximately(second).Should().Be(first);
+
+ [Theory]
+ [InlineData(1.0f, 2.0f, 0.1f)]
+ [InlineData(1.0f, 1.0f, 0.1f)]
+ [InlineData(1.1f, 1.01f, 0.1f)]
+ [InlineData(1.0f, 2.0f, 1.0f)]
+ public static void EqualOrLessWithTolerance_Float(float first, float second, float tolerance) =>
+ first.MustBeLessThanOrApproximately(second, tolerance).Should().Be(first);
+
+ [Theory]
+ [InlineData(5.3, 5.0, 0.1)]
+ [InlineData(100.5, 100.0, 0.1)]
+ [InlineData(-19.8, -20.0, 0.1)]
+ [InlineData(0.0003, 0.0001, 0.00005)]
+ public static void NotLessThanOrApproximately_Double(double value, double other, double tolerance)
+ {
+ var act = () => value.MustBeLessThanOrApproximately(other, tolerance, nameof(value));
+
+ var exceptionAssertion = act.Should().Throw().Which;
+ exceptionAssertion.Message.Should().Contain(
+ $"{nameof(value)} must be less than or approximately equal to {other} with a tolerance of {tolerance}, but it actually is {value}."
+ );
+ exceptionAssertion.ParamName.Should().BeSameAs(nameof(value));
+ }
+
+ [Theory]
+ [InlineData(5.3f, 5.0f, 0.1f)]
+ [InlineData(100.5f, 100.0f, 0.1f)]
+ [InlineData(-19.8f, -20.0f, 0.1f)]
+ [InlineData(0.0003f, 0.0001f, 0.00005f)]
+ public static void NotLessThanOrApproximately_Float(float value, float other, float tolerance)
+ {
+ var act = () => value.MustBeLessThanOrApproximately(other, tolerance, nameof(value));
+
+ var exceptionAssertion = act.Should().Throw().Which;
+ exceptionAssertion.Message.Should().Contain(
+ $"{nameof(value)} must be less than or approximately equal to {other} with a tolerance of {tolerance}, but it actually is {value}."
+ );
+ exceptionAssertion.ParamName.Should().BeSameAs(nameof(value));
+ }
+
+ [Fact]
+ public static void DefaultTolerance_Double()
+ {
+ // Should pass - difference is 0.00005 which is less than default tolerance 0.0001
+ const double value = 1.0;
+ value.MustBeLessThanOrApproximately(1.00005).Should().Be(value);
+
+ // Should throw - difference is 0.0002 which is greater than default tolerance 0.0001
+ Action act = () => 1.0002.MustBeLessThanOrApproximately(1.0, "parameter");
+ act.Should().Throw()
+ .WithParameterName("parameter");
+ }
+
+ [Fact]
+ public static void DefaultTolerance_Float()
+ {
+ // Should pass - difference is 0.00005f which is less than default tolerance 0.0001f
+ const float value = 1.0f;
+ value.MustBeLessThanOrApproximately(1.00005f).Should().Be(value);
+
+ // Should throw - difference is 0.0002f which is greater than default tolerance 0.0001f
+ Action act = () => 1.0002f.MustBeLessThanOrApproximately(1.0f, "parameter");
+ act.Should().Throw()
+ .WithParameterName("parameter");
+ }
+
+ [Fact]
+ public static void CustomException_Double() =>
+ Test.CustomException(
+ 5.3,
+ 5.0,
+ (x, y, exceptionFactory) => x.MustBeLessThanOrApproximately(y, exceptionFactory)
+ );
+
+ [Fact]
+ public static void CustomExceptionWithTolerance_Double() =>
+ Test.CustomException(
+ 5.5,
+ 5.0,
+ 0.1,
+ (x, y, z, exceptionFactory) => x.MustBeLessThanOrApproximately(y, z, exceptionFactory)
+ );
+
+ [Fact]
+ public static void CustomException_Float() =>
+ Test.CustomException(
+ 5.3f,
+ 5.0f,
+ (x, y, exceptionFactory) => x.MustBeLessThanOrApproximately(y, exceptionFactory)
+ );
+
+ [Fact]
+ public static void CustomExceptionWithTolerance_Float() =>
+ Test.CustomException(
+ 5.5f,
+ 5.0f,
+ 0.1f,
+ (x, y, z, exceptionFactory) => x.MustBeLessThanOrApproximately(y, z, exceptionFactory)
+ );
+
+ [Fact]
+ public static void NoCustomExceptionThrown_Double() =>
+ 5.1.MustBeLessThanOrApproximately(5.2, (_, _) => null).Should().Be(5.1);
+
+ [Fact]
+ public static void NoCustomExceptionThrownWithTolerance_Double() =>
+ 5.0.MustBeLessThanOrApproximately(5.2, 0.1, (_, _, _) => null).Should().Be(5.0);
+
+ [Fact]
+ public static void NoCustomExceptionThrown_Float() =>
+ 5.0f.MustBeLessThanOrApproximately(5.2f, (_, _) => null).Should().Be(5.0f);
+
+ [Fact]
+ public static void NoCustomExceptionThrownWithTolerance_Float() =>
+ 5.0f.MustBeLessThanOrApproximately(5.2f, 0.1f, (_, _, _) => null).Should().Be(5.0f);
+
+ [Fact]
+ public static void CustomMessage_Double() =>
+ Test.CustomMessage(
+ message => 101.0.MustBeLessThanOrApproximately(100.0, 0.5, message: message)
+ );
+
+ [Fact]
+ public static void CustomMessage_Float() =>
+ Test.CustomMessage(
+ message => 101.0f.MustBeLessThanOrApproximately(100.0f, 0.5f, message: message)
+ );
+
+ [Fact]
+ public static void CallerArgumentExpression_Double()
+ {
+ const double seventyNinePoint0 = 79.0;
+
+ var act = () => seventyNinePoint0.MustBeLessThanOrApproximately(78.1);
+
+ act.Should().Throw()
+ .WithParameterName(nameof(seventyNinePoint0));
+ }
+
+ [Fact]
+ public static void CallerArgumentExpressionWithTolerance_Double()
+ {
+ const double threePointFive = 3.5;
+
+ var act = () => threePointFive.MustBeLessThanOrApproximately(3.14159, 0.1);
+
+ act.Should().Throw()
+ .WithParameterName(nameof(threePointFive));
+ }
+
+ [Fact]
+ public static void CallerArgumentExpression_Float()
+ {
+ const float seventyNinePoint0 = 79.0f;
+
+ var act = () => seventyNinePoint0.MustBeLessThanOrApproximately(78.1f);
+
+ act.Should().Throw()
+ .WithParameterName(nameof(seventyNinePoint0));
+ }
+
+ [Fact]
+ public static void CallerArgumentExpressionWithTolerance_Float()
+ {
+ const float threePointFive = 3.5f;
+
+ var act = () => threePointFive.MustBeLessThanOrApproximately(3.14159f, 0.1f);
+
+ act.Should().Throw()
+ .WithParameterName(nameof(threePointFive));
+ }
+
+#if NET8_0
+ [Theory]
+ [InlineData(15.9, 15.91, 0.1)]
+ [InlineData(24.45, 24.4999, 0.0001)]
+ [InlineData(-3.2, -3.12, 0.001)]
+ [InlineData(2.37, 2.369, 0.05)]
+ [InlineData(14.0, 15.0, 0.1)] // Less than case
+ [InlineData(15.0, 14.95, 0.1)] // Approximately equal case
+ public static void EqualOrLessWithCustomTolerance_GenericDouble(double first, double second, double tolerance) =>
+ first.MustBeLessThanOrApproximately(second, tolerance).Should().Be(first);
+
+ [Theory]
+ [InlineData(5, 10, 1)] // Less than case
+ [InlineData(5, 5, 1)] // Equal case
+ [InlineData(6, 5, 1)] // Approximately equal case
+ [InlineData(7, 5, 2)] // Not less than or approximately equal case
+ public static void EqualOrLessWithCustomTolerance_GenericInt32(int first, int second, int tolerance) =>
+ first.MustBeLessThanOrApproximately(second, tolerance).Should().Be(first);
+
+ [Theory]
+ [InlineData(5L, 10L, 1L)] // Less than case
+ [InlineData(5L, 5L, 1L)] // Equal case
+ [InlineData(6L, 5L, 1L)] // Approximately equal case
+ [InlineData(7L, 4L, 3L)] // Not less than or approximately equal case
+ public static void EqualOrLessWithCustomTolerance_GenericInt64(long first, long second, long tolerance) =>
+ first.MustBeLessThanOrApproximately(second, tolerance).Should().Be(first);
+
+ [Theory]
+ [MemberData(nameof(DecimalTestData))]
+ public static void GenericDecimalWithCustomTolerance(
+ decimal first,
+ decimal second,
+ decimal tolerance
+ ) =>
+ first.MustBeLessThanOrApproximately(second, tolerance).Should().Be(first);
+
+ public static TheoryData DecimalTestData() => new ()
+ {
+ { 1.1m, 1.3m, 0.1m }, // Less than case
+ { 1.1m, 1.1m, 0.1m }, // Equal case
+ { 1.1m, 1.0m, 0.2m }, // Approximately equal case
+ { 1.3m, 1.292m, 0.1m }, // Not less than or approximately equal case
+ };
+
+ [Theory]
+ [InlineData(5.3, 5.0, 0.1)]
+ [InlineData(100.5, 100.0, 0.1)]
+ [InlineData(-19.8, -20.0, 0.1)]
+ [InlineData(0.0003, 0.0001, 0.00005)]
+ public static void NotLessThanOrApproximately_Generic(double value, double other, double tolerance)
+ {
+ var act = () => value.MustBeLessThanOrApproximately(other, tolerance, nameof(value));
+
+ var exceptionAssertion = act.Should().Throw().Which;
+ exceptionAssertion.Message.Should().Contain(
+ $"{nameof(value)} must be less than or approximately equal to {other} with a tolerance of {tolerance}, but it actually is {value}."
+ );
+ exceptionAssertion.ParamName.Should().BeSameAs(nameof(value));
+ }
+
+ [Fact]
+ public static void CustomExceptionWithTolerance_Generic() =>
+ Test.CustomException(
+ 5.5,
+ 5.0,
+ 0.1,
+ (x, y, t, exceptionFactory) => x.MustBeLessThanOrApproximately(y, t, exceptionFactory)
+ );
+
+ [Fact]
+ public static void NoCustomExceptionThrown_Generic() =>
+ 5.0.MustBeLessThanOrApproximately(5.2, 0.1, (_, _, _) => null).Should().Be(5.0);
+
+ [Fact]
+ public static void CustomMessage_Generic() =>
+ Test.CustomMessage(
+ message => 101.0.MustBeLessThanOrApproximately(100.0, 0.5, message: message)
+ );
+
+ [Fact]
+ public static void CallerArgumentExpressionWithTolerance_Generic()
+ {
+ const double seventyNinePoint0 = 79.0;
+
+ var act = () => seventyNinePoint0.MustBeLessThanOrApproximately(78.1, 0.5);
+
+ act.Should().Throw()
+ .WithParameterName(nameof(seventyNinePoint0));
+ }
+#endif
+}
\ No newline at end of file
diff --git a/Code/Light.GuardClauses.Tests/ComparableAssertions/MustNotBeApproximatelyTests.cs b/Code/Light.GuardClauses.Tests/ComparableAssertions/MustNotBeApproximatelyTests.cs
new file mode 100644
index 00000000..da9e7ea6
--- /dev/null
+++ b/Code/Light.GuardClauses.Tests/ComparableAssertions/MustNotBeApproximatelyTests.cs
@@ -0,0 +1,238 @@
+using System;
+using FluentAssertions;
+using Xunit;
+
+namespace Light.GuardClauses.Tests.ComparableAssertions;
+
+public static class MustNotBeApproximatelyTests
+{
+ [Theory]
+ [InlineData(5.3, 5.0, 0.2)]
+ [InlineData(10.4, 10.3, 0.01)]
+ [InlineData(3.15, 3.14, 0.001)]
+ [InlineData(-42.002, -42.0001, 0.001)]
+ public static void ValuesNotApproximatelyEqual_Double(double value, double other, double tolerance) =>
+ value.MustNotBeApproximately(other, tolerance).Should().Be(value);
+
+ [Theory]
+ [InlineData(5.3f, 5.0f, 0.2f)]
+ [InlineData(10.4f, 10.3f, 0.01f)]
+ [InlineData(3.15f, 3.14f, 0.001f)]
+ [InlineData(-42.002f, -42.0001f, 0.001f)]
+ public static void ValuesNotApproximatelyEqual_Float(float value, float other, float tolerance) =>
+ value.MustNotBeApproximately(other, tolerance).Should().Be(value);
+
+ [Theory]
+ [InlineData(5.0, 5.05, 0.1)]
+ [InlineData(100.0, 99.95, 0.1)]
+ [InlineData(-20.0, -20.05, 0.1)]
+ [InlineData(0.0001, 0.00015, 0.0001)]
+ public static void ValuesApproximatelyEqual_Double(double value, double other, double tolerance)
+ {
+ var act = () => value.MustNotBeApproximately(other, tolerance, nameof(value));
+
+ var exceptionAssertion = act.Should().Throw().Which;
+ exceptionAssertion.Message.Should().Contain(
+ $"{nameof(value)} must not be approximately equal to {other} with a tolerance of {tolerance}, but it actually is {value}."
+ );
+ exceptionAssertion.ParamName.Should().BeSameAs(nameof(value));
+ }
+
+ [Theory]
+ [InlineData(5.0f, 5.05f, 0.1f)]
+ [InlineData(100.0f, 99.95f, 0.1f)]
+ [InlineData(-20.0f, -20.05f, 0.1f)]
+ [InlineData(0.0001f, 0.00015f, 0.0001f)]
+ public static void ValuesApproximatelyEqual_Float(float value, float other, float tolerance)
+ {
+ var act = () => value.MustNotBeApproximately(other, tolerance, nameof(value));
+
+ var exceptionAssertion = act.Should().Throw().Which;
+ exceptionAssertion.Message.Should().Contain(
+ $"{nameof(value)} must not be approximately equal to {other} with a tolerance of {tolerance}, but it actually is {value}."
+ );
+ exceptionAssertion.ParamName.Should().BeSameAs(nameof(value));
+ }
+
+ [Fact]
+ public static void DefaultTolerance_Double()
+ {
+ // Should throw - difference is 0.00005 which is less than default tolerance 0.0001
+ const double value = 1.00005;
+ Action act = () => value.MustNotBeApproximately(1.0, "parameter");
+ act.Should().Throw()
+ .WithParameterName("parameter");
+
+ // Should pass - difference is 0.0002 which is greater than default tolerance 0.0001
+ const double value2 = 1.0002;
+ value2.MustNotBeApproximately(1.0).Should().Be(value2);
+ }
+
+ [Fact]
+ public static void DefaultTolerance_Float()
+ {
+ // Should throw - difference is 0.00005f which is less than default tolerance 0.0001f
+ const float value = 1.00005f;
+ Action act = () => value.MustNotBeApproximately(1.0f, "parameter");
+ act.Should().Throw()
+ .WithParameterName("parameter");
+
+ // Should pass - difference is 0.0002f which is greater than default tolerance 0.0001f
+ const float value2 = 1.0002f;
+ value2.MustNotBeApproximately(1.0f).Should().Be(value2);
+ }
+
+ [Fact]
+ public static void CustomException_Double() =>
+ Test.CustomException(
+ 5.0,
+ 5.0000000001,
+ (x, y, exceptionFactory) => x.MustNotBeApproximately(y, exceptionFactory)
+ );
+
+ [Fact]
+ public static void CustomExceptionWithTolerance_Double() =>
+ Test.CustomException(
+ 5.0,
+ 5.05,
+ 0.1,
+ (x, y, z, exceptionFactory) => x.MustNotBeApproximately(y, z, exceptionFactory)
+ );
+
+ [Fact]
+ public static void CustomException_Float() =>
+ Test.CustomException(
+ 5.0f,
+ 5.000001f,
+ (x, y, exceptionFactory) => x.MustNotBeApproximately(y, exceptionFactory)
+ );
+
+ [Fact]
+ public static void CustomExceptionWithTolerance_Float() =>
+ Test.CustomException(
+ 5.0f,
+ 5.05f,
+ 0.1f,
+ (x, y, z, exceptionFactory) => x.MustNotBeApproximately(y, z, exceptionFactory)
+ );
+
+ [Fact]
+ public static void NoCustomExceptionThrown_Double() =>
+ 5.2.MustNotBeApproximately(5.0, 0.1, (_, _, _) => null).Should().Be(5.2);
+
+ [Fact]
+ public static void NoCustomExceptionThrown_Float() =>
+ 5.2f.MustNotBeApproximately(5.0f, 0.1f, (_, _, _) => null).Should().Be(5.2f);
+
+ [Fact]
+ public static void CustomMessage_Double() =>
+ Test.CustomMessage(
+ message => 100.0.MustNotBeApproximately(100.05, 0.1, message: message)
+ );
+
+ [Fact]
+ public static void CustomMessage_Float() =>
+ Test.CustomMessage(
+ message => 100.0f.MustNotBeApproximately(100.05f, 0.1f, message: message)
+ );
+
+ [Fact]
+ public static void CallerArgumentExpression_Double()
+ {
+ const double seventyEightO1 = 78.1;
+
+ var act = () => seventyEightO1.MustNotBeApproximately(78.099999);
+
+ act.Should().Throw()
+ .WithParameterName(nameof(seventyEightO1));
+ }
+
+ [Fact]
+ public static void CallerArgumentExpressionWithTolerance_Double()
+ {
+ const double pi = 3.14159;
+
+ var act = () => pi.MustNotBeApproximately(3.14, 0.01);
+
+ act.Should().Throw()
+ .WithParameterName(nameof(pi));
+ }
+
+ [Fact]
+ public static void CallerArgumentExpression_Float()
+ {
+ const float seventyEightO1 = 78.1f;
+
+ var act = () => seventyEightO1.MustNotBeApproximately(78.100005f);
+
+ act.Should().Throw()
+ .WithParameterName(nameof(seventyEightO1));
+ }
+
+ [Fact]
+ public static void CallerArgumentExpressionWithTolerance_Float()
+ {
+ const float pi = 3.14159f;
+
+ var act = () => pi.MustNotBeApproximately(3.14f, 0.01f);
+
+ act.Should().Throw()
+ .WithParameterName(nameof(pi));
+ }
+
+#if NET8_0
+ [Theory]
+ [InlineData(5.3, 5.0, 0.2)]
+ [InlineData(10.4, 10.3, 0.01)]
+ [InlineData(3.15, 3.14, 0.001)]
+ [InlineData(-42.002, -42.0001, 0.001)]
+ public static void ValuesNotApproximatelyEqual_Generic(double value, double other, double tolerance) =>
+ value.MustNotBeApproximately(other, tolerance).Should().Be(value);
+
+ [Theory]
+ [InlineData(5.0, 5.05, 0.1)]
+ [InlineData(100.0, 99.95, 0.1)]
+ [InlineData(-20.0, -20.05, 0.1)]
+ [InlineData(0.0001, 0.00015, 0.0001)]
+ public static void ValuesApproximatelyEqual_Generic(double value, double other, double tolerance)
+ {
+ var act = () => value.MustNotBeApproximately(other, tolerance, nameof(value));
+
+ var exceptionAssertion = act.Should().Throw().Which;
+ exceptionAssertion.Message.Should().Contain(
+ $"{nameof(value)} must not be approximately equal to {other} with a tolerance of {tolerance}, but it actually is {value}."
+ );
+ exceptionAssertion.ParamName.Should().BeSameAs(nameof(value));
+ }
+
+ [Fact]
+ public static void CustomExceptionWithTolerance_Generic() =>
+ Test.CustomException(
+ 5.0,
+ 5.05,
+ 0.1,
+ (x, y, t, exceptionFactory) => x.MustNotBeApproximately(y, t, exceptionFactory)
+ );
+
+ [Fact]
+ public static void NoCustomExceptionThrown_Generic() =>
+ 5.2.MustNotBeApproximately(5.0, 0.1, (_, _, _) => null).Should().Be(5.2);
+
+ [Fact]
+ public static void CustomMessage_Generic() =>
+ Test.CustomMessage(
+ message => 100.0.MustNotBeApproximately(100.05, 0.1, message: message)
+ );
+
+ [Fact]
+ public static void CallerArgumentExpressionWithTolerance_Generic()
+ {
+ const double seventyEightO1 = 78.1;
+
+ var act = () => seventyEightO1.MustNotBeApproximately(78.0, 0.2);
+
+ act.Should().Throw()
+ .WithParameterName(nameof(seventyEightO1));
+ }
+#endif
+}
\ No newline at end of file
diff --git a/Code/Light.GuardClauses.Tests/ExecuteReadOnlySpanAssertion.cs b/Code/Light.GuardClauses.Tests/ExecuteReadOnlySpanAssertion.cs
new file mode 100644
index 00000000..725c9eca
--- /dev/null
+++ b/Code/Light.GuardClauses.Tests/ExecuteReadOnlySpanAssertion.cs
@@ -0,0 +1,27 @@
+#nullable enable
+using System;
+using Light.GuardClauses.ExceptionFactory;
+
+namespace Light.GuardClauses.Tests;
+
+public delegate void ExecuteReadOnlySpanAssertion(
+ ReadOnlySpan span,
+ ReadOnlySpanExceptionFactory exceptionFactory
+);
+
+public delegate void ExecuteSpanAssertion(
+ Span span,
+ ReadOnlySpanExceptionFactory exceptionFactory
+);
+
+public delegate void ExecuteReadOnlySpanAssertion(
+ ReadOnlySpan span,
+ TValue additionalValue,
+ ReadOnlySpanExceptionFactory exceptionFactory
+);
+
+public delegate void ExecuteSpanAssertion(
+ Span span,
+ TValue additionalValue,
+ ReadOnlySpanExceptionFactory exceptionFactory
+);
diff --git a/Code/Light.GuardClauses.Tests/StringAssertions/InvalidEmailAddresses.cs b/Code/Light.GuardClauses.Tests/StringAssertions/InvalidEmailAddresses.cs
new file mode 100644
index 00000000..a31d0416
--- /dev/null
+++ b/Code/Light.GuardClauses.Tests/StringAssertions/InvalidEmailAddresses.cs
@@ -0,0 +1,33 @@
+using Xunit;
+
+namespace Light.GuardClauses.Tests.StringAssertions;
+
+public class InvalidEmailAddresses : TheoryData
+{
+ public InvalidEmailAddresses()
+ {
+ Add("plainaddress");
+ Add("#@%^%#$@#$@#.com");
+ Add("@domain.com");
+ Add("Joe Smith ");
+ Add("email.domain.com");
+ Add("email@domain@domain.com");
+ Add(".email@domain.com");
+ Add("email.@domain.com");
+ Add("email..email@domain.com");
+ Add("email@domain.com (Joe Smith)");
+ Add("email@domain");
+ Add("email@-domain.com");
+ Add("email@111.222.333.44444");
+ Add("email@domain..com");
+ Add("email@256.256.256.256");
+ }
+}
+
+public sealed class InvalidEmailAddressesWithNull : InvalidEmailAddresses
+{
+ public InvalidEmailAddressesWithNull()
+ {
+ Add(null);
+ }
+}
diff --git a/Code/Light.GuardClauses.Tests/StringAssertions/IsEmailAddressTest.cs b/Code/Light.GuardClauses.Tests/StringAssertions/IsEmailAddressTest.cs
deleted file mode 100644
index 9e0b9944..00000000
--- a/Code/Light.GuardClauses.Tests/StringAssertions/IsEmailAddressTest.cs
+++ /dev/null
@@ -1,50 +0,0 @@
-using FluentAssertions;
-using Xunit;
-
-namespace Light.GuardClauses.Tests.StringAssertions;
-
-public sealed class IsEmailAddressTest
-{
- [Theory]
- [InlineData(null)]
- [InlineData("plainaddress")]
- [InlineData("#@%^%#$@#$@#.com")]
- [InlineData("@domain.com")]
- [InlineData("Joe Smith ")]
- [InlineData("email.domain.com")]
- [InlineData("email@domain@domain.com")]
- [InlineData(".email@domain.com")]
- [InlineData("email.@domain.com")]
- [InlineData("email..email@domain.com")]
- [InlineData("あいうえお@domain.com")]
- [InlineData("email@domain.com (Joe Smith)")]
- [InlineData("email@domain")]
- [InlineData("email@-domain.com")]
- [InlineData("email@111.222.333.44444")]
- [InlineData("email@domain..com")]
- public void IsNotValidEmailAddress(string email)
- {
- var isValid = email.IsEmailAddress();
-
- isValid.Should().BeFalse();
- }
-
- [Theory]
- [InlineData("email@domain.com")]
- [InlineData("firstname.lastname@domain.com")]
- [InlineData("email@subdomain.domain.com")]
- [InlineData("firstname+lastname@domain.com")]
- [InlineData("email@123.123.123.123")]
- [InlineData("1234567890@domain.com")]
- [InlineData("email@domain-one.com")]
- [InlineData("_______@domain.com")]
- [InlineData("email@domain.name")]
- [InlineData("email@domain.co.jp")]
- [InlineData("firstname-lastname@domain.com")]
- public void IsValidEmailAddress(string email)
- {
- var isValid = email.IsEmailAddress();
-
- isValid.Should().BeTrue();
- }
-}
\ No newline at end of file
diff --git a/Code/Light.GuardClauses.Tests/StringAssertions/IsEmailAddressTests.cs b/Code/Light.GuardClauses.Tests/StringAssertions/IsEmailAddressTests.cs
new file mode 100644
index 00000000..0cd37508
--- /dev/null
+++ b/Code/Light.GuardClauses.Tests/StringAssertions/IsEmailAddressTests.cs
@@ -0,0 +1,108 @@
+using System;
+using FluentAssertions;
+using Xunit;
+
+namespace Light.GuardClauses.Tests.StringAssertions;
+
+public sealed class IsEmailAddressTests
+{
+ [Theory]
+ [ClassData(typeof(InvalidEmailAddressesWithNull))]
+ public void IsNotValidEmailAddress(string email)
+ {
+ var isValid = email.IsEmailAddress();
+
+ isValid.Should().BeFalse();
+ }
+
+ [Theory]
+ [ClassData(typeof(ValidEmailAddresses))]
+ public void IsValidEmailAddress(string email)
+ {
+ var isValid = email.IsEmailAddress();
+
+ isValid.Should().BeTrue();
+ }
+
+#if NET8_0
+ [Theory]
+ [ClassData(typeof(InvalidEmailAddressesWithNull))]
+ public void IsNotValidEmailAddress_ReadOnlySpan(string email)
+ {
+ var span = new ReadOnlySpan(email?.ToCharArray() ?? []);
+ var isValid = span.IsEmailAddress();
+
+ isValid.Should().BeFalse();
+ }
+
+ [Theory]
+ [ClassData(typeof(ValidEmailAddresses))]
+ public void IsValidEmailAddress_ReadOnlySpan(string email)
+ {
+ var span = email.AsSpan();
+ var isValid = span.IsEmailAddress();
+
+ isValid.Should().BeTrue();
+ }
+
+ [Theory]
+ [ClassData(typeof(InvalidEmailAddressesWithNull))]
+ public void IsNotValidEmailAddress_Span(string email)
+ {
+ var span = new Span(email?.ToCharArray() ?? []);
+ var isValid = span.IsEmailAddress();
+
+ isValid.Should().BeFalse();
+ }
+
+ [Theory]
+ [ClassData(typeof(ValidEmailAddresses))]
+ public void IsValidEmailAddress_Span(string email)
+ {
+ var span = new Span(email.ToCharArray());
+ var isValid = span.IsEmailAddress();
+
+ isValid.Should().BeTrue();
+ }
+
+ [Theory]
+ [ClassData(typeof(InvalidEmailAddressesWithNull))]
+ public void IsNotValidEmailAddress_Memory(string email)
+ {
+ var memory = email?.ToCharArray().AsMemory() ?? Memory.Empty;
+ var isValid = memory.IsEmailAddress();
+
+ isValid.Should().BeFalse();
+ }
+
+ [Theory]
+ [ClassData(typeof(ValidEmailAddresses))]
+ public void IsValidEmailAddress_Memory(string email)
+ {
+ var memory = email.ToCharArray().AsMemory();
+ var isValid = memory.IsEmailAddress();
+
+ isValid.Should().BeTrue();
+ }
+
+ [Theory]
+ [ClassData(typeof(InvalidEmailAddressesWithNull))]
+ public void IsNotValidEmailAddress_ReadOnlyMemory(string email)
+ {
+ var memory = new ReadOnlyMemory(email?.ToCharArray() ?? []);
+ var isValid = memory.IsEmailAddress();
+
+ isValid.Should().BeFalse();
+ }
+
+ [Theory]
+ [ClassData(typeof(ValidEmailAddresses))]
+ public void IsValidEmailAddress_ReadOnlyMemory(string email)
+ {
+ var memory = new ReadOnlyMemory(email.ToCharArray());
+ var isValid = memory.IsEmailAddress();
+
+ isValid.Should().BeTrue();
+ }
+#endif
+}
diff --git a/Code/Light.GuardClauses.Tests/StringAssertions/IsEmptyOrWhiteSpaceTests.cs b/Code/Light.GuardClauses.Tests/StringAssertions/IsEmptyOrWhiteSpaceTests.cs
new file mode 100644
index 00000000..2048883a
--- /dev/null
+++ b/Code/Light.GuardClauses.Tests/StringAssertions/IsEmptyOrWhiteSpaceTests.cs
@@ -0,0 +1,120 @@
+using System;
+using FluentAssertions;
+using Xunit;
+
+namespace Light.GuardClauses.Tests.StringAssertions;
+
+public static class IsEmptyOrWhiteSpaceTests
+{
+ [Fact]
+ public static void EmptySpan()
+ {
+ var span = new Span([]);
+ span.IsEmptyOrWhiteSpace().Should().BeTrue();
+ }
+
+ [Fact]
+ public static void WhiteSpaceSpan()
+ {
+ var span = new Span(" \t\n".ToCharArray());
+ span.IsEmptyOrWhiteSpace().Should().BeTrue();
+ }
+
+ [Fact]
+ public static void NonWhiteSpaceSpan()
+ {
+ var span = new Span("abc".ToCharArray());
+ span.IsEmptyOrWhiteSpace().Should().BeFalse();
+ }
+
+ [Fact]
+ public static void MixedSpan()
+ {
+ var span = new Span(" a\t".ToCharArray());
+ span.IsEmptyOrWhiteSpace().Should().BeFalse();
+ }
+
+ [Fact]
+ public static void EmptyReadOnlySpan()
+ {
+ var readOnlySpan = string.Empty.AsSpan();
+ readOnlySpan.IsEmptyOrWhiteSpace().Should().BeTrue();
+ }
+
+ [Fact]
+ public static void WhiteSpaceReadOnlySpan()
+ {
+ var readOnlySpan = " \t\n".AsSpan();
+ readOnlySpan.IsEmptyOrWhiteSpace().Should().BeTrue();
+ }
+
+ [Fact]
+ public static void NonWhiteSpaceReadOnlySpan()
+ {
+ var readOnlySpan = "abc".AsSpan();
+ readOnlySpan.IsEmptyOrWhiteSpace().Should().BeFalse();
+ }
+
+ [Fact]
+ public static void MixedReadOnlySpan()
+ {
+ var readOnlySpan = " a\t".AsSpan();
+ readOnlySpan.IsEmptyOrWhiteSpace().Should().BeFalse();
+ }
+
+ [Fact]
+ public static void EmptyMemory()
+ {
+ var memory = new Memory([]);
+ memory.IsEmptyOrWhiteSpace().Should().BeTrue();
+ }
+
+ [Fact]
+ public static void WhiteSpaceMemory()
+ {
+ var memory = new Memory(" \t\n".ToCharArray());
+ memory.IsEmptyOrWhiteSpace().Should().BeTrue();
+ }
+
+ [Fact]
+ public static void NonWhiteSpaceMemory()
+ {
+ var memory = new Memory("abc".ToCharArray());
+ memory.IsEmptyOrWhiteSpace().Should().BeFalse();
+ }
+
+ [Fact]
+ public static void MixedMemory()
+ {
+ var memory = new Memory(" a\t".ToCharArray());
+ memory.IsEmptyOrWhiteSpace().Should().BeFalse();
+ }
+
+ [Fact]
+ public static void EmptyReadOnlyMemory()
+ {
+ var readOnlyMemory = new ReadOnlyMemory([]);
+ readOnlyMemory.IsEmptyOrWhiteSpace().Should().BeTrue();
+ }
+
+ [Fact]
+ public static void WhiteSpaceReadOnlyMemory()
+ {
+ var readOnlyMemory = new ReadOnlyMemory(" \t\n".ToCharArray());
+ readOnlyMemory.IsEmptyOrWhiteSpace().Should().BeTrue();
+ }
+
+ [Fact]
+ public static void NonWhiteSpaceReadOnlyMemory()
+ {
+ var readOnlyMemory = new ReadOnlyMemory("abc".ToCharArray());
+ readOnlyMemory.IsEmptyOrWhiteSpace().Should().BeFalse();
+ }
+
+ [Fact]
+ public static void MixedReadOnlyMemory()
+ {
+ var readOnlyMemory = new ReadOnlyMemory(" a\t".ToCharArray());
+ readOnlyMemory.IsEmptyOrWhiteSpace().Should().BeFalse();
+ }
+}
diff --git a/Code/Light.GuardClauses.Tests/StringAssertions/IsFileExtensionTests.cs b/Code/Light.GuardClauses.Tests/StringAssertions/IsFileExtensionTests.cs
new file mode 100644
index 00000000..ae1bc19c
--- /dev/null
+++ b/Code/Light.GuardClauses.Tests/StringAssertions/IsFileExtensionTests.cs
@@ -0,0 +1,90 @@
+using FluentAssertions;
+using System;
+using Xunit;
+
+namespace Light.GuardClauses.Tests.StringAssertions;
+
+public static class IsFileExtensionTests
+{
+ [Theory]
+ [MemberData(nameof(ValidFileExtensionData))]
+ public static void ValidFileExtensions_String(string extension) =>
+ extension.IsFileExtension().Should().BeTrue();
+
+ [Theory]
+ [MemberData(nameof(ValidFileExtensionData))]
+ public static void ValidFileExtensions_Span(string extension)
+ {
+ var span = extension.ToCharArray().AsSpan();
+ span.IsFileExtension().Should().BeTrue();
+ }
+
+ [Theory]
+ [MemberData(nameof(ValidFileExtensionData))]
+ public static void ValidFileExtensions_Memory(string extension)
+ {
+ var memory = extension.ToCharArray().AsMemory();
+ memory.IsFileExtension().Should().BeTrue();
+ }
+
+ [Theory]
+ [MemberData(nameof(ValidFileExtensionData))]
+ public static void ValidFileExtensions_ReadOnlyMemory(string extension)
+ {
+ var readOnlyMemory = extension.AsMemory();
+ readOnlyMemory.IsFileExtension().Should().BeTrue();
+ }
+
+ [Theory]
+ [MemberData(nameof(InvalidFileExtensionData))]
+ public static void InvalidFileExtensions_String(string extension) =>
+ extension.IsFileExtension().Should().BeFalse();
+
+ [Theory]
+ [MemberData(nameof(InvalidFileExtensionData))]
+ public static void InvalidFileExtensions_Span(string extension)
+ {
+ var span = extension?.ToCharArray() ?? Span.Empty;
+ span.IsFileExtension().Should().BeFalse();
+ }
+
+ [Theory]
+ [MemberData(nameof(InvalidFileExtensionData))]
+ public static void InvalidFileExtensions_Memory(string extension)
+ {
+ var memory = extension?.ToCharArray() ?? Memory.Empty;
+ memory.IsFileExtension().Should().BeFalse();
+ }
+
+ [Theory]
+ [MemberData(nameof(InvalidFileExtensionData))]
+ public static void InvalidFileExtensions_ReadOnlyMemory(string extension)
+ {
+ var readOnlyMemory = extension?.ToCharArray() ?? ReadOnlyMemory.Empty;
+ readOnlyMemory.IsFileExtension().Should().BeFalse();
+ }
+
+ public static TheoryData ValidFileExtensionData() =>
+ [
+ ".txt",
+ ".tar.gz",
+ ".docx",
+ ".config",
+ ];
+
+ public static TheoryData InvalidFileExtensionData() =>
+ [
+ null,
+ string.Empty,
+ "txt", // No leading period
+ ".", // Just a period
+ ".txt!", // Invalid character
+ ".txt ", // Contains space
+ ".doc/", // Invalid character
+ "..", // Just periods
+ "...",
+ "....",
+ ".txt.", // Invalid - ends with period
+ ".docx.",
+ ];
+}
\ No newline at end of file
diff --git a/Code/Light.GuardClauses.Tests/StringAssertions/MustBeEmailAddressTests.cs b/Code/Light.GuardClauses.Tests/StringAssertions/MustBeEmailAddressTests.cs
index 8d75d3a7..f6d13149 100644
--- a/Code/Light.GuardClauses.Tests/StringAssertions/MustBeEmailAddressTests.cs
+++ b/Code/Light.GuardClauses.Tests/StringAssertions/MustBeEmailAddressTests.cs
@@ -9,35 +9,36 @@ namespace Light.GuardClauses.Tests.StringAssertions;
public static class MustBeEmailAddressTests
{
[Theory]
- [InlineData("email@domain.com")]
- [InlineData("email@123.123.123.123")]
- public static void ValidEmailAddress(string emailAddress) => emailAddress.MustBeEmailAddress().Should().BeSameAs(emailAddress);
+ [ClassData(typeof(ValidEmailAddresses))]
+ public static void ValidEmailAddress(string emailAddress) =>
+ emailAddress.MustBeEmailAddress().Should().BeSameAs(emailAddress);
[Theory]
- [InlineData("plainAddress")]
- [InlineData("Joe Smith ")]
+ [ClassData(typeof(InvalidEmailAddresses))]
public static void InvalidEmailAddress(string emailAddress)
{
Action act = () => emailAddress.MustBeEmailAddress();
act.Should().Throw()
- .And.Message.Should().Contain($"emailAddress must be a valid email address, but it actually is \"{emailAddress}\".");
+ .And.Message.Should().Contain(
+ $"emailAddress must be a valid email address, but it actually is \"{emailAddress}\"."
+ );
}
[Theory]
- [InlineData(".email@domain.com")]
- [InlineData("email.@domain.com")]
+ [ClassData(typeof(InvalidEmailAddresses))]
public static void InvalidEmailAddressArgumentName(string emailAddress)
{
Action act = () => emailAddress.MustBeEmailAddress(nameof(emailAddress));
act.Should().Throw()
- .And.Message.Should().Contain($"emailAddress must be a valid email address, but it actually is \"{emailAddress}\".");
+ .And.Message.Should().Contain(
+ $"emailAddress must be a valid email address, but it actually is \"{emailAddress}\"."
+ );
}
[Theory]
- [InlineData("email@domain")]
- [InlineData("email@-domain.com")]
+ [ClassData(typeof(InvalidEmailAddresses))]
public static void InvalidEmailAddressCustomMessage(string emailAddress)
{
const string customMessage = "This email address is not valid";
@@ -55,7 +56,7 @@ public static void InvalidEmailCustomRegex()
{
const string email = "email@domain@domain.com";
- Action act = () => email.MustBeEmailAddress(CustomRegex, "email");
+ Action act = () => email.MustBeEmailAddress(CustomRegex);
act.Should().Throw()
.And.Message.Should().Contain($"email must be a valid email address, but it actually is \"{email}\".");
@@ -82,27 +83,35 @@ public static void NullCustomRegex(string email, Regex regex)
new ()
{
{ null, CustomRegex },
- { "invalidEmailAddress", null }
+ { "invalidEmailAddress", null },
};
[Fact]
public static void CustomException() =>
- Test.CustomException("email.domain.com",
- (input, exceptionFactory) => input.MustBeEmailAddress(exceptionFactory));
+ Test.CustomException(
+ "email.domain.com",
+ (input, exceptionFactory) => input.MustBeEmailAddress(exceptionFactory)
+ );
[Fact]
public static void CustomMessage() =>
- Test.CustomMessage(message => "#@%^%#$@#$@#.com".MustBeEmailAddress(message: message));
+ Test.CustomMessage(
+ message => "#@%^%#$@#$@#.com".MustBeEmailAddress(message: message)
+ );
[Fact]
public static void CustomExceptionCustomRegex() =>
- Test.CustomException("invalidEmailAddress",
- CustomRegex,
- (i, r, exceptionFactory) => i.MustBeEmailAddress(r, exceptionFactory));
+ Test.CustomException(
+ "invalidEmailAddress",
+ CustomRegex,
+ (i, r, exceptionFactory) => i.MustBeEmailAddress(r, exceptionFactory)
+ );
[Fact]
public static void CustomMessageCustomRegex() =>
- Test.CustomMessage(message => "invalidEmailAddress".MustBeEmailAddress(CustomRegex, message: message));
+ Test.CustomMessage(
+ message => "invalidEmailAddress".MustBeEmailAddress(CustomRegex, message: message)
+ );
[Fact]
public static void CallerArgumentExpression()
@@ -114,4 +123,346 @@ public static void CallerArgumentExpression()
act.Should().Throw()
.WithParameterName(nameof(email));
}
-}
\ No newline at end of file
+
+#if NET8_0
+ [Theory]
+ [ClassData(typeof(ValidEmailAddresses))]
+ public static void ValidEmailAddress_ReadOnlySpan(string email)
+ {
+ var result = email.AsSpan().MustBeEmailAddress();
+ result.ToString().Should().Be(email);
+ }
+
+ [Theory]
+ [ClassData(typeof(InvalidEmailAddresses))]
+ public static void InvalidEmailAddress_ReadOnlySpan(string email)
+ {
+ var act = () =>
+ {
+ var readOnlySpan = email.AsSpan();
+ readOnlySpan.MustBeEmailAddress();
+ };
+ act.Should().Throw()
+ .And.Message.Should()
+ .Contain($"readOnlySpan must be a valid email address, but it actually is \"{email}\".");
+ }
+
+ [Theory]
+ [ClassData(typeof(InvalidEmailAddresses))]
+ public static void InvalidEmailAddressArgumentName_ReadOnlySpan(string email)
+ {
+ Action act = () => email.AsSpan().MustBeEmailAddress(nameof(email));
+ act.Should().Throw()
+ .And.Message.Should().Contain(nameof(email));
+ }
+
+ [Theory]
+ [ClassData(typeof(InvalidEmailAddresses))]
+ public static void InvalidEmailAddressCustomMessage_ReadOnlySpan(string email)
+ {
+ const string customMessage = "This email address is not valid";
+ Action act = () => email.AsSpan().MustBeEmailAddress(nameof(email), customMessage);
+ act.Should().Throw()
+ .And.Message.Should().Contain(customMessage);
+ }
+
+ [Fact]
+ public static void CallerArgumentExpression_ReadOnlySpan()
+ {
+ var act = () =>
+ {
+ var email = "This is not an email address".AsSpan();
+ email.MustBeEmailAddress();
+ };
+ act.Should().Throw()
+ .WithParameterName("email");
+ }
+
+ [Fact]
+ public static void CustomException_ReadOnlySpan() =>
+ Test.CustomSpanException(
+ "email.domain.com".AsSpan(),
+ (input, exceptionFactory) => input.MustBeEmailAddress(exceptionFactory)
+ );
+
+ [Fact]
+ public static void CustomExceptionCustomRegex_ReadOnlySpan() =>
+ Test.CustomSpanException(
+ "invalidEmailAddress".AsSpan(),
+ CustomRegex,
+ (i, r, exceptionFactory) => i.MustBeEmailAddress(r, exceptionFactory)
+ );
+
+ [Fact]
+ public static void CustomMessageCustomRegex_ReadOnlySpan() =>
+ Test.CustomMessage(
+ message => "invalidEmailAddress".AsSpan().MustBeEmailAddress(CustomRegex, message: message)
+ );
+
+ // Tests for Span
+ [Theory]
+ [ClassData(typeof(ValidEmailAddresses))]
+ public static void ValidEmailAddress_Span(string email)
+ {
+ var emailChars = email.ToCharArray();
+ var span = new Span(emailChars);
+ var result = span.MustBeEmailAddress();
+ new string(result).Should().Be(email);
+ }
+
+ [Theory]
+ [ClassData(typeof(InvalidEmailAddresses))]
+ public static void InvalidEmailAddress_Span(string email)
+ {
+ var emailChars = email.ToCharArray();
+ var act = () =>
+ {
+ var span = new Span(emailChars);
+ span.MustBeEmailAddress();
+ };
+ act.Should().Throw()
+ .And.Message.Should().Contain($"span must be a valid email address, but it actually is \"{email}\".");
+ }
+
+ [Theory]
+ [ClassData(typeof(InvalidEmailAddresses))]
+ public static void InvalidEmailAddressArgumentName_Span(string email)
+ {
+ var emailChars = email.ToCharArray();
+ var act = () =>
+ {
+ var span = new Span(emailChars);
+ span.MustBeEmailAddress(nameof(span));
+ };
+ act.Should().Throw()
+ .And.Message.Should().Contain("span");
+ }
+
+ [Theory]
+ [ClassData(typeof(InvalidEmailAddresses))]
+ public static void InvalidEmailAddressCustomMessage_Span(string email)
+ {
+ const string customMessage = "This email address is not valid";
+ var emailChars = email.ToCharArray();
+ var act = () =>
+ {
+ var span = new Span(emailChars);
+ span.MustBeEmailAddress(nameof(span), customMessage);
+ };
+ act.Should().Throw()
+ .And.Message.Should().Contain(customMessage);
+ }
+
+ [Fact]
+ public static void CallerArgumentExpression_Span()
+ {
+ var act = () =>
+ {
+ var emailChars = "This is not an email address".ToCharArray();
+ var span = new Span(emailChars);
+ span.MustBeEmailAddress();
+ };
+ act.Should().Throw()
+ .WithParameterName("span");
+ }
+
+ [Fact]
+ public static void CustomException_Span()
+ {
+ var emailChars = "email.domain.com".ToCharArray();
+ var span = new Span(emailChars);
+ Test.CustomSpanException(
+ span,
+ (input, exceptionFactory) => input.MustBeEmailAddress(exceptionFactory)
+ );
+ }
+
+ [Fact]
+ public static void CustomExceptionCustomRegex_Span()
+ {
+ var emailChars = "invalidEmailAddress".ToCharArray();
+ var span = new Span(emailChars);
+ Test.CustomSpanException(
+ span,
+ CustomRegex,
+ (i, r, exceptionFactory) => i.MustBeEmailAddress(r, exceptionFactory)
+ );
+ }
+
+ [Fact]
+ public static void CustomMessageCustomRegex_Span() =>
+ Test.CustomMessage(
+ message =>
+ {
+ var span = new Span("invalidEmailAddress".ToCharArray());
+ span.MustBeEmailAddress(CustomRegex, message: message);
+ }
+ );
+
+ // Tests for Memory
+ [Theory]
+ [ClassData(typeof(ValidEmailAddresses))]
+ public static void ValidEmailAddress_Memory(string email)
+ {
+ var memory = email.ToCharArray().AsMemory();
+ var result = memory.MustBeEmailAddress();
+ result.ToString().Should().Be(email);
+ }
+
+ [Theory]
+ [ClassData(typeof(InvalidEmailAddresses))]
+ public static void InvalidEmailAddress_Memory(string email)
+ {
+ var memory = email.ToCharArray().AsMemory();
+ Action act = () => memory.MustBeEmailAddress();
+ act.Should().Throw()
+ .And.Message.Should().Contain($"memory must be a valid email address, but it actually is \"{email}\".");
+ }
+
+ [Theory]
+ [ClassData(typeof(InvalidEmailAddresses))]
+ public static void InvalidEmailAddressArgumentName_Memory(string email)
+ {
+ var memory = email.ToCharArray().AsMemory();
+ Action act = () => memory.MustBeEmailAddress(nameof(memory));
+ act.Should().Throw()
+ .And.Message.Should().Contain(nameof(memory));
+ }
+
+ [Theory]
+ [ClassData(typeof(InvalidEmailAddresses))]
+ public static void InvalidEmailAddressCustomMessage_Memory(string email)
+ {
+ const string customMessage = "This email address is not valid";
+ var memory = email.ToCharArray().AsMemory();
+ Action act = () => memory.MustBeEmailAddress(nameof(memory), customMessage);
+ act.Should().Throw()
+ .And.Message.Should().Contain(customMessage);
+ }
+
+ [Fact]
+ public static void CallerArgumentExpression_Memory()
+ {
+ var act = () =>
+ {
+ var memory = "This is not an email address".ToCharArray().AsMemory();
+ memory.MustBeEmailAddress();
+ };
+ act.Should().Throw()
+ .WithParameterName("memory");
+ }
+
+ [Fact]
+ public static void CustomException_Memory()
+ {
+ var memory = "email.domain.com".ToCharArray().AsMemory();
+ Test.CustomMemoryException(
+ memory,
+ (input, exceptionFactory) => input.MustBeEmailAddress(exceptionFactory)
+ );
+ }
+
+ [Fact]
+ public static void CustomExceptionCustomRegex_Memory()
+ {
+ var memory = "invalidEmailAddress".ToCharArray().AsMemory();
+ Test.CustomMemoryException(
+ memory,
+ CustomRegex,
+ (i, r, exceptionFactory) => i.MustBeEmailAddress(r, exceptionFactory)
+ );
+ }
+
+ [Fact]
+ public static void CustomMessageCustomRegex_Memory()
+ {
+ var memory = "invalidEmailAddress".AsMemory();
+ Test.CustomMessage(
+ message => memory.MustBeEmailAddress(CustomRegex, message: message)
+ );
+ }
+
+ // Tests for ReadOnlyMemory
+ [Theory]
+ [ClassData(typeof(ValidEmailAddresses))]
+ public static void ValidEmailAddress_ReadOnlyMemory(string email)
+ {
+ var memory = email.AsMemory();
+ var result = memory.MustBeEmailAddress();
+ result.ToString().Should().Be(email);
+ }
+
+ [Theory]
+ [ClassData(typeof(InvalidEmailAddresses))]
+ public static void InvalidEmailAddress_ReadOnlyMemory(string email)
+ {
+ var memory = email.AsMemory();
+ Action act = () => memory.MustBeEmailAddress();
+ act.Should().Throw()
+ .And.Message.Should().StartWith($"memory must be a valid email address, but it actually is \"{email}\".");
+ }
+
+ [Theory]
+ [ClassData(typeof(InvalidEmailAddresses))]
+ public static void InvalidEmailAddressArgumentName_ReadOnlyMemory(string email)
+ {
+ var readOnlyMemory = email.AsMemory();
+ Action act = () => readOnlyMemory.MustBeEmailAddress(nameof(readOnlyMemory));
+ act.Should().Throw()
+ .And.Message.Should().Contain(nameof(readOnlyMemory));
+ }
+
+ [Theory]
+ [ClassData(typeof(InvalidEmailAddresses))]
+ public static void InvalidEmailAddressCustomMessage_ReadOnlyMemory(string email)
+ {
+ const string customMessage = "This email address is not valid";
+ var readOnlyMemory = email.AsMemory();
+ Action act = () => readOnlyMemory.MustBeEmailAddress(nameof(readOnlyMemory), customMessage);
+ act.Should().Throw()
+ .And.Message.Should().Contain(customMessage);
+ }
+
+ [Fact]
+ public static void CallerArgumentExpression_ReadOnlyMemory()
+ {
+ var act = () =>
+ {
+ var readOnlyMemory = "This is not an email address".AsMemory();
+ readOnlyMemory.MustBeEmailAddress();
+ };
+ act.Should().Throw()
+ .WithParameterName("readOnlyMemory");
+ }
+
+ [Fact]
+ public static void CustomException_ReadOnlyMemory()
+ {
+ var readOnlyMemory = "email.domain.com".AsMemory();
+ Test.CustomMemoryException(
+ readOnlyMemory,
+ (input, exceptionFactory) => input.MustBeEmailAddress(exceptionFactory)
+ );
+ }
+
+ [Fact]
+ public static void CustomExceptionCustomRegex_ReadOnlyMemory()
+ {
+ var readOnlyMemory = "invalidEmailAddress".AsMemory();
+ Test.CustomMemoryException(
+ readOnlyMemory,
+ CustomRegex,
+ (i, r, exceptionFactory) => i.MustBeEmailAddress(r, exceptionFactory)
+ );
+ }
+
+ [Fact]
+ public static void CustomMessageCustomRegex_ReadOnlyMemory()
+ {
+ var readOnlyMemory = "invalidEmailAddress".AsMemory();
+ Test.CustomMessage(
+ message => readOnlyMemory.MustBeEmailAddress(CustomRegex, message: message)
+ );
+ }
+#endif
+}
diff --git a/Code/Light.GuardClauses.Tests/StringAssertions/MustBeFileExtensionTests.cs b/Code/Light.GuardClauses.Tests/StringAssertions/MustBeFileExtensionTests.cs
new file mode 100644
index 00000000..a5df0736
--- /dev/null
+++ b/Code/Light.GuardClauses.Tests/StringAssertions/MustBeFileExtensionTests.cs
@@ -0,0 +1,305 @@
+using System;
+using FluentAssertions;
+using Light.GuardClauses.Exceptions;
+using Light.GuardClauses.FrameworkExtensions;
+using Xunit;
+
+namespace Light.GuardClauses.Tests.StringAssertions;
+
+public static class MustBeFileExtensionTests
+{
+ public static readonly TheoryData InvalidFileExtensionsData =
+ [
+ "txt",
+ ".jpg/",
+ ".",
+ "..",
+ "...",
+ "....",
+ ".docx.",
+ ];
+
+ public static readonly TheoryData ValidFileExtensionsData =
+ [
+ ".txt",
+ ".jpg",
+ ".tar.gz",
+ ];
+
+ [Theory]
+ [MemberData(nameof(ValidFileExtensionsData))]
+ public static void ValidFileExtensions(string input) =>
+ input.MustBeFileExtension().Should().BeSameAs(input);
+
+ [Fact]
+ public static void StringIsNull()
+ {
+ var nullString = default(string);
+
+ // ReSharper disable once ExpressionIsAlwaysNull
+ var act = () => nullString.MustBeFileExtension(nameof(nullString));
+
+ act.Should().Throw()
+ .WithParameterName(nameof(nullString));
+ }
+
+ [Theory]
+ [MemberData(nameof(InvalidFileExtensionsData))]
+ public static void InvalidFileExtensions(string invalidString)
+ {
+ var act = () => invalidString.MustBeFileExtension(nameof(invalidString));
+
+ act.Should().Throw()
+ .And.Message.Should().Contain(
+ $"invalidString must be a valid file extension, but it actually is {invalidString.ToStringOrNull()}"
+ );
+ }
+
+ [Fact]
+ public static void CustomExceptionStringNull() =>
+ Test.CustomException(
+ default(string),
+ (@null, exceptionFactory) => @null.MustBeFileExtension(exceptionFactory)
+ );
+
+ [Theory]
+ [MemberData(nameof(InvalidFileExtensionsData))]
+ public static void CustomExceptionInvalidFileExtensions(string invalidString) =>
+ Test.CustomException(
+ invalidString,
+ (@string, exceptionFactory) => @string.MustBeFileExtension(exceptionFactory)
+ );
+
+ [Fact]
+ public static void CallerArgumentExpression()
+ {
+ const string invalidString = "txt";
+
+ var act = () => invalidString.MustBeFileExtension();
+
+ act.Should().Throw()
+ .WithParameterName(nameof(invalidString));
+ }
+
+ [Theory]
+ [MemberData(nameof(ValidFileExtensionsData))]
+ public static void ValidFileExtensions_ReadOnlySpan(string input)
+ {
+ var span = input.AsSpan();
+
+ var result = span.MustBeFileExtension();
+
+ result.Equals(span, StringComparison.Ordinal).Should().BeTrue();
+ }
+
+ [Theory]
+ [MemberData(nameof(InvalidFileExtensionsData))]
+ public static void InvalidFileExtensions_ReadOnlySpan(string invalidString)
+ {
+ var act = () =>
+ {
+ var span = new ReadOnlySpan(invalidString?.ToCharArray() ?? []);
+ span.MustBeFileExtension("parameterName");
+ };
+
+ act.Should().Throw()
+ .And.Message.Should().Contain(
+ $"parameterName must be a valid file extension, but it actually is {invalidString}"
+ );
+ }
+
+ [Theory]
+ [MemberData(nameof(InvalidFileExtensionsData))]
+ public static void CustomExceptionInvalidFileExtensions_ReadOnlySpan(string invalidString)
+ {
+ var exception = new Exception();
+
+ var act = () =>
+ {
+ var span = new ReadOnlySpan(invalidString?.ToCharArray() ?? []);
+ span.MustBeFileExtension(_ => exception);
+ };
+
+ act.Should().Throw().Which.Should().BeSameAs(exception);
+ }
+
+ [Fact]
+ public static void CallerArgumentExpression_ReadOnlySpan()
+ {
+ var act = () =>
+ {
+ var invalidSpan = "txt".AsSpan();
+ invalidSpan.MustBeFileExtension();
+ };
+
+ act.Should().Throw()
+ .WithParameterName("invalidSpan");
+ }
+
+ [Theory]
+ [MemberData(nameof(ValidFileExtensionsData))]
+ public static void ValidFileExtensions_Span(string input)
+ {
+ var span = input.AsSpan().ToArray().AsSpan();
+
+ var result = span.MustBeFileExtension();
+
+ result.ToString().Should().Be(input);
+ }
+
+ [Theory]
+ [MemberData(nameof(InvalidFileExtensionsData))]
+ public static void InvalidFileExtensions_Span(string invalidString)
+ {
+ var act = () =>
+ {
+ var span = new Span(invalidString?.ToCharArray() ?? []);
+ span.MustBeFileExtension("parameterName");
+ };
+
+ act.Should().Throw()
+ .And.Message.Should().Contain(
+ $"parameterName must be a valid file extension, but it actually is {invalidString}"
+ );
+ }
+
+ [Theory]
+ [MemberData(nameof(InvalidFileExtensionsData))]
+ public static void CustomExceptionInvalidFileExtensions_Span(string invalidString)
+ {
+ var exception = new Exception();
+
+ var act = () =>
+ {
+ var span = new Span(invalidString?.ToCharArray() ?? []);
+ span.MustBeFileExtension(_ => exception);
+ };
+
+ act.Should().Throw().Which.Should().BeSameAs(exception);
+ }
+
+ [Fact]
+ public static void CallerArgumentExpression_Span()
+ {
+ var act = () =>
+ {
+ var charArray = "txt".ToCharArray();
+ var invalidSpan = new Span(charArray);
+ invalidSpan.MustBeFileExtension();
+ };
+
+ act.Should().Throw()
+ .WithParameterName("invalidSpan");
+ }
+
+ [Theory]
+ [MemberData(nameof(ValidFileExtensionsData))]
+ public static void ValidFileExtensions_Memory(string input)
+ {
+ var memory = input.ToCharArray().AsMemory();
+
+ var result = memory.MustBeFileExtension();
+
+ result.ToString().Should().Be(input);
+ }
+
+ [Theory]
+ [MemberData(nameof(InvalidFileExtensionsData))]
+ public static void InvalidFileExtensions_Memory(string invalidString)
+ {
+ var act = () =>
+ {
+ var memory = new Memory(invalidString?.ToCharArray() ?? []);
+ memory.MustBeFileExtension("parameterName");
+ };
+
+ act.Should().Throw()
+ .And.Message.Should().Contain(
+ $"parameterName must be a valid file extension, but it actually is {invalidString}"
+ );
+ }
+
+ [Theory]
+ [MemberData(nameof(InvalidFileExtensionsData))]
+ public static void CustomExceptionInvalidFileExtensions_Memory(string invalidString)
+ {
+ var exception = new Exception();
+
+ var act = () =>
+ {
+ var memory = new Memory(invalidString?.ToCharArray() ?? []);
+ memory.MustBeFileExtension(_ => exception);
+ };
+
+ act.Should().Throw().Which.Should().BeSameAs(exception);
+ }
+
+ [Fact]
+ public static void CallerArgumentExpression_Memory()
+ {
+ var act = () =>
+ {
+ var charArray = "txt".ToCharArray();
+ var invalidMemory = new Memory(charArray);
+ invalidMemory.MustBeFileExtension();
+ };
+
+ act.Should().Throw()
+ .WithParameterName("invalidMemory");
+ }
+
+ [Theory]
+ [MemberData(nameof(ValidFileExtensionsData))]
+ public static void ValidFileExtensions_ReadOnlyMemory(string input)
+ {
+ var memory = input.AsMemory();
+
+ var result = memory.MustBeFileExtension();
+
+ result.Equals(memory).Should().BeTrue();
+ }
+
+ [Theory]
+ [MemberData(nameof(InvalidFileExtensionsData))]
+ public static void InvalidFileExtensions_ReadOnlyMemory(string invalidString)
+ {
+ var act = () =>
+ {
+ var memory = new ReadOnlyMemory(invalidString?.ToCharArray() ?? []);
+ memory.MustBeFileExtension("parameterName");
+ };
+
+ act.Should().Throw()
+ .And.Message.Should().Contain(
+ $"parameterName must be a valid file extension, but it actually is {invalidString}"
+ );
+ }
+
+ [Theory]
+ [MemberData(nameof(InvalidFileExtensionsData))]
+ public static void CustomExceptionInvalidFileExtensions_ReadOnlyMemory(string invalidString)
+ {
+ var exception = new Exception();
+
+ var act = () =>
+ {
+ var memory = new ReadOnlyMemory(invalidString?.ToCharArray() ?? []);
+ memory.MustBeFileExtension(_ => exception);
+ };
+
+ act.Should().Throw().Which.Should().BeSameAs(exception);
+ }
+
+ [Fact]
+ public static void CallerArgumentExpression_ReadOnlyMemory()
+ {
+ var act = void () =>
+ {
+ var invalidMemory = "txt".AsMemory();
+ invalidMemory.MustBeFileExtension();
+ };
+
+ act.Should().Throw()
+ .WithParameterName("invalidMemory");
+ }
+}
diff --git a/Code/Light.GuardClauses.Tests/StringAssertions/ValidEmailAddresses.cs b/Code/Light.GuardClauses.Tests/StringAssertions/ValidEmailAddresses.cs
new file mode 100644
index 00000000..d134ce1b
--- /dev/null
+++ b/Code/Light.GuardClauses.Tests/StringAssertions/ValidEmailAddresses.cs
@@ -0,0 +1,28 @@
+using Xunit;
+
+namespace Light.GuardClauses.Tests.StringAssertions;
+
+public sealed class ValidEmailAddresses : TheoryData
+{
+ public ValidEmailAddresses()
+ {
+ Add("email@domain.com");
+ Add("firstname.lastname@domain.com");
+ Add("email@subdomain.domain.com");
+ Add("firstname+lastname@domain.com");
+ Add("email@123.123.123.123");
+ Add("1234567890@domain.com");
+ Add("email@domain-one.com");
+ Add("_______@domain.com");
+ Add("email@domain.name");
+ Add("email@domain.co.jp");
+ Add("firstname-lastname@domain.com");
+ Add("email@domain.museum"); // Long TLD (>4 chars)
+ Add("email@domain.travel"); // Another long TLD
+ Add("email@domain.photography"); // Even longer TLD
+ Add("email@[IPv6:2001:db8::1]"); // IPv6 format
+ Add("\"quoted\"@domain.com"); // Quoted local part
+ Add("user.name+tag+sorting@example.com"); // Gmail-style + addressing
+ Add("あいうえお@domain.com"); // Unicode character test
+ }
+}
diff --git a/Code/Light.GuardClauses.Tests/Test.cs b/Code/Light.GuardClauses.Tests/Test.cs
index 6ee89578..d2956eab 100644
--- a/Code/Light.GuardClauses.Tests/Test.cs
+++ b/Code/Light.GuardClauses.Tests/Test.cs
@@ -1,11 +1,12 @@
-using System;
+#nullable enable
+
+using System;
using FluentAssertions;
using FluentAssertions.Specialized;
+using Light.GuardClauses.ExceptionFactory;
using Xunit.Abstractions;
using Xunit.Sdk;
-#nullable enable
-
namespace Light.GuardClauses.Tests;
public static class Test
@@ -48,7 +49,108 @@ Exception ExceptionFactory(T parameter)
}
}
- public static void CustomException(T1 first, T2 second, Action> executeAssertion)
+ public static void CustomSpanException(
+ ReadOnlySpan invalidValue,
+ ExecuteReadOnlySpanAssertion executeAssertion
+ )
+ {
+ T[]? capturedParameter = null;
+
+ Exception ExceptionFactory(ReadOnlySpan parameter)
+ {
+ capturedParameter = parameter.ToArray();
+ return Exception;
+ }
+
+ try
+ {
+ executeAssertion(invalidValue, ExceptionFactory);
+ throw new XunitException("The assertion should have thrown a custom exception at this point.");
+ }
+ catch (ExceptionDummy exception)
+ {
+ exception.Should().BeSameAs(exception);
+ capturedParameter.Should().Equal(invalidValue.ToArray());
+ }
+ }
+
+ public static void CustomSpanException(Span invalidValue, ExecuteSpanAssertion executeAssertion)
+ {
+ T[]? capturedParameter = null;
+
+ Exception ExceptionFactory(ReadOnlySpan parameter)
+ {
+ capturedParameter = parameter.ToArray();
+ return Exception;
+ }
+
+ try
+ {
+ executeAssertion(invalidValue, ExceptionFactory);
+ throw new XunitException("The assertion should have thrown a custom exception at this point.");
+ }
+ catch (ExceptionDummy exception)
+ {
+ exception.Should().BeSameAs(exception);
+ capturedParameter.Should().Equal(invalidValue.ToArray());
+ }
+ }
+
+ public static void CustomMemoryException(
+ Memory invalidValue,
+ Action, ReadOnlySpanExceptionFactory> executeAssertion
+ )
+ {
+ Memory capturedParameter = default;
+
+ Exception ExceptionFactory(ReadOnlySpan parameter)
+ {
+ capturedParameter = parameter.ToArray();
+ return Exception;
+ }
+
+ try
+ {
+ executeAssertion(invalidValue, ExceptionFactory);
+ throw new XunitException("The assertion should have thrown a custom exception at this point.");
+ }
+ catch (ExceptionDummy exception)
+ {
+ exception.Should().BeSameAs(exception);
+ capturedParameter.ToArray().Should().Equal(invalidValue.ToArray());
+ }
+ }
+
+ public static void CustomMemoryException(
+ ReadOnlyMemory invalidValue,
+ Action, ReadOnlySpanExceptionFactory> executeAssertion
+ )
+ {
+ ReadOnlyMemory capturedParameter = default;
+
+ Exception ExceptionFactory(ReadOnlySpan parameter)
+ {
+ capturedParameter = parameter.ToArray();
+ return Exception;
+ }
+
+ try
+ {
+ executeAssertion(invalidValue, ExceptionFactory);
+ throw new XunitException("The assertion should have thrown a custom exception at this point.");
+ }
+ catch (ExceptionDummy exception)
+ {
+ exception.Should().BeSameAs(exception);
+ capturedParameter.ToArray().Should().Equal(invalidValue.ToArray());
+ }
+ }
+
+ public static void CustomException(
+ T1 first,
+ T2 second,
+ Action> executeAssertion
+ )
{
T1? capturedFirst = default;
T2? capturedSecond = default;
@@ -73,7 +175,128 @@ Exception ExceptionFactory(T1 x, T2 y)
}
}
- public static void CustomException(T1 first, T2 second, T3 third, Action> executeAssertion)
+ public static void CustomSpanException(
+ ReadOnlySpan invalidValue,
+ T2 additionalValue,
+ ExecuteReadOnlySpanAssertion executeAssertion
+ )
+ {
+ T1[]? capturedFirst = null;
+ T2? capturedSecond = default;
+
+ Exception ExceptionFactory(ReadOnlySpan parameter, T2 second)
+ {
+ capturedFirst = parameter.ToArray();
+ capturedSecond = second;
+ return Exception;
+ }
+
+ try
+ {
+ executeAssertion(invalidValue, additionalValue, ExceptionFactory);
+ throw new XunitException("The assertion should have thrown a custom exception at this point.");
+ }
+ catch (ExceptionDummy exception)
+ {
+ exception.Should().BeSameAs(exception);
+ capturedFirst.Should().Equal(invalidValue.ToArray());
+ capturedSecond.Should().Be(additionalValue);
+ }
+ }
+
+ public static void CustomSpanException(
+ Span invalidValue,
+ T2 additionalValue,
+ ExecuteSpanAssertion executeAssertion
+ )
+ {
+ T1[]? capturedFirst = null;
+ T2? capturedSecond = default;
+
+ Exception ExceptionFactory(ReadOnlySpan parameter, T2 second)
+ {
+ capturedFirst = parameter.ToArray();
+ capturedSecond = second;
+ return Exception;
+ }
+
+ try
+ {
+ executeAssertion(invalidValue, additionalValue, ExceptionFactory);
+ throw new XunitException("The assertion should have thrown a custom exception at this point.");
+ }
+ catch (ExceptionDummy exception)
+ {
+ exception.Should().BeSameAs(exception);
+ capturedFirst.Should().Equal(invalidValue.ToArray());
+ capturedSecond.Should().Be(additionalValue);
+ }
+ }
+
+ public static void CustomMemoryException