< Summary

Class:Towel.Measurements.Measurement
Assembly:Towel
File(s):File 1: /home/runner/work/Towel/Towel/Sources/Towel/Measurements/Measurement.cs
Covered lines:0
Uncovered lines:134
Coverable lines:134
Total lines:239
Line coverage:0% (0 of 134)
Covered branches:0
Total branches:54
Branch coverage:0% (0 of 54)

Metrics

MethodBranch coverage Cyclomatic complexity Line coverage
File 1: Convert(...)100%10%
File 1: .ctor(...)100%10%
File 1: BuildParsingLibrary()0%140%
File 1: BuildTypeSpecificParsingLibrary()0%60%
File 1: TryParse(...)0%300%
File 1: TryParse(...)0%40%

File(s)

/home/runner/work/Towel/Towel/Sources/Towel/Measurements/Measurement.cs

#LineLine coverage
 1using System.Reflection;
 2using System.Text;
 3using System.Text.RegularExpressions;
 4
 5namespace Towel.Measurements;
 6
 7/// <summary>Static class with methods regarding measurements.</summary>
 8public static class Measurement
 9{
 10  #region Convert
 11
 12  /// <summary>Interface for unit conversion.</summary>
 13  /// <typeparam name="TUnitsType">The unit type of the interface.</typeparam>
 14  public interface IUnits<TUnitsType>
 15  {
 16    /// <summary>Converts the units of measurement of a value.</summary>
 17    /// <typeparam name="T">The generic type of the value to convert.</typeparam>
 18    /// <param name="value">The value to be converted.</param>
 19    /// <param name="from">The current units of the value.</param>
 20    /// <param name="to">The desired units of the value.</param>
 21    /// <returns>The value converted into the desired units.</returns>
 22    T Convert<T>(T value, TUnitsType from, TUnitsType to);
 23  }
 24
 25  /// <summary>Converts the units of measurement of a value.</summary>
 26  /// <typeparam name="T">The generic type of the value to convert.</typeparam>
 27  /// <typeparam name="TUnitsType">The type of units to be converted.</typeparam>
 28  /// <param name="value">The value to be converted.</param>
 29  /// <param name="from">The current units of the value.</param>
 30  /// <param name="to">The desired units of the value.</param>
 31  /// <returns>The value converted into the desired units.</returns>
 32  public static T Convert<T, TUnitsType>(T value, TUnitsType from, TUnitsType to)
 33    where TUnitsType : IUnits<TUnitsType>
 034  {
 035    return from.Convert(value, from, to);
 036  }
 37
 38  #endregion
 39
 40  #region Parse
 41
 42  #region Attributes
 43
 44  [AttributeUsage(AttributeTargets.Method)]
 45  internal class ParseableAttribute : Attribute
 46  {
 47    internal string Key;
 048    internal ParseableAttribute(string key) { Key = key; }
 49  }
 50
 51  [AttributeUsage(AttributeTargets.Enum)]
 52  internal class ParseableUnitAttribute : Attribute { }
 53
 54  #endregion
 55
 56  #region Parsing Libaries
 57
 58  internal static bool ParsingLibraryBuilt = false;
 59  internal static string? AllUnitsRegexPattern;
 60  internal static IMap<string, string>? UnitStringToUnitTypeString;
 61  internal static IMap<Enum, string>? UnitStringToEnumMap;
 62
 63  internal static void BuildParsingLibrary()
 064  {
 65    // make a regex pattern with all the currently supported unit types and
 66    // build the unit string to unit type string map
 067    var strings = new ListArray<string>();
 068    var unitStringToUnitTypeString = new MapHashLinked<string, string, StringEquate, StringHash>();
 069    foreach (Type type in Assembly.GetExecutingAssembly().GetTypesWithAttribute<ParseableUnitAttribute>())
 070    {
 071      if (!type.IsEnum)
 072      {
 073        throw new Exception("There is a bug in Towel. " + nameof(ParseableUnitAttribute) + " is on a non enum type.");
 74      }
 075      if (!type.Name.Equals("Units") || type.DeclaringType is null)
 076      {
 077        throw new Exception("There is a bug in Towel. A unit type definition does not follow the required structure.");
 78      }
 079      string unitTypeString = type.DeclaringType.Name;
 080      foreach (Enum @enum in Enum.GetValues(type).Cast<Enum>())
 081      {
 082        strings.Add(@enum.ToString());
 083        unitStringToUnitTypeString.Add(@enum.ToString(), unitTypeString);
 084      }
 085    }
 086    strings.Add(@"\*");
 087    strings.Add(@"\/");
 088    AllUnitsRegexPattern = string.Join("|", strings);
 089    UnitStringToUnitTypeString = unitStringToUnitTypeString;
 90
 91    // make the Enum arrays to units map
 092    IMap<Enum, string> unitStringToEnumMap = new MapHashLinked<Enum, string, StringEquate, StringHash>();
 093    foreach (Type type in Assembly.GetExecutingAssembly().GetTypesWithAttribute<ParseableUnitAttribute>())
 094    {
 095      foreach (Enum @enum in Enum.GetValues(type))
 096      {
 097        unitStringToEnumMap.Add(@enum.ToString(), @enum);
 098      }
 099    }
 0100    UnitStringToEnumMap = unitStringToEnumMap;
 101
 0102    ParsingLibraryBuilt = true;
 0103  }
 104
 105  internal static class TypeSpecificParsingLibrary<T>
 106  {
 107    internal static bool TypeSpecificParsingLibraryBuilt = false;
 108    internal static IMap<Func<T, object[], object>, string>? UnitsStringsToFactoryFunctions;
 109
 110    internal static void BuildTypeSpecificParsingLibrary()
 0111    {
 112      // make the delegates for constructing the measurements
 0113      var unitsStringsToFactoryFunctions = new MapHashLinked<Func<T, object[], object>, string, StringEquate, StringHash
 0114      foreach (MethodInfo methodInfo in typeof(ParsingFunctions).GetMethods())
 0115      {
 0116        if (methodInfo.DeclaringType == typeof(ParsingFunctions))
 0117        {
 0118          MethodInfo genericMethodInfo = methodInfo.MakeGenericMethod(typeof(T));
 0119          ParseableAttribute? parsableAttribute = genericMethodInfo.GetCustomAttribute<ParseableAttribute>();
 0120          if (parsableAttribute is null)
 0121          {
 0122            throw new TowelBugException($"Encountered null {nameof(ConstructorInfo)} in {nameof(BuildTypeSpecificParsing
 123          }
 0124          Func<T, object[], object> factory = genericMethodInfo.CreateDelegate<Func<T, object[], object>>();
 0125          unitsStringsToFactoryFunctions.Add(parsableAttribute.Key, factory);
 0126        }
 0127      }
 0128      UnitsStringsToFactoryFunctions = unitsStringsToFactoryFunctions;
 129
 0130      TypeSpecificParsingLibraryBuilt = true;
 0131    }
 132  }
 133
 134  #endregion
 135
 136  /// <summary>Parses a measurement from a string.</summary>
 137  /// <typeparam name="T">The numeric type to parse the quantity as.</typeparam>
 138  /// <param name="string">The string to parse.</param>
 139  /// <param name="tryParse">Explicit try parse function for the numeric type.</param>
 140  /// <returns>True if successful or false if not.</returns>
 141  public static (bool Success, object? Value) TryParse<T>(string @string, Func<string, (bool Success, T? Value)>? tryPar
 0142  {
 0143    if (!ParsingLibraryBuilt)
 0144    {
 0145      BuildParsingLibrary();
 0146    }
 0147    if (!TypeSpecificParsingLibrary<T>.TypeSpecificParsingLibraryBuilt)
 0148    {
 0149      TypeSpecificParsingLibrary<T>.BuildTypeSpecificParsingLibrary();
 0150    }
 151
 0152    ListArray<object> parameters = new();
 0153    bool AtLeastOneUnit = false;
 0154    bool? numerator = null;
 0155    MatchCollection matchCollection = Regex.Matches(@string, AllUnitsRegexPattern!);
 0156    if (matchCollection.Count <= 0 || matchCollection[0].Index <= 0)
 0157    {
 0158      return (false, default);
 159    }
 0160    string numericString = @string.Substring(0, matchCollection[0].Index);
 161    T value;
 162    try
 0163    {
 0164      value = Symbolics.ParseAndSimplifyToConstant<T>(numericString, tryParse);
 0165    }
 0166    catch
 0167    {
 0168      return (false, default);
 169    }
 0170    StringBuilder stringBuilder = new();
 0171    foreach (Match match in matchCollection)
 0172    {
 0173      string matchValue = match.Value;
 0174      if (matchValue.Equals("*") || matchValue.Equals("/"))
 0175      {
 0176        if (numerator is not null)
 0177        {
 0178          return (false, default);
 179        }
 0180        if (!AtLeastOneUnit)
 0181        {
 0182          return (false, default);
 183        }
 0184        numerator = matchValue.Equals("*");
 0185        continue;
 186      }
 0187      var (success, exception, @enum) = UnitStringToEnumMap!.TryGet(match.Value);
 0188      if (!success)
 0189      {
 0190        return (false, default);
 191      }
 0192      if (!AtLeastOneUnit)
 0193      {
 0194        if (numerator is not null)
 0195        {
 0196          return (false, default);
 197        }
 0198        AtLeastOneUnit = true;
 0199        stringBuilder.Append(@enum!.GetType().DeclaringType!.Name);
 0200        parameters.Add(@enum);
 0201      }
 202      else
 0203      {
 0204        if (numerator is null)
 0205        {
 0206          return (false, default);
 207        }
 0208        if (numerator.Value)
 0209        {
 0210          stringBuilder.Append("*" + @enum!.GetType().DeclaringType!.Name);
 0211          parameters.Add(@enum);
 0212        }
 213        else
 0214        {
 0215          stringBuilder.Append("/" + @enum!.GetType().DeclaringType!.Name);
 0216          parameters.Add(@enum);
 0217        }
 0218      }
 0219      numerator = null;
 0220    }
 0221    if (numerator is not null)
 0222    {
 0223      return (false, default);
 224    }
 0225    string key = stringBuilder.ToString();
 0226    Func<T, object[], object> factory = TypeSpecificParsingLibrary<T>.UnitsStringsToFactoryFunctions![key];
 0227    return (true, factory(value, parameters.ToArray()));
 0228  }
 229
 230  internal static (bool Success, TMeasurement? Value) TryParse<T, TMeasurement>(string @string, Func<string, (bool Succe
 0231  {
 0232    var (success, value) = TryParse(@string, tryParse);
 0233    return success && value is TMeasurement measurement
 0234      ? (true, measurement)
 0235      : (false, default);
 0236  }
 237
 238  #endregion
 239}