diff --git a/StrongTypes.slnx b/StrongTypes.slnx
index 3bca7ec..5a73028 100644
--- a/StrongTypes.slnx
+++ b/StrongTypes.slnx
@@ -6,6 +6,7 @@
+
diff --git a/src/StrongTypes.SourceGenerators/IsExternalInit.cs b/src/StrongTypes.SourceGenerators/IsExternalInit.cs
new file mode 100644
index 0000000..ed1a03e
--- /dev/null
+++ b/src/StrongTypes.SourceGenerators/IsExternalInit.cs
@@ -0,0 +1,3 @@
+namespace System.Runtime.CompilerServices;
+
+internal static class IsExternalInit { }
diff --git a/src/StrongTypes.SourceGenerators/NumericWrapperGenerator.cs b/src/StrongTypes.SourceGenerators/NumericWrapperGenerator.cs
new file mode 100644
index 0000000..d399466
--- /dev/null
+++ b/src/StrongTypes.SourceGenerators/NumericWrapperGenerator.cs
@@ -0,0 +1,314 @@
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.Linq;
+using System.Text;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Microsoft.CodeAnalysis.Text;
+
+namespace StrongTypes.SourceGenerators;
+
+[Generator]
+public sealed class NumericWrapperGenerator : IIncrementalGenerator
+{
+ private const string AttributeFullName = "StrongTypes.NumericWrapperAttribute";
+
+ public void Initialize(IncrementalGeneratorInitializationContext context)
+ {
+ var targets = context.SyntaxProvider
+ .ForAttributeWithMetadataName(
+ AttributeFullName,
+ predicate: static (node, _) => node is StructDeclarationSyntax,
+ transform: static (ctx, _) => Model.From(ctx))
+ .Where(static m => m is not null)
+ .Select(static (m, _) => m!);
+
+ context.RegisterSourceOutput(targets, static (spc, model) =>
+ {
+ spc.AddSource($"{model.HintName}.g.cs", SourceText.From(Emitter.EmitType(model), Encoding.UTF8));
+ spc.AddSource($"{model.HintName}.Extensions.g.cs", SourceText.From(Emitter.EmitExtensions(model), Encoding.UTF8));
+ });
+ }
+
+ internal sealed record Model(
+ string Namespace,
+ string TypeName,
+ string TypeNameWithArity,
+ string SelfType,
+ string UnderlyingType,
+ string? TypeParameterList,
+ ImmutableArray ConstraintClauses,
+ string AccessModifier,
+ string InvariantDescription,
+ bool GenerateSum,
+ string HintName)
+ {
+ public static Model? From(GeneratorAttributeSyntaxContext ctx)
+ {
+ if (ctx.TargetSymbol is not INamedTypeSymbol symbol)
+ return null;
+
+ var valueProperty = symbol.GetMembers("Value")
+ .OfType()
+ .FirstOrDefault(p => p.DeclaredAccessibility == Accessibility.Public && !p.IsStatic);
+
+ if (valueProperty is null)
+ return null;
+
+ var underlyingType = valueProperty.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
+ var ns = symbol.ContainingNamespace.IsGlobalNamespace
+ ? string.Empty
+ : symbol.ContainingNamespace.ToDisplayString();
+
+ var typeName = symbol.Name;
+ string? typeParameterList = null;
+ string selfType = typeName;
+ string typeNameWithArity = typeName;
+ if (symbol.IsGenericType)
+ {
+ var parameters = string.Join(", ", symbol.TypeParameters.Select(tp => tp.Name));
+ typeParameterList = $"<{parameters}>";
+ selfType = $"{typeName}<{parameters}>";
+ typeNameWithArity = $"{typeName}`{symbol.TypeParameters.Length}";
+ }
+
+ var constraintClauses = BuildConstraintClauses(symbol);
+
+ var attr = ctx.Attributes[0];
+ string invariantDescription = "valid";
+ bool generateSum = false;
+
+ foreach (var kvp in attr.NamedArguments)
+ {
+ switch (kvp.Key)
+ {
+ case "InvariantDescription" when kvp.Value.Value is string s:
+ invariantDescription = s;
+ break;
+ case "GenerateSum" when kvp.Value.Value is bool b:
+ generateSum = b;
+ break;
+ }
+ }
+
+ var accessibility = symbol.DeclaredAccessibility switch
+ {
+ Accessibility.Public => "public",
+ Accessibility.Internal => "internal",
+ _ => "internal"
+ };
+
+ var hintName = string.IsNullOrEmpty(ns)
+ ? typeNameWithArity
+ : $"{ns}.{typeNameWithArity}";
+
+ return new Model(
+ Namespace: ns,
+ TypeName: typeName,
+ TypeNameWithArity: typeNameWithArity,
+ SelfType: selfType,
+ UnderlyingType: underlyingType,
+ TypeParameterList: typeParameterList,
+ ConstraintClauses: constraintClauses,
+ AccessModifier: accessibility,
+ InvariantDescription: invariantDescription,
+ GenerateSum: generateSum,
+ HintName: hintName);
+ }
+
+ private static ImmutableArray BuildConstraintClauses(INamedTypeSymbol symbol)
+ {
+ if (!symbol.IsGenericType)
+ return ImmutableArray.Empty;
+
+ var builder = ImmutableArray.CreateBuilder();
+ foreach (var tp in symbol.TypeParameters)
+ {
+ var parts = new List();
+
+ if (tp.HasReferenceTypeConstraint)
+ parts.Add("class");
+ else if (tp.HasValueTypeConstraint)
+ parts.Add("struct");
+ else if (tp.HasUnmanagedTypeConstraint)
+ parts.Add("unmanaged");
+ else if (tp.HasNotNullConstraint)
+ parts.Add("notnull");
+
+ foreach (var c in tp.ConstraintTypes)
+ parts.Add(c.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat));
+
+ if (tp.HasConstructorConstraint)
+ parts.Add("new()");
+
+ if (parts.Count == 0)
+ continue;
+
+ builder.Add($"where {tp.Name} : {string.Join(", ", parts)}");
+ }
+ return builder.ToImmutable();
+ }
+ }
+
+ internal static class Emitter
+ {
+ public static string EmitType(Model m)
+ {
+ var sb = new StringBuilder();
+ sb.AppendLine("// ");
+ sb.AppendLine("#nullable enable");
+ sb.AppendLine();
+ sb.AppendLine("using System;");
+ sb.AppendLine();
+ if (!string.IsNullOrEmpty(m.Namespace))
+ {
+ sb.Append("namespace ").Append(m.Namespace).AppendLine(";");
+ sb.AppendLine();
+ }
+
+ var self = m.SelfType;
+ var t = m.UnderlyingType;
+
+ sb.Append("partial struct ").Append(m.TypeName);
+ if (m.TypeParameterList is not null)
+ sb.Append(m.TypeParameterList);
+ sb.AppendLine(" :");
+ sb.Append(" global::System.IEquatable<").Append(self).AppendLine(">,");
+ sb.Append(" global::System.IEquatable<").Append(t).AppendLine(">,");
+ sb.Append(" global::System.IComparable<").Append(self).AppendLine(">,");
+ sb.Append(" global::System.IComparable<").Append(t).AppendLine(">,");
+ sb.AppendLine(" global::System.IComparable");
+
+ foreach (var c in m.ConstraintClauses)
+ sb.Append(" ").AppendLine(c);
+
+ sb.AppendLine("{");
+
+ sb.Append(" public static implicit operator ").Append(t).Append('(').Append(self).AppendLine(" value) => value.Value;");
+ sb.Append(" public static explicit operator ").Append(self).Append('(').Append(t).AppendLine(" value) => Create(value);");
+ sb.AppendLine();
+
+ sb.Append(" public static ").Append(self).Append(" Create(").Append(t).AppendLine(" value)");
+ sb.Append(" => TryCreate(value) ?? throw new global::System.ArgumentException($\"Value must be ")
+ .Append(m.InvariantDescription)
+ .AppendLine(", but was '{value}'.\", nameof(value));");
+ sb.AppendLine();
+
+ sb.AppendLine(" public override int GetHashCode() => Value.GetHashCode();");
+ sb.AppendLine();
+
+ sb.AppendLine(" public override bool Equals(object? obj) => obj switch");
+ sb.AppendLine(" {");
+ sb.Append(" ").Append(self).AppendLine(" other => Equals(other),");
+ sb.Append(" ").Append(t).AppendLine(" other => Equals(other),");
+ sb.AppendLine(" _ => false");
+ sb.AppendLine(" };");
+ sb.AppendLine();
+
+ sb.Append(" public bool Equals(").Append(self).AppendLine(" other) => Value.Equals(other.Value);");
+ sb.Append(" public bool Equals(").Append(t).AppendLine("? other) => other is not null && Value.Equals(other);");
+ sb.AppendLine();
+
+ sb.Append(" public static bool operator ==(").Append(self).Append(" left, ").Append(self).AppendLine(" right) => left.Equals(right);");
+ sb.Append(" public static bool operator !=(").Append(self).Append(" left, ").Append(self).AppendLine(" right) => !left.Equals(right);");
+ sb.Append(" public static bool operator ==(").Append(self).Append(" left, ").Append(t).AppendLine(" right) => left.Value.Equals(right);");
+ sb.Append(" public static bool operator !=(").Append(self).Append(" left, ").Append(t).AppendLine(" right) => !left.Value.Equals(right);");
+ sb.Append(" public static bool operator ==(").Append(t).Append(" left, ").Append(self).AppendLine(" right) => right.Value.Equals(left);");
+ sb.Append(" public static bool operator !=(").Append(t).Append(" left, ").Append(self).AppendLine(" right) => !right.Value.Equals(left);");
+ sb.AppendLine();
+
+ sb.Append(" public int CompareTo(").Append(self).AppendLine(" other) => Value.CompareTo(other.Value);");
+ sb.Append(" public int CompareTo(").Append(t).AppendLine("? other) => other is null ? 1 : Value.CompareTo(other);");
+ sb.AppendLine();
+
+ sb.AppendLine(" int global::System.IComparable.CompareTo(object? obj) => obj switch");
+ sb.AppendLine(" {");
+ sb.AppendLine(" null => 1,");
+ sb.Append(" ").Append(self).AppendLine(" other => CompareTo(other),");
+ sb.Append(" ").Append(t).AppendLine(" other => CompareTo(other),");
+ sb.Append(" _ => throw new global::System.ArgumentException($\"Object must be of type ").Append(m.TypeName).Append(" or {typeof(").Append(t).AppendLine(").Name}.\", nameof(obj))");
+ sb.AppendLine(" };");
+ sb.AppendLine();
+
+ foreach (var op in new[] { "<", "<=", ">", ">=" })
+ sb.Append(" public static bool operator ").Append(op).Append('(').Append(self).Append(" left, ").Append(self).Append(" right) => left.CompareTo(right) ").Append(op).AppendLine(" 0;");
+ foreach (var op in new[] { "<", "<=", ">", ">=" })
+ sb.Append(" public static bool operator ").Append(op).Append('(').Append(self).Append(" left, ").Append(t).Append(" right) => left.Value.CompareTo(right) ").Append(op).AppendLine(" 0;");
+ foreach (var op in new[] { "<", "<=", ">", ">=" })
+ sb.Append(" public static bool operator ").Append(op).Append('(').Append(t).Append(" left, ").Append(self).Append(" right) => left.CompareTo(right.Value) ").Append(op).AppendLine(" 0;");
+ sb.AppendLine();
+
+ sb.AppendLine(" public override string ToString() => Value.ToString() ?? string.Empty;");
+
+ sb.AppendLine("}");
+ return sb.ToString();
+ }
+
+ public static string EmitExtensions(Model m)
+ {
+ var sb = new StringBuilder();
+ sb.AppendLine("// ");
+ sb.AppendLine("#nullable enable");
+ sb.AppendLine();
+ sb.AppendLine("using System;");
+ sb.AppendLine("using System.Collections.Generic;");
+ sb.AppendLine();
+ if (!string.IsNullOrEmpty(m.Namespace))
+ {
+ sb.Append("namespace ").Append(m.Namespace).AppendLine(";");
+ sb.AppendLine();
+ }
+
+ var self = m.SelfType;
+ var t = m.UnderlyingType;
+ var className = $"{m.TypeName}Extensions";
+ var methodTypeParams = m.TypeParameterList ?? string.Empty;
+ var methodConstraints = m.ConstraintClauses.IsDefaultOrEmpty
+ ? string.Empty
+ : " " + string.Join(" ", m.ConstraintClauses);
+
+ sb.Append(m.AccessModifier).Append(" static class ").AppendLine(className);
+ sb.AppendLine("{");
+
+ EmitMinMax(sb, "Min", "<", self, methodTypeParams, methodConstraints);
+ sb.AppendLine();
+ EmitMinMax(sb, "Max", ">", self, methodTypeParams, methodConstraints);
+
+ if (m.GenerateSum)
+ {
+ sb.AppendLine();
+ sb.Append(" public static ").Append(self).Append(" Sum").Append(methodTypeParams)
+ .Append("(this global::System.Collections.Generic.IEnumerable<").Append(self).Append("> values)").Append(methodConstraints).AppendLine();
+ sb.AppendLine(" {");
+ sb.AppendLine(" if (values is null) throw new global::System.ArgumentNullException(nameof(values));");
+ sb.Append(" ").Append(t).AppendLine(" sum = default!;");
+ sb.AppendLine(" foreach (var value in values)");
+ sb.AppendLine(" {");
+ sb.AppendLine(" sum = checked(sum + value.Value);");
+ sb.AppendLine(" }");
+ sb.Append(" return ").Append(self).AppendLine(".Create(sum);");
+ sb.AppendLine(" }");
+ }
+
+ sb.AppendLine("}");
+ return sb.ToString();
+ }
+
+ private static void EmitMinMax(StringBuilder sb, string name, string op, string self, string methodTypeParams, string methodConstraints)
+ {
+ sb.Append(" public static ").Append(self).Append(' ').Append(name).Append(methodTypeParams)
+ .Append("(this global::System.Collections.Generic.IEnumerable<").Append(self).Append("> values)").Append(methodConstraints).AppendLine();
+ sb.AppendLine(" {");
+ sb.AppendLine(" if (values is null) throw new global::System.ArgumentNullException(nameof(values));");
+ sb.AppendLine(" using var e = values.GetEnumerator();");
+ sb.AppendLine(" if (!e.MoveNext()) throw new global::System.InvalidOperationException(\"Sequence contains no elements.\");");
+ sb.AppendLine(" var result = e.Current;");
+ sb.AppendLine(" while (e.MoveNext())");
+ sb.AppendLine(" {");
+ sb.Append(" if (e.Current.CompareTo(result) ").Append(op).AppendLine(" 0) result = e.Current;");
+ sb.AppendLine(" }");
+ sb.AppendLine(" return result;");
+ sb.AppendLine(" }");
+ }
+ }
+}
diff --git a/src/StrongTypes.SourceGenerators/StrongTypes.SourceGenerators.csproj b/src/StrongTypes.SourceGenerators/StrongTypes.SourceGenerators.csproj
new file mode 100644
index 0000000..d6d5a6d
--- /dev/null
+++ b/src/StrongTypes.SourceGenerators/StrongTypes.SourceGenerators.csproj
@@ -0,0 +1,15 @@
+
+
+ netstandard2.0
+ latest
+ enable
+ true
+ true
+ false
+ true
+ false
+
+
+
+
+
diff --git a/src/StrongTypes/Numbers/Negative.cs b/src/StrongTypes/Numbers/Negative.cs
index 1707bee..9554791 100644
--- a/src/StrongTypes/Numbers/Negative.cs
+++ b/src/StrongTypes/Numbers/Negative.cs
@@ -1,6 +1,5 @@
#nullable enable
-using System;
using System.Numerics;
using System.Text.Json.Serialization;
@@ -10,18 +9,14 @@ namespace StrongTypes;
/// A numeric value guaranteed to be strictly less than T.Zero.
///
///
-/// Construct via or . Internally the
-/// value is stored as an offset from -T.One so that default(Negative<T>)
-/// represents -T.One — i.e. the zero-initialized struct still satisfies
-/// the negativity invariant.
+/// Construct via or Create (generated). Internally
+/// the value is stored as an offset from -T.One so that
+/// default(Negative<T>) represents -T.One — i.e. the
+/// zero-initialized struct still satisfies the negativity invariant.
///
+[NumericWrapper(InvariantDescription = "negative", GenerateSum = true)]
[JsonConverter(typeof(NumericStrongTypeJsonConverterFactory))]
-public readonly struct Negative :
- IEquatable>,
- IEquatable,
- IComparable>,
- IComparable,
- IComparable
+public readonly partial struct Negative
where T : INumber
{
// Stored as (Value - (-T.One)) == (Value + T.One); default represents Value == -T.One.
@@ -34,10 +29,6 @@ private Negative(T offset)
public T Value => _offset - T.One;
- public static implicit operator T(Negative value) => value.Value;
-
- public static explicit operator Negative(T value) => Create(value);
-
///
/// Returns a wrapping , or
/// null if is not strictly less than zero.
@@ -46,64 +37,4 @@ private Negative(T offset)
{
return value < T.Zero ? new Negative(value + T.One) : null;
}
-
- ///
- /// Returns a wrapping .
- /// Throws if is not
- /// strictly less than zero.
- ///
- public static Negative Create(T value)
- {
- return TryCreate(value)
- ?? throw new ArgumentException($"Value must be negative, but was '{value}'.", nameof(value));
- }
-
- public override int GetHashCode() => Value.GetHashCode();
-
- public override bool Equals(object? obj) =>
- obj switch
- {
- Negative other => Equals(other),
- T other => Equals(other),
- _ => false
- };
-
- public bool Equals(Negative other) => Value.Equals(other.Value);
-
- public bool Equals(T? other) => other is not null && Value.Equals(other);
-
- public static bool operator ==(Negative left, Negative right) => left.Equals(right);
- public static bool operator !=(Negative left, Negative right) => !left.Equals(right);
- public static bool operator ==(Negative left, T right) => left.Value.Equals(right);
- public static bool operator !=(Negative left, T right) => !left.Value.Equals(right);
- public static bool operator ==(T left, Negative right) => right.Value.Equals(left);
- public static bool operator !=(T left, Negative right) => !right.Value.Equals(left);
-
- public int CompareTo(Negative other) => Value.CompareTo(other.Value);
-
- public int CompareTo(T? other) => other is null ? 1 : Value.CompareTo(other);
-
- int IComparable.CompareTo(object? obj) =>
- obj switch
- {
- null => 1,
- Negative other => CompareTo(other),
- T other => CompareTo(other),
- _ => throw new ArgumentException($"Object must be of type {nameof(Negative)} or {typeof(T).Name}.", nameof(obj))
- };
-
- public static bool operator <(Negative left, Negative right) => left.CompareTo(right) < 0;
- public static bool operator <=(Negative left, Negative right) => left.CompareTo(right) <= 0;
- public static bool operator >(Negative left, Negative right) => left.CompareTo(right) > 0;
- public static bool operator >=(Negative left, Negative right) => left.CompareTo(right) >= 0;
- public static bool operator <(Negative left, T right) => left.Value.CompareTo(right) < 0;
- public static bool operator <=(Negative left, T right) => left.Value.CompareTo(right) <= 0;
- public static bool operator >(Negative left, T right) => left.Value.CompareTo(right) > 0;
- public static bool operator >=(Negative left, T right) => left.Value.CompareTo(right) >= 0;
- public static bool operator <(T left, Negative right) => left.CompareTo(right.Value) < 0;
- public static bool operator <=(T left, Negative right) => left.CompareTo(right.Value) <= 0;
- public static bool operator >(T left, Negative right) => left.CompareTo(right.Value) > 0;
- public static bool operator >=(T left, Negative right) => left.CompareTo(right.Value) >= 0;
-
- public override string ToString() => Value.ToString() ?? string.Empty;
}
diff --git a/src/StrongTypes/Numbers/NonNegative.cs b/src/StrongTypes/Numbers/NonNegative.cs
index 8cd3d19..5f5bc5e 100644
--- a/src/StrongTypes/Numbers/NonNegative.cs
+++ b/src/StrongTypes/Numbers/NonNegative.cs
@@ -1,6 +1,5 @@
#nullable enable
-using System;
using System.Numerics;
using System.Text.Json.Serialization;
@@ -10,18 +9,14 @@ namespace StrongTypes;
/// A numeric value guaranteed to be greater than or equal to T.Zero.
///
///
-/// Construct via or . Unlike
+/// Construct via or Create (generated). Unlike
/// , default(NonNegative<T>) wraps
/// T.Zero and therefore does satisfy the invariant, though the
/// factories remain the intended entry point.
///
+[NumericWrapper(InvariantDescription = "non-negative", GenerateSum = true)]
[JsonConverter(typeof(NumericStrongTypeJsonConverterFactory))]
-public readonly struct NonNegative :
- IEquatable>,
- IEquatable,
- IComparable>,
- IComparable,
- IComparable
+public readonly partial struct NonNegative
where T : INumber
{
private NonNegative(T value)
@@ -31,10 +26,6 @@ private NonNegative(T value)
public T Value { get; }
- public static implicit operator T(NonNegative value) => value.Value;
-
- public static explicit operator NonNegative(T value) => Create(value);
-
///
/// Returns a wrapping , or
/// null if is less than zero.
@@ -43,64 +34,4 @@ private NonNegative(T value)
{
return value >= T.Zero ? new NonNegative(value) : null;
}
-
- ///
- /// Returns a wrapping .
- /// Throws if is less
- /// than zero.
- ///
- public static NonNegative Create(T value)
- {
- return TryCreate(value)
- ?? throw new ArgumentException($"Value must be non-negative, but was '{value}'.", nameof(value));
- }
-
- public override int GetHashCode() => Value.GetHashCode();
-
- public override bool Equals(object? obj) =>
- obj switch
- {
- NonNegative other => Equals(other),
- T other => Equals(other),
- _ => false
- };
-
- public bool Equals(NonNegative other) => Value.Equals(other.Value);
-
- public bool Equals(T? other) => other is not null && Value.Equals(other);
-
- public static bool operator ==(NonNegative left, NonNegative right) => left.Equals(right);
- public static bool operator !=(NonNegative left, NonNegative right) => !left.Equals(right);
- public static bool operator ==(NonNegative left, T right) => left.Value.Equals(right);
- public static bool operator !=(NonNegative left, T right) => !left.Value.Equals(right);
- public static bool operator ==(T left, NonNegative right) => right.Value.Equals(left);
- public static bool operator !=(T left, NonNegative right) => !right.Value.Equals(left);
-
- public int CompareTo(NonNegative other) => Value.CompareTo(other.Value);
-
- public int CompareTo(T? other) => other is null ? 1 : Value.CompareTo(other);
-
- int IComparable.CompareTo(object? obj) =>
- obj switch
- {
- null => 1,
- NonNegative other => CompareTo(other),
- T other => CompareTo(other),
- _ => throw new ArgumentException($"Object must be of type {nameof(NonNegative)} or {typeof(T).Name}.", nameof(obj))
- };
-
- public static bool operator <(NonNegative left, NonNegative right) => left.CompareTo(right) < 0;
- public static bool operator <=(NonNegative left, NonNegative right) => left.CompareTo(right) <= 0;
- public static bool operator >(NonNegative left, NonNegative right) => left.CompareTo(right) > 0;
- public static bool operator >=(NonNegative left, NonNegative right) => left.CompareTo(right) >= 0;
- public static bool operator <(NonNegative left, T right) => left.Value.CompareTo(right) < 0;
- public static bool operator <=(NonNegative left, T right) => left.Value.CompareTo(right) <= 0;
- public static bool operator >(NonNegative left, T right) => left.Value.CompareTo(right) > 0;
- public static bool operator >=(NonNegative left, T right) => left.Value.CompareTo(right) >= 0;
- public static bool operator <(T left, NonNegative right) => left.CompareTo(right.Value) < 0;
- public static bool operator <=(T left, NonNegative right) => left.CompareTo(right.Value) <= 0;
- public static bool operator >(T left, NonNegative right) => left.CompareTo(right.Value) > 0;
- public static bool operator >=(T left, NonNegative right) => left.CompareTo(right.Value) >= 0;
-
- public override string ToString() => Value.ToString() ?? string.Empty;
}
diff --git a/src/StrongTypes/Numbers/NonPositive.cs b/src/StrongTypes/Numbers/NonPositive.cs
index 8cc8e18..75477eb 100644
--- a/src/StrongTypes/Numbers/NonPositive.cs
+++ b/src/StrongTypes/Numbers/NonPositive.cs
@@ -1,6 +1,5 @@
#nullable enable
-using System;
using System.Numerics;
using System.Text.Json.Serialization;
@@ -10,18 +9,14 @@ namespace StrongTypes;
/// A numeric value guaranteed to be less than or equal to T.Zero.
///
///
-/// Construct via or . Unlike
+/// Construct via or Create (generated). Unlike
/// , default(NonPositive<T>) wraps
/// T.Zero and therefore does satisfy the invariant, though the
/// factories remain the intended entry point.
///
+[NumericWrapper(InvariantDescription = "non-positive", GenerateSum = true)]
[JsonConverter(typeof(NumericStrongTypeJsonConverterFactory))]
-public readonly struct NonPositive :
- IEquatable>,
- IEquatable,
- IComparable>,
- IComparable,
- IComparable
+public readonly partial struct NonPositive
where T : INumber
{
private NonPositive(T value)
@@ -31,10 +26,6 @@ private NonPositive(T value)
public T Value { get; }
- public static implicit operator T(NonPositive value) => value.Value;
-
- public static explicit operator NonPositive(T value) => Create(value);
-
///
/// Returns a wrapping , or
/// null if is greater than zero.
@@ -43,64 +34,4 @@ private NonPositive(T value)
{
return value <= T.Zero ? new NonPositive(value) : null;
}
-
- ///
- /// Returns a wrapping .
- /// Throws if is greater
- /// than zero.
- ///
- public static NonPositive Create(T value)
- {
- return TryCreate(value)
- ?? throw new ArgumentException($"Value must be non-positive, but was '{value}'.", nameof(value));
- }
-
- public override int GetHashCode() => Value.GetHashCode();
-
- public override bool Equals(object? obj) =>
- obj switch
- {
- NonPositive other => Equals(other),
- T other => Equals(other),
- _ => false
- };
-
- public bool Equals(NonPositive other) => Value.Equals(other.Value);
-
- public bool Equals(T? other) => other is not null && Value.Equals(other);
-
- public static bool operator ==(NonPositive left, NonPositive right) => left.Equals(right);
- public static bool operator !=(NonPositive left, NonPositive right) => !left.Equals(right);
- public static bool operator ==(NonPositive left, T right) => left.Value.Equals(right);
- public static bool operator !=(NonPositive left, T right) => !left.Value.Equals(right);
- public static bool operator ==(T left, NonPositive right) => right.Value.Equals(left);
- public static bool operator !=(T left, NonPositive right) => !right.Value.Equals(left);
-
- public int CompareTo(NonPositive other) => Value.CompareTo(other.Value);
-
- public int CompareTo(T? other) => other is null ? 1 : Value.CompareTo(other);
-
- int IComparable.CompareTo(object? obj) =>
- obj switch
- {
- null => 1,
- NonPositive other => CompareTo(other),
- T other => CompareTo(other),
- _ => throw new ArgumentException($"Object must be of type {nameof(NonPositive)} or {typeof(T).Name}.", nameof(obj))
- };
-
- public static bool operator <(NonPositive left, NonPositive right) => left.CompareTo(right) < 0;
- public static bool operator <=(NonPositive left, NonPositive right) => left.CompareTo(right) <= 0;
- public static bool operator >(NonPositive left, NonPositive right) => left.CompareTo(right) > 0;
- public static bool operator >=(NonPositive left, NonPositive right) => left.CompareTo(right) >= 0;
- public static bool operator <(NonPositive left, T right) => left.Value.CompareTo(right) < 0;
- public static bool operator <=(NonPositive left, T right) => left.Value.CompareTo(right) <= 0;
- public static bool operator >(NonPositive left, T right) => left.Value.CompareTo(right) > 0;
- public static bool operator >=(NonPositive left, T right) => left.Value.CompareTo(right) >= 0;
- public static bool operator <(T left, NonPositive right) => left.CompareTo(right.Value) < 0;
- public static bool operator <=(T left, NonPositive right) => left.CompareTo(right.Value) <= 0;
- public static bool operator >(T left, NonPositive right) => left.CompareTo(right.Value) > 0;
- public static bool operator >=(T left, NonPositive right) => left.CompareTo(right.Value) >= 0;
-
- public override string ToString() => Value.ToString() ?? string.Empty;
}
diff --git a/src/StrongTypes/Numbers/NumericWrapperAttribute.cs b/src/StrongTypes/Numbers/NumericWrapperAttribute.cs
new file mode 100644
index 0000000..a63ad6e
--- /dev/null
+++ b/src/StrongTypes/Numbers/NumericWrapperAttribute.cs
@@ -0,0 +1,38 @@
+#nullable enable
+
+using System;
+
+namespace StrongTypes;
+
+///
+/// Marks a partial struct as a numeric strong-type wrapper. The source
+/// generator fills in the standard equality, comparison, operator, conversion,
+/// and Create-factory boilerplate, as well as LINQ-style extension methods
+/// on IEnumerable<Self>.
+///
+///
+/// The target must declare:
+///
+/// - a public Value instance property exposing the underlying numeric
+/// value;
+/// - a public static TryCreate method returning Self? that
+/// encodes the validation rule.
+///
+///
+[AttributeUsage(AttributeTargets.Struct, AllowMultiple = false, Inherited = false)]
+public sealed class NumericWrapperAttribute : Attribute
+{
+ ///
+ /// Short adjective phrase used in the Create-throws message, e.g.
+ /// "positive" or "non-negative". The final message reads
+ /// $"Value must be {InvariantDescription}, but was '{value}'.".
+ ///
+ public string InvariantDescription { get; set; } = "valid";
+
+ ///
+ /// Emit a Sum extension on IEnumerable<Self>. Only set
+ /// to true when the wrapper's invariant is closed under addition —
+ /// e.g. NonNegative yes, bounded ranges no.
+ ///
+ public bool GenerateSum { get; set; }
+}
diff --git a/src/StrongTypes/Numbers/Positive.cs b/src/StrongTypes/Numbers/Positive.cs
index 706efe4..31d5b80 100644
--- a/src/StrongTypes/Numbers/Positive.cs
+++ b/src/StrongTypes/Numbers/Positive.cs
@@ -1,6 +1,5 @@
#nullable enable
-using System;
using System.Numerics;
using System.Text.Json.Serialization;
@@ -10,18 +9,14 @@ namespace StrongTypes;
/// A numeric value guaranteed to be strictly greater than T.Zero.
///
///
-/// Construct via or . Internally the
-/// value is stored as an offset from T.One so that default(Positive<T>)
-/// represents T.One — i.e. the zero-initialized struct still satisfies
-/// the positivity invariant.
+/// Construct via or Create (generated). Internally
+/// the value is stored as an offset from T.One so that
+/// default(Positive<T>) represents T.One — i.e. the
+/// zero-initialized struct still satisfies the positivity invariant.
///
+[NumericWrapper(InvariantDescription = "positive", GenerateSum = true)]
[JsonConverter(typeof(NumericStrongTypeJsonConverterFactory))]
-public readonly struct Positive :
- IEquatable>,
- IEquatable,
- IComparable>,
- IComparable,
- IComparable
+public readonly partial struct Positive
where T : INumber
{
// Stored as (Value - T.One); default(Positive) therefore represents Value == T.One.
@@ -34,10 +29,6 @@ private Positive(T offset)
public T Value => _offset + T.One;
- public static implicit operator T(Positive value) => value.Value;
-
- public static explicit operator Positive(T value) => Create(value);
-
///
/// Returns a wrapping , or
/// null if is not strictly greater than zero.
@@ -46,64 +37,4 @@ private Positive(T offset)
{
return value > T.Zero ? new Positive(value - T.One) : null;
}
-
- ///
- /// Returns a wrapping .
- /// Throws if is not
- /// strictly greater than zero.
- ///
- public static Positive Create(T value)
- {
- return TryCreate(value)
- ?? throw new ArgumentException($"Value must be positive, but was '{value}'.", nameof(value));
- }
-
- public override int GetHashCode() => Value.GetHashCode();
-
- public override bool Equals(object? obj) =>
- obj switch
- {
- Positive other => Equals(other),
- T other => Equals(other),
- _ => false
- };
-
- public bool Equals(Positive other) => Value.Equals(other.Value);
-
- public bool Equals(T? other) => other is not null && Value.Equals(other);
-
- public static bool operator ==(Positive left, Positive right) => left.Equals(right);
- public static bool operator !=(Positive left, Positive right) => !left.Equals(right);
- public static bool operator ==(Positive left, T right) => left.Value.Equals(right);
- public static bool operator !=(Positive left, T right) => !left.Value.Equals(right);
- public static bool operator ==(T left, Positive right) => right.Value.Equals(left);
- public static bool operator !=(T left, Positive right) => !right.Value.Equals(left);
-
- public int CompareTo(Positive other) => Value.CompareTo(other.Value);
-
- public int CompareTo(T? other) => other is null ? 1 : Value.CompareTo(other);
-
- int IComparable.CompareTo(object? obj) =>
- obj switch
- {
- null => 1,
- Positive other => CompareTo(other),
- T other => CompareTo(other),
- _ => throw new ArgumentException($"Object must be of type {nameof(Positive)} or {typeof(T).Name}.", nameof(obj))
- };
-
- public static bool operator <(Positive left, Positive right) => left.CompareTo(right) < 0;
- public static bool operator <=(Positive left, Positive right) => left.CompareTo(right) <= 0;
- public static bool operator >(Positive left, Positive right) => left.CompareTo(right) > 0;
- public static bool operator >=(Positive left, Positive right) => left.CompareTo(right) >= 0;
- public static bool operator <(Positive left, T right) => left.Value.CompareTo(right) < 0;
- public static bool operator <=(Positive left, T right) => left.Value.CompareTo(right) <= 0;
- public static bool operator >(Positive left, T right) => left.Value.CompareTo(right) > 0;
- public static bool operator >=(Positive left, T right) => left.Value.CompareTo(right) >= 0;
- public static bool operator <(T left, Positive right) => left.CompareTo(right.Value) < 0;
- public static bool operator <=(T left, Positive right) => left.CompareTo(right.Value) <= 0;
- public static bool operator >(T left, Positive right) => left.CompareTo(right.Value) > 0;
- public static bool operator >=(T left, Positive right) => left.CompareTo(right.Value) >= 0;
-
- public override string ToString() => Value.ToString() ?? string.Empty;
}
diff --git a/src/StrongTypes/Numbers/SumExtensions.cs b/src/StrongTypes/Numbers/SumExtensions.cs
deleted file mode 100644
index 339f2b5..0000000
--- a/src/StrongTypes/Numbers/SumExtensions.cs
+++ /dev/null
@@ -1,74 +0,0 @@
-#nullable enable
-
-using System;
-using System.Collections.Generic;
-using System.Numerics;
-
-namespace StrongTypes;
-
-///
-/// Sum extensions for every numeric strong type. Kept together as a single
-/// feature slice — adding a new wrapper adds one method here rather than a new file.
-///
-public static class SumExtensions
-{
- ///
- /// Sums the values of a sequence of .
- ///
- /// The accumulated sum overflows .
- /// is empty (the sum is T.Zero, which is not positive).
- public static Positive Sum(this IEnumerable> values) where T : INumber
- {
- T sum = T.Zero;
- foreach (var value in values)
- {
- sum = checked(sum + value.Value);
- }
- return Positive.Create(sum);
- }
-
- ///
- /// Sums the values of a sequence of . An empty
- /// sequence yields T.Zero, which is a valid non-negative value.
- ///
- /// The accumulated sum overflows .
- public static NonNegative Sum(this IEnumerable> values) where T : INumber
- {
- T sum = T.Zero;
- foreach (var value in values)
- {
- sum = checked(sum + value.Value);
- }
- return NonNegative.Create(sum);
- }
-
- ///
- /// Sums the values of a sequence of .
- ///
- /// The accumulated sum overflows .
- /// is empty (the sum is T.Zero, which is not negative).
- public static Negative Sum(this IEnumerable> values) where T : INumber
- {
- T sum = T.Zero;
- foreach (var value in values)
- {
- sum = checked(sum + value.Value);
- }
- return Negative.Create(sum);
- }
-
- ///
- /// Sums the values of a sequence of . An empty
- /// sequence yields T.Zero, which is a valid non-positive value.
- ///
- /// The accumulated sum overflows .
- public static NonPositive Sum(this IEnumerable> values) where T : INumber
- {
- T sum = T.Zero;
- foreach (var value in values)
- {
- sum = checked(sum + value.Value);
- }
- return NonPositive.Create(sum);
- }
-}
diff --git a/src/StrongTypes/StrongTypes.csproj b/src/StrongTypes/StrongTypes.csproj
index abc89c6..d554b9e 100644
--- a/src/StrongTypes/StrongTypes.csproj
+++ b/src/StrongTypes/StrongTypes.csproj
@@ -29,6 +29,20 @@
+
+
+
+
+ $(TargetsForTfmSpecificContentInPackage);_IncludeSourceGeneratorInPackage
+
+
+
+
+
+