From c31b2b556bf1f682045225d4415f30a276ca51f6 Mon Sep 17 00:00:00 2001 From: Chris Cameron Date: Thu, 13 Jun 2019 16:56:49 -0400 Subject: [PATCH] feat: Adding methods for remapping and clamping numeric values to ranges --- .../Attributes/RangeAttributeTest.cs | 52 ---- ICD.Common.Utils/Attributes/RangeAttribute.cs | 283 +++++++++++++----- 2 files changed, 215 insertions(+), 120 deletions(-) diff --git a/ICD.Common.Utils.Tests/Attributes/RangeAttributeTest.cs b/ICD.Common.Utils.Tests/Attributes/RangeAttributeTest.cs index e777821..45c5a13 100644 --- a/ICD.Common.Utils.Tests/Attributes/RangeAttributeTest.cs +++ b/ICD.Common.Utils.Tests/Attributes/RangeAttributeTest.cs @@ -29,8 +29,6 @@ namespace ICD.Common.Utils.Tests.Attributes #region Methods - #region Remap To - [TestCase((double)0, (double)0)] [TestCase((double)1, (double)1)] [TestCase(ushort.MaxValue, double.MaxValue)] @@ -40,18 +38,6 @@ namespace ICD.Common.Utils.Tests.Attributes Assert.AreEqual(expected, RangeAttribute.RemapToDouble(value)); } - [TestCase((double)0, (ushort)32767)] - [TestCase(double.MaxValue, ushort.MaxValue)] - [TestCase(double.MinValue, ushort.MinValue)] - public void RemapToUShortTest(object value, ushort expected) - { - Assert.AreEqual(expected, RangeAttribute.RemapToUShort(value)); - } - - #endregion - - #region Remap From - [TestCase((double)0, typeof(ushort), (ushort)32767)] [TestCase(double.MinValue, typeof(ushort), ushort.MinValue)] [TestCase(double.MaxValue, typeof(ushort), ushort.MaxValue)] @@ -60,44 +46,6 @@ namespace ICD.Common.Utils.Tests.Attributes Assert.AreEqual(expected, RangeAttribute.RemapFromDouble(value, type)); } - [TestCase((ushort)0, typeof(double), double.MinValue)] - [TestCase(ushort.MinValue, typeof(double), double.MinValue)] - [TestCase(ushort.MaxValue, typeof(double), double.MaxValue)] - public void RemapFromUShortTest(ushort value, Type type, object expected) - { - Assert.AreEqual(expected, RangeAttribute.RemapFromUShort(value, type)); - } - - #endregion - - #region Remap To Attribute And Clamp - - public void RemapAndClampToDoubleTest(object value, object min, object max, double expected) - { - Assert.AreEqual(expected, new RangeAttribute(min, max).RemapAndClampToDouble(value)); - } - - public void RemapAndClampToUShortTest(object value, object min, object max, ushort expected) - { - Assert.AreEqual(expected, new RangeAttribute(min, max).RemapAndClampToDouble(value)); - } - - #endregion - - #region Remap And Clamp - - public void RemapAndClampFromDoubleTest(double value, object min, object max, object expected) - { - Assert.AreEqual(expected, new RangeAttribute(min, max).RemapAndClamp(value)); - } - - public void RemapAndClampFromUShortTest(ushort value, object min, object max, object expected) - { - Assert.AreEqual(expected, new RangeAttribute(min, max).RemapAndClamp(value)); - } - - #endregion - #endregion } } diff --git a/ICD.Common.Utils/Attributes/RangeAttribute.cs b/ICD.Common.Utils/Attributes/RangeAttribute.cs index 9590381..7220488 100644 --- a/ICD.Common.Utils/Attributes/RangeAttribute.cs +++ b/ICD.Common.Utils/Attributes/RangeAttribute.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Globalization; using ICD.Common.Utils.Extensions; namespace ICD.Common.Utils.Attributes @@ -13,6 +14,29 @@ namespace ICD.Common.Utils.Attributes AttributeTargets.ReturnValue)] public sealed class RangeAttribute : AbstractIcdAttribute { + /// + /// Remaps from the source numeric min/max to double min/max. + /// + private static readonly Dictionary> s_Clamp = + new Dictionary> + { + // Duh + {typeof(double), o => o}, + + // Signed + {typeof(short), o => o < short.MinValue ? short.MinValue : o > short.MaxValue ? short.MaxValue : o}, + {typeof(int), o => o < int.MinValue ? int.MinValue : o > int.MaxValue ? int.MaxValue : o}, + {typeof(long), o => o < long.MinValue ? long.MinValue : o > long.MaxValue ? long.MaxValue : o}, + {typeof(float),o => o < float.MinValue ? float.MinValue : o > float.MaxValue ? float.MaxValue : o}, + {typeof(decimal), o => o < (double)decimal.MinValue ? (double)decimal.MinValue : o > (double)decimal.MaxValue ? (double)decimal.MaxValue : o}, + + // Unsigned + {typeof(ushort), o => o < ushort.MinValue ? ushort.MinValue : o > ushort.MaxValue ? ushort.MaxValue : o}, + {typeof(uint), o => o < uint.MinValue ? uint.MinValue : o > uint.MaxValue ? uint.MaxValue : o}, + {typeof(ulong), o => o < ulong.MinValue ? ulong.MinValue : o > ulong.MaxValue ? ulong.MaxValue : o}, + {typeof(byte), o => o < byte.MinValue ? byte.MinValue : o > byte.MaxValue ? byte.MaxValue : o} + }; + /// /// Remaps from the source numeric min/max to double min/max. /// @@ -59,6 +83,52 @@ namespace ICD.Common.Utils.Attributes {typeof(byte), v => (byte)((v / double.MaxValue + 1) / 2 * byte.MaxValue)} }; + /// + /// Gets the min value of a given numeric type as a double. + /// + private static readonly Dictionary s_MinAsDouble = + new Dictionary + { + // Duh + {typeof(double), double.MinValue}, + + // Signed + {typeof(short), Convert.ToDouble(short.MinValue)}, + {typeof(int), Convert.ToDouble(int.MinValue)}, + {typeof(long), Convert.ToDouble(long.MinValue)}, + {typeof(float), Convert.ToDouble(float.MinValue)}, + {typeof(decimal), Convert.ToDouble(decimal.MinValue)}, + + // Unsigned + {typeof(ushort), Convert.ToDouble(ushort.MinValue)}, + {typeof(uint), Convert.ToDouble(uint.MinValue)}, + {typeof(ulong), Convert.ToDouble(ulong.MinValue)}, + {typeof(byte), Convert.ToDouble(byte.MinValue)} + }; + + /// + /// Gets the min value of a given numeric type as a double. + /// + private static readonly Dictionary s_MaxAsDouble = + new Dictionary + { + // Duh + {typeof(double), double.MaxValue}, + + // Signed + {typeof(short), Convert.ToDouble(short.MaxValue)}, + {typeof(int), Convert.ToDouble(int.MaxValue)}, + {typeof(long), Convert.ToDouble(long.MaxValue)}, + {typeof(float), Convert.ToDouble(float.MaxValue)}, + {typeof(decimal), Convert.ToDouble(decimal.MaxValue)}, + + // Unsigned + {typeof(ushort), Convert.ToDouble(ushort.MaxValue)}, + {typeof(uint), Convert.ToDouble(uint.MaxValue)}, + {typeof(ulong), Convert.ToDouble(ulong.MaxValue)}, + {typeof(byte), Convert.ToDouble(byte.MaxValue)} + }; + private readonly object m_Min; private readonly object m_Max; @@ -215,8 +285,6 @@ namespace ICD.Common.Utils.Attributes #region Methods - #region Remap To - /// /// Remaps the given numeric value from its min/max range into double min/max range. /// @@ -234,20 +302,6 @@ namespace ICD.Common.Utils.Attributes return remap(value); } - /// - /// Remaps the given numeric value from its min/max range into ushort min/max range. - /// - /// - /// - public static ushort RemapToUShort(object value) - { - return (ushort)(ushort.MaxValue * (RemapToDouble(value) / double.MaxValue + 1) / 2); - } - - #endregion - - #region Remap From - /// /// Remaps the given double value from its min/max range into the target type min/max range. /// @@ -267,82 +321,175 @@ namespace ICD.Common.Utils.Attributes } /// - /// Remaps the given ushort value from its min/max range into the target type min/max range. + /// Clamps the given numeric value into the valid ranges of the target numeric type. /// /// /// /// - public static object RemapFromUShort(ushort value, Type type) + public static object Clamp(object value, Type type) { + if (value == null) + throw new ArgumentNullException("value"); + + if (type == null) + throw new ArgumentNullException("type"); + + if (!type.IsNumeric()) + throw new ArgumentException("Target type is not numeric"); + + if (!value.GetType().IsNumeric()) + throw new ArgumentException("Source value is not numeric"); + + double doubleValue = Convert.ToDouble(value); + double clamped = Clamp(doubleValue, type); + + return Convert.ChangeType(clamped, value.GetType(), CultureInfo.InvariantCulture); + } + + /// + /// Clamps the given double value into the valid ranges of the target numeric type. + /// + /// + /// + /// + public static double Clamp(double value, Type type) + { + if (type == null) + throw new ArgumentNullException("type"); + + Func clamp; + if (!s_Clamp.TryGetValue(type, out clamp)) + throw new NotSupportedException("Value type is not supported."); + + return clamp(value); + } + + /// + /// Remaps the numeric value into the min-max range of the target numeric type. + /// + /// + /// + /// + public static object Remap(object value, Type type) + { + if (value == null) + throw new ArgumentNullException("value"); + + if (type == null) + throw new ArgumentNullException("type"); + + if (!type.IsNumeric()) + throw new ArgumentException("Target type is not numeric"); + + if (!value.GetType().IsNumeric()) + throw new ArgumentException("Source value is not numeric"); + double intermediate = RemapToDouble(value); - return RemapFromDouble(intermediate, type); + object remapped = RemapFromDouble(intermediate, type); + + return Convert.ChangeType(remapped, value.GetType(), CultureInfo.InvariantCulture); + } + + /// + /// Clamps the given numeric value to the defined min/max then remaps to the target numeric type. + /// + /// + /// + /// + public object ClampMinMaxThenRemap(object value, Type type) + { + if (value == null) + throw new ArgumentNullException("value"); + + if (type == null) + throw new ArgumentNullException("type"); + + if (!type.IsNumeric()) + throw new ArgumentException("Target type is not numeric"); + + if (!value.GetType().IsNumeric()) + throw new ArgumentException("Source value is not numeric"); + + double min = Convert.ToDouble(Min); + double max = Convert.ToDouble(Max); + double doubleValue = Convert.ToDouble(value); + + double clamped = MathUtils.Clamp(doubleValue, min, max); + object remapped = Remap(clamped, type); + + return Convert.ChangeType(remapped, value.GetType(), CultureInfo.InvariantCulture); + } + + /// + /// Remaps the given numeric value to the defined min/max. + /// + /// + /// + public object RemapMinMax(object value) + { + if (value == null) + throw new ArgumentNullException("value"); + + if (!value.GetType().IsNumeric()) + throw new ArgumentException("Source value is not numeric"); + + double sourceMin = GetMinAsDouble(value.GetType()); + double sourceMax = GetMaxAsDouble(value.GetType()); + + double targetMin = Convert.ToDouble(Min); + double targetMax = Convert.ToDouble(Max); + + double doubleValue = Convert.ToDouble(value); + + double remapped = MathUtils.MapRange(sourceMin, sourceMax, targetMin, targetMax, doubleValue); + + return Convert.ChangeType(remapped, Min.GetType(), CultureInfo.InvariantCulture); } #endregion - #region Remap To Attribute And Clamp + #region Private Methods /// - /// Remaps the given numeric value from its min/max range into the min/max range of this attribute, - /// clamps, and then remaps the result to double. + /// Gets the min value for the given numeric type as a double. /// - /// + /// /// - public double RemapAndClampToDouble(object value) + private static double GetMinAsDouble(Type type) { - double intermediate = RemapToDouble(value); - double min = RemapToDouble(Min); - double max = RemapToDouble(Max); + if (type == null) + throw new ArgumentNullException("type"); - return MathUtils.Clamp(intermediate, min, max); + if (!type.IsNumeric()) + throw new ArgumentException("Target type is not numeric"); + + double min; + if (!s_MinAsDouble.TryGetValue(type, out min)) + throw new NotSupportedException("Type is not supported."); + + return min; } /// - /// Remaps the given numeric value from its min/max range into the min/max range of this attribute, - /// clamps, and then remaps the result to ushort. + /// Gets the max value for the given numeric type as a double. /// - /// + /// /// - public ushort RemapAndClampToUShort(object value) + private static double GetMaxAsDouble(Type type) { - double intermediate = RemapAndClampToDouble(value); - return RemapToUShort(intermediate); + if (type == null) + throw new ArgumentNullException("type"); + + if (!type.IsNumeric()) + throw new ArgumentException("Target type is not numeric"); + + double max; + if (!s_MaxAsDouble.TryGetValue(type, out max)) + throw new NotSupportedException("Type is not supported."); + + return max; } #endregion - - #region Remap And Clamp - - /// - /// Remaps the ushort value from its min/max range into the min/max range of this attribute, - /// clamps and returns the result. - /// - /// - /// - public object RemapAndClamp(double value) - { - double min = RemapToDouble(Min); - double max = RemapToDouble(Max); - - double clamp = MathUtils.Clamp(value, min, max); - - return RemapFromDouble(clamp, m_Min.GetType()); - } - - /// - /// Remaps the ushort value from its min/max range into the min/max range of this attribute, - /// clamps and returns the result. - /// - /// - /// - public object RemapAndClamp(ushort value) - { - double intermediate = RemapToDouble(value); - return RemapAndClamp(intermediate); - } - - #endregion - - #endregion } } \ No newline at end of file