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 + + + + + +