diff --git a/src/Nox.Types.EntityFramework/Types/Percentage/PercentageConverter.cs b/src/Nox.Types.EntityFramework/Types/Percentage/PercentageConverter.cs new file mode 100644 index 0000000..fa8530f --- /dev/null +++ b/src/Nox.Types.EntityFramework/Types/Percentage/PercentageConverter.cs @@ -0,0 +1,13 @@ +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +namespace Nox.Types.EntityFramework.Types; + +public class PercentageConverter : ValueConverter +{ + /// + /// Initializes a new instance of the class. + /// + public PercentageConverter() : base(percentage => percentage.Value, percentageValue => Percentage.From(percentageValue)) + { + } +} diff --git a/src/Nox.Types/Types/Percentage/Percentage.cs b/src/Nox.Types/Types/Percentage/Percentage.cs index 3b856b6..d738993 100644 --- a/src/Nox.Types/Types/Percentage/Percentage.cs +++ b/src/Nox.Types/Types/Percentage/Percentage.cs @@ -1,9 +1,70 @@ +using System; +using System.Globalization; + namespace Nox.Types; +/// +/// Represents a Nox type and value object. +/// +public sealed class Percentage : ValueObject +{ + private PercentageTypeOptions _percentageTypeOptions = new(); + + /// + /// Creates a new instance of class with the specified values. + /// + /// The value to create the with + /// + /// + public static Percentage From(float value, PercentageTypeOptions options) + { + var newObject = new Percentage + { + Value = value, + _percentageTypeOptions = options + }; + + var validationResult = newObject.Validate(); + + if (!validationResult.IsValid) + { + throw new TypeValidationException(validationResult.Errors); + } + + return newObject; + } + /// - /// Represents a Nox type and value object. + /// Validates a object. /// - /// Placeholder, needs to be implemented - public sealed class Percentage : ValueObject + /// true if the value is valid. + internal override ValidationResult Validate() { + var result = base.Validate(); + + if (Value < _percentageTypeOptions.MinValue && !float.IsNaN(Value) && !float.IsInfinity(Value)) + { + result.Errors.Add(new ValidationFailure(nameof(Value), $"Could not create a Nox Percentage type as value {Value} is less than than the minimum specified value of {_percentageTypeOptions.MinValue}")); + } + + if (Value > _percentageTypeOptions.MaxValue && !float.IsNaN(Value) && !float.IsInfinity(Value)) + { + result.Errors.Add(new ValidationFailure(nameof(Value), $"Could not create a Nox Percentage type a value {Value} is greater than than the maximum specified value of {_percentageTypeOptions.MaxValue}")); + } + + Value = (float)Math.Round(Value, _percentageTypeOptions.Digits); + + return result; } + + public override string ToString() + => $"{(Value * 100).ToString($"0.{new string('#', _percentageTypeOptions.Digits)}", CultureInfo.InvariantCulture)}%"; + + /// + /// Returns a string representation of the object using the specified . + /// + /// The format provider for the length value. + /// A string representation of the object with the value formatted using the specified . + public string ToString(IFormatProvider formatProvider) + => $"{Value.ToString(formatProvider)}%"; +} diff --git a/src/Nox.Types/Types/Percentage/TypeOptions/PercentageTypeOptions.cs b/src/Nox.Types/Types/Percentage/TypeOptions/PercentageTypeOptions.cs new file mode 100644 index 0000000..99d2d70 --- /dev/null +++ b/src/Nox.Types/Types/Percentage/TypeOptions/PercentageTypeOptions.cs @@ -0,0 +1,10 @@ +namespace Nox.Types; +public class PercentageTypeOptions +{ + public static readonly float DefaultMinValue = 0; + public static readonly float DefaultMaxValue = 1; + public float MinValue { get; set; } = DefaultMinValue; + public float MaxValue { get; set; } = DefaultMaxValue; + + public int Digits { get; set; } = 2; +} diff --git a/tests/Nox.Types.Tests/Types/Percentage/PercentageTests.cs b/tests/Nox.Types.Tests/Types/Percentage/PercentageTests.cs index f4f8ce3..d9f7464 100644 --- a/tests/Nox.Types.Tests/Types/Percentage/PercentageTests.cs +++ b/tests/Nox.Types.Tests/Types/Percentage/PercentageTests.cs @@ -1,11 +1,95 @@ -// ReSharper disable once CheckNamespace +using FluentAssertions; +using System.Globalization; + namespace Nox.Types.Tests.Types; public class PercentageTests { [Fact] - public void When_Create_Should() + public void Percentage_Constructor_ReturnsSameValue() + { + var testPercentage = 0.5f; + + var number = Percentage.From(testPercentage); + + number.Value.Should().Be(testPercentage); + } + + [Fact] + public void Percentage_Constructor_ThrowsException_WhenValueExceedsMaxAllowed() + { + var testPercentage = 3.2f; + + var action = () => Percentage.From(testPercentage); + + action.Should().Throw() + .And.Errors.Should().BeEquivalentTo(new[] { new ValidationFailure("Value", "Could not create a Nox Percentage type a value 3.2 is greater than than the maximum specified value of 1") }); + + } + + [Fact] + public void Percentage_Constructor_ThrowsException_WhenValueIsLessThanMinAllowed() + { + var testPercentage = -0.3f; + + var action = () => Percentage.From(testPercentage); + + action.Should().Throw() + .And.Errors.Should().BeEquivalentTo(new[] { new ValidationFailure("Value", "Could not create a Nox Percentage type as value -0.3 is less than than the minimum specified value of 0") }); + } + + [Fact] + public void Percentage_Constructor_RoundsFloatValues_WhenConstructedWithFloatInput() + { + var testPercentage = 0.4f; + + var percentage = Percentage.From(testPercentage); + + percentage.Value.Should().Be(0.4f); + } + + [Fact] + public void Percentage_ToString_Returns_Value() + { + void Test() + { + var pecentageValue = 0.45f; + + var percentage = Percentage.From(pecentageValue); + + var percentageAsString = percentage.ToString(); + + Assert.Equal("45%", percentageAsString); + } + + TestUtility.RunInInvariantCulture(Test); + } + + [Theory] + [InlineData("en-US")] + [InlineData("pt-PT")] + public void Percentage_ValueInFloat_ToString_IsCultureIndepdendent(string culture) + { + void Test() + { + var percentage = Percentage.From(0.25f); + percentage.ToString().Should().Be("25%"); + } + + TestUtility.RunInCulture(Test, culture); + } + + [Theory] + [InlineData("en-US", "0.43%")] + [InlineData("pt-PT", "0,43%")] + public void Percentage_ValueInFloat_ToString_IsCultureDependent(string culture, string expected) { + void Test() + { + var percentage = Percentage.From(0.43f); + percentage.ToString(new CultureInfo(culture)).Should().Be(expected); + } + TestUtility.RunInCulture(Test, culture); } } \ No newline at end of file