| | 1 | | using System.Reflection; |
| | 2 | | using System.Text; |
| | 3 | | using System.Text.RegularExpressions; |
| | 4 | |
|
| | 5 | | namespace Towel.Measurements; |
| | 6 | |
|
| | 7 | | /// <summary>Static class with methods regarding measurements.</summary> |
| | 8 | | public 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> |
| 0 | 34 | | { |
| 0 | 35 | | return from.Convert(value, from, to); |
| 0 | 36 | | } |
| | 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; |
| 0 | 48 | | 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() |
| 0 | 64 | | { |
| | 65 | | // make a regex pattern with all the currently supported unit types and |
| | 66 | | // build the unit string to unit type string map |
| 0 | 67 | | var strings = new ListArray<string>(); |
| 0 | 68 | | var unitStringToUnitTypeString = new MapHashLinked<string, string, StringEquate, StringHash>(); |
| 0 | 69 | | foreach (Type type in Assembly.GetExecutingAssembly().GetTypesWithAttribute<ParseableUnitAttribute>()) |
| 0 | 70 | | { |
| 0 | 71 | | if (!type.IsEnum) |
| 0 | 72 | | { |
| 0 | 73 | | throw new Exception("There is a bug in Towel. " + nameof(ParseableUnitAttribute) + " is on a non enum type."); |
| | 74 | | } |
| 0 | 75 | | if (!type.Name.Equals("Units") || type.DeclaringType is null) |
| 0 | 76 | | { |
| 0 | 77 | | throw new Exception("There is a bug in Towel. A unit type definition does not follow the required structure."); |
| | 78 | | } |
| 0 | 79 | | string unitTypeString = type.DeclaringType.Name; |
| 0 | 80 | | foreach (Enum @enum in Enum.GetValues(type).Cast<Enum>()) |
| 0 | 81 | | { |
| 0 | 82 | | strings.Add(@enum.ToString()); |
| 0 | 83 | | unitStringToUnitTypeString.Add(@enum.ToString(), unitTypeString); |
| 0 | 84 | | } |
| 0 | 85 | | } |
| 0 | 86 | | strings.Add(@"\*"); |
| 0 | 87 | | strings.Add(@"\/"); |
| 0 | 88 | | AllUnitsRegexPattern = string.Join("|", strings); |
| 0 | 89 | | UnitStringToUnitTypeString = unitStringToUnitTypeString; |
| | 90 | |
|
| | 91 | | // make the Enum arrays to units map |
| 0 | 92 | | IMap<Enum, string> unitStringToEnumMap = new MapHashLinked<Enum, string, StringEquate, StringHash>(); |
| 0 | 93 | | foreach (Type type in Assembly.GetExecutingAssembly().GetTypesWithAttribute<ParseableUnitAttribute>()) |
| 0 | 94 | | { |
| 0 | 95 | | foreach (Enum @enum in Enum.GetValues(type)) |
| 0 | 96 | | { |
| 0 | 97 | | unitStringToEnumMap.Add(@enum.ToString(), @enum); |
| 0 | 98 | | } |
| 0 | 99 | | } |
| 0 | 100 | | UnitStringToEnumMap = unitStringToEnumMap; |
| | 101 | |
|
| 0 | 102 | | ParsingLibraryBuilt = true; |
| 0 | 103 | | } |
| | 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() |
| 0 | 111 | | { |
| | 112 | | // make the delegates for constructing the measurements |
| 0 | 113 | | var unitsStringsToFactoryFunctions = new MapHashLinked<Func<T, object[], object>, string, StringEquate, StringHash |
| 0 | 114 | | foreach (MethodInfo methodInfo in typeof(ParsingFunctions).GetMethods()) |
| 0 | 115 | | { |
| 0 | 116 | | if (methodInfo.DeclaringType == typeof(ParsingFunctions)) |
| 0 | 117 | | { |
| 0 | 118 | | MethodInfo genericMethodInfo = methodInfo.MakeGenericMethod(typeof(T)); |
| 0 | 119 | | ParseableAttribute? parsableAttribute = genericMethodInfo.GetCustomAttribute<ParseableAttribute>(); |
| 0 | 120 | | if (parsableAttribute is null) |
| 0 | 121 | | { |
| 0 | 122 | | throw new TowelBugException($"Encountered null {nameof(ConstructorInfo)} in {nameof(BuildTypeSpecificParsing |
| | 123 | | } |
| 0 | 124 | | Func<T, object[], object> factory = genericMethodInfo.CreateDelegate<Func<T, object[], object>>(); |
| 0 | 125 | | unitsStringsToFactoryFunctions.Add(parsableAttribute.Key, factory); |
| 0 | 126 | | } |
| 0 | 127 | | } |
| 0 | 128 | | UnitsStringsToFactoryFunctions = unitsStringsToFactoryFunctions; |
| | 129 | |
|
| 0 | 130 | | TypeSpecificParsingLibraryBuilt = true; |
| 0 | 131 | | } |
| | 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 |
| 0 | 142 | | { |
| 0 | 143 | | if (!ParsingLibraryBuilt) |
| 0 | 144 | | { |
| 0 | 145 | | BuildParsingLibrary(); |
| 0 | 146 | | } |
| 0 | 147 | | if (!TypeSpecificParsingLibrary<T>.TypeSpecificParsingLibraryBuilt) |
| 0 | 148 | | { |
| 0 | 149 | | TypeSpecificParsingLibrary<T>.BuildTypeSpecificParsingLibrary(); |
| 0 | 150 | | } |
| | 151 | |
|
| 0 | 152 | | ListArray<object> parameters = new(); |
| 0 | 153 | | bool AtLeastOneUnit = false; |
| 0 | 154 | | bool? numerator = null; |
| 0 | 155 | | MatchCollection matchCollection = Regex.Matches(@string, AllUnitsRegexPattern!); |
| 0 | 156 | | if (matchCollection.Count <= 0 || matchCollection[0].Index <= 0) |
| 0 | 157 | | { |
| 0 | 158 | | return (false, default); |
| | 159 | | } |
| 0 | 160 | | string numericString = @string.Substring(0, matchCollection[0].Index); |
| | 161 | | T value; |
| | 162 | | try |
| 0 | 163 | | { |
| 0 | 164 | | value = Symbolics.ParseAndSimplifyToConstant<T>(numericString, tryParse); |
| 0 | 165 | | } |
| 0 | 166 | | catch |
| 0 | 167 | | { |
| 0 | 168 | | return (false, default); |
| | 169 | | } |
| 0 | 170 | | StringBuilder stringBuilder = new(); |
| 0 | 171 | | foreach (Match match in matchCollection) |
| 0 | 172 | | { |
| 0 | 173 | | string matchValue = match.Value; |
| 0 | 174 | | if (matchValue.Equals("*") || matchValue.Equals("/")) |
| 0 | 175 | | { |
| 0 | 176 | | if (numerator is not null) |
| 0 | 177 | | { |
| 0 | 178 | | return (false, default); |
| | 179 | | } |
| 0 | 180 | | if (!AtLeastOneUnit) |
| 0 | 181 | | { |
| 0 | 182 | | return (false, default); |
| | 183 | | } |
| 0 | 184 | | numerator = matchValue.Equals("*"); |
| 0 | 185 | | continue; |
| | 186 | | } |
| 0 | 187 | | var (success, exception, @enum) = UnitStringToEnumMap!.TryGet(match.Value); |
| 0 | 188 | | if (!success) |
| 0 | 189 | | { |
| 0 | 190 | | return (false, default); |
| | 191 | | } |
| 0 | 192 | | if (!AtLeastOneUnit) |
| 0 | 193 | | { |
| 0 | 194 | | if (numerator is not null) |
| 0 | 195 | | { |
| 0 | 196 | | return (false, default); |
| | 197 | | } |
| 0 | 198 | | AtLeastOneUnit = true; |
| 0 | 199 | | stringBuilder.Append(@enum!.GetType().DeclaringType!.Name); |
| 0 | 200 | | parameters.Add(@enum); |
| 0 | 201 | | } |
| | 202 | | else |
| 0 | 203 | | { |
| 0 | 204 | | if (numerator is null) |
| 0 | 205 | | { |
| 0 | 206 | | return (false, default); |
| | 207 | | } |
| 0 | 208 | | if (numerator.Value) |
| 0 | 209 | | { |
| 0 | 210 | | stringBuilder.Append("*" + @enum!.GetType().DeclaringType!.Name); |
| 0 | 211 | | parameters.Add(@enum); |
| 0 | 212 | | } |
| | 213 | | else |
| 0 | 214 | | { |
| 0 | 215 | | stringBuilder.Append("/" + @enum!.GetType().DeclaringType!.Name); |
| 0 | 216 | | parameters.Add(@enum); |
| 0 | 217 | | } |
| 0 | 218 | | } |
| 0 | 219 | | numerator = null; |
| 0 | 220 | | } |
| 0 | 221 | | if (numerator is not null) |
| 0 | 222 | | { |
| 0 | 223 | | return (false, default); |
| | 224 | | } |
| 0 | 225 | | string key = stringBuilder.ToString(); |
| 0 | 226 | | Func<T, object[], object> factory = TypeSpecificParsingLibrary<T>.UnitsStringsToFactoryFunctions![key]; |
| 0 | 227 | | return (true, factory(value, parameters.ToArray())); |
| 0 | 228 | | } |
| | 229 | |
|
| | 230 | | internal static (bool Success, TMeasurement? Value) TryParse<T, TMeasurement>(string @string, Func<string, (bool Succe |
| 0 | 231 | | { |
| 0 | 232 | | var (success, value) = TryParse(@string, tryParse); |
| 0 | 233 | | return success && value is TMeasurement measurement |
| 0 | 234 | | ? (true, measurement) |
| 0 | 235 | | : (false, default); |
| 0 | 236 | | } |
| | 237 | |
|
| | 238 | | #endregion |
| | 239 | | } |