diff --git a/UnitsNet.Serialization.JsonNet/UnitsNetJsonConverter.cs b/UnitsNet.Serialization.JsonNet/UnitsNetJsonConverter.cs index 081a702784..50de04d529 100644 --- a/UnitsNet.Serialization.JsonNet/UnitsNetJsonConverter.cs +++ b/UnitsNet.Serialization.JsonNet/UnitsNetJsonConverter.cs @@ -2,13 +2,9 @@ // Copyright 2013 Andreas Gullberg Larsen (andreas.larsen84@gmail.com). Maintained at https://github.com/angularsen/UnitsNet. using System; -using System.Linq; -using System.Reflection; using JetBrains.Annotations; using Newtonsoft.Json; using Newtonsoft.Json.Linq; -using UnitsNet.Serialization.JsonNet.Internal; -using UnitsNet.Units; namespace UnitsNet.Serialization.JsonNet { @@ -26,11 +22,6 @@ namespace UnitsNet.Serialization.JsonNet /// public class UnitsNetJsonConverter : JsonConverter { - /// - /// Numeric value field of a quantity, typically of type double or decimal. - /// - private const string ValueFieldName = "_value"; - /// /// Reads the JSON representation of the object. /// @@ -46,29 +37,20 @@ public override object ReadJson(JsonReader reader, Type objectType, object exist JsonSerializer serializer) { if (reader.ValueType != null) - { return reader.Value; - } + object obj = TryDeserializeIComparable(reader, serializer); // A null System.Nullable value or a comparable type was deserialized so return this if (!(obj is ValueUnit vu)) - { return obj; - } // "MassUnit.Kilogram" => "MassUnit" and "Kilogram" string unitEnumTypeName = vu.Unit.Split('.')[0]; string unitEnumValue = vu.Unit.Split('.')[1]; - // "MassUnit" => "Mass" - string quantityTypeName = unitEnumTypeName.Substring(0, unitEnumTypeName.Length - "Unit".Length); - // "UnitsNet.Units.MassUnit,UnitsNet" string unitEnumTypeAssemblyQualifiedName = "UnitsNet.Units." + unitEnumTypeName + ",UnitsNet"; - // "UnitsNet.Mass,UnitsNet" - string quantityTypeAssemblyQualifiedName = "UnitsNet." + quantityTypeName + ",UnitsNet"; - // -- see http://stackoverflow.com/a/6465096/1256096 for details Type unitEnumType = Type.GetType(unitEnumTypeAssemblyQualifiedName); if (unitEnumType == null) @@ -78,63 +60,10 @@ public override object ReadJson(JsonReader reader, Type objectType, object exist throw ex; } - Type quantityType = Type.GetType(quantityTypeAssemblyQualifiedName); - if (quantityType == null) - { - var ex = new UnitsNetException("Unable to find unit type."); - ex.Data["type"] = quantityTypeAssemblyQualifiedName; - throw ex; - } - double value = vu.Value; - object unitValue = Enum.Parse(unitEnumType, unitEnumValue); // Ex: MassUnit.Kilogram - - return CreateQuantity(quantityType, value, unitValue); - } - - /// - /// Creates a quantity (ex: Mass) based on the reflected quantity type, a numeric value and a unit value (ex: MassUnit.Kilogram). - /// - /// Type of quantity, such as . - /// Numeric value. - /// The unit, such as . - /// The constructed quantity, such as . - private static object CreateQuantity(Type quantityType, double value, object unitValue) - { - // We want the non-nullable return type, example candidates if quantity type is Mass: - // double Mass.From(double, MassUnit) - // double? Mass.From(double?, MassUnit) - MethodInfo notNullableFromMethod = quantityType - .GetDeclaredMethods() - .Single(m => m.Name == "From" && Nullable.GetUnderlyingType(m.ReturnType) == null); - - // Of type QuantityValue - object quantityValue = GetFromMethodValueArgument(notNullableFromMethod, value); - - // Ex: Mass.From(55, MassUnit.Gram) - // See ValueUnit about precision loss for quantities using decimal type. - return notNullableFromMethod.Invoke(null, new[] {quantityValue, unitValue}); - } - - /// - /// Returns numeric value wrapped as , based on the type of argument - /// of . Today this is always , but - /// we may extend to other types later such as QuantityValueDecimal. - /// - /// The reflected From(value, unit) method. - /// The value to convert to the correct wrapper type. - /// - private static object GetFromMethodValueArgument(MethodInfo fromMethod, double value) - { - Type valueParameterType = fromMethod.GetParameters()[0].ParameterType; - if (valueParameterType == typeof(QuantityValue)) - { - // We use this type that takes implicit cast from all number types to avoid explosion of method overloads that take a numeric value. - return (QuantityValue) value; - } + Enum unitValue = (Enum)Enum.Parse(unitEnumType, unitEnumValue); // Ex: MassUnit.Kilogram - throw new Exception( - $"The first parameter of the reflected quantity From() method was expected to be either UnitsNet.QuantityValue, but was instead {valueParameterType}."); + return Quantity.From(value, unitValue); } private static object TryDeserializeIComparable(JsonReader reader, JsonSerializer serializer) @@ -182,74 +111,14 @@ public override void WriteJson(JsonWriter writer, object obj, JsonSerializer ser return; } - object quantityValue = GetValueOfQuantity(obj, quantityType); // double or decimal value - string quantityUnitName = GetUnitFullNameOfQuantity(obj, quantityType); // Example: "MassUnit.Kilogram" + IQuantity quantity = obj as IQuantity; serializer.Serialize(writer, new ValueUnit { // See ValueUnit about precision loss for quantities using decimal type. - Value = Convert.ToDouble(quantityValue), - Unit = quantityUnitName - }); - } - - /// - /// Given quantity (ex: ), returns the full name (ex: "MassUnit.Kilogram") of the constructed unit given by the property. - /// - /// Quantity, such as . - /// The type of , passed in here to reuse a previous lookup. - /// "MassUnit.Kilogram" for a mass quantity whose Unit property is MassUnit.Kilogram. - private static string GetUnitFullNameOfQuantity(object obj, Type quantityType) - { - // Get value of Unit property - PropertyInfo unitProperty = quantityType.GetProperty("Unit"); - Enum quantityUnit = (Enum) unitProperty.GetValue(obj, null); // MassUnit.Kilogram - - Type unitType = quantityUnit.GetType(); // MassUnit - return $"{unitType.Name}.{quantityUnit}"; // "MassUnit.Kilogram" - } - - private static object GetValueOfQuantity(object value, Type quantityType) - { - FieldInfo valueField = GetPrivateInstanceField(quantityType, ValueFieldName); - - // See ValueUnit about precision loss for quantities using decimal type. - object quantityValue = valueField.GetValue(value); - return quantityValue; - } - - private static FieldInfo GetPrivateInstanceField(Type quantityType, string fieldName) - { - FieldInfo baseValueField; - try - { - baseValueField = quantityType -#if (NETSTANDARD1_0) - .GetTypeInfo() - .DeclaredFields - .Where(f => !f.IsPublic && !f.IsStatic) -#else - .GetFields(BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly) -#endif - .SingleOrDefault(f => f.Name == fieldName); - } - catch (InvalidOperationException) - { - var ex = new UnitsNetException($"Expected exactly one private field named [{fieldName}], but found multiple."); - ex.Data["type"] = quantityType; - ex.Data["fieldName"] = fieldName; - throw ex; - } - - if (baseValueField == null) - { - var ex = new UnitsNetException("No private fields found in type."); - ex.Data["type"] = quantityType; - ex.Data["fieldName"] = fieldName; - throw ex; - } - - return baseValueField; + Value = quantity.Value, + Unit = $"{quantity.QuantityInfo.UnitType.Name}.{quantity.Unit}" // Example: "MassUnit.Kilogram" + } ); } /// @@ -280,9 +149,7 @@ private class ValueUnit public override bool CanConvert(Type objectType) { if (IsNullable(objectType)) - { return CanConvertNullable(objectType); - } return objectType.Namespace != null && (objectType.Namespace.Equals(nameof(UnitsNet)) || @@ -314,6 +181,5 @@ protected virtual bool CanConvertNullable(Type objectType) } #endregion - } } diff --git a/UnitsNet.Tests/UnitConverterTest.cs b/UnitsNet.Tests/UnitConverterTest.cs index db197fd945..4668ad351b 100644 --- a/UnitsNet.Tests/UnitConverterTest.cs +++ b/UnitsNet.Tests/UnitConverterTest.cs @@ -79,9 +79,9 @@ public void ConvertByAbbreviation_ConvertsTheValueToGivenUnit(double expectedVal [Theory] [InlineData(1, "UnknownQuantity", "m", "cm")] - public void ConvertByAbbreviation_ThrowsQuantityNotFoundExceptionOnUnknownQuantity(double inputValue, string quantityTypeName, string fromUnit, string toUnit) + public void ConvertByAbbreviation_ThrowsUnitNotFoundExceptionOnUnknownQuantity( double inputValue, string quantityTypeName, string fromUnit, string toUnit) { - Assert.Throws(() => UnitConverter.ConvertByAbbreviation(inputValue, quantityTypeName, fromUnit, toUnit)); + Assert.Throws(() => UnitConverter.ConvertByAbbreviation(inputValue, quantityTypeName, fromUnit, toUnit)); } [Theory] diff --git a/UnitsNet/QuantityNotFoundException.cs b/UnitsNet/QuantityNotFoundException.cs index bda26cd584..78ce500b9e 100644 --- a/UnitsNet/QuantityNotFoundException.cs +++ b/UnitsNet/QuantityNotFoundException.cs @@ -1,4 +1,4 @@ -// Licensed under MIT No Attribution, see LICENSE file at the root. +// Licensed under MIT No Attribution, see LICENSE file at the root. // Copyright 2013 Andreas Gullberg Larsen (andreas.larsen84@gmail.com). Maintained at https://github.com/angularsen/UnitsNet. using System; @@ -9,6 +9,7 @@ namespace UnitsNet /// Quantity type was not found. This is typically thrown for dynamic conversions, /// such as . /// + [Obsolete("")] public class QuantityNotFoundException : UnitsNetException { /// diff --git a/UnitsNet/UnitConverter.cs b/UnitsNet/UnitConverter.cs index e6d21c34b2..74f83c59ff 100644 --- a/UnitsNet/UnitConverter.cs +++ b/UnitsNet/UnitConverter.cs @@ -4,9 +4,7 @@ using System; using System.Globalization; using System.Linq; -using System.Reflection; using JetBrains.Annotations; -using UnitsNet.InternalHelpers; using UnitsNet.Units; namespace UnitsNet @@ -17,18 +15,6 @@ namespace UnitsNet [PublicAPI] public static class UnitConverter { - private static readonly string UnitTypeNamespace = typeof(LengthUnit).Namespace; - private static readonly Assembly UnitsNetAssembly = typeof(Length).Wrap().Assembly; - - private static readonly Type[] QuantityTypes = UnitsNetAssembly.GetTypes() - .Where(typeof(IQuantity).Wrap().IsAssignableFrom) - .Where(x => x.Wrap().IsClass || x.Wrap().IsValueType) // Future-proofing: we are discussing changing quantities from struct to class - .ToArray(); - - private static readonly Type[] UnitTypes = UnitsNetAssembly.GetTypes() - .Where(x => x.Namespace == UnitTypeNamespace && x.Wrap().IsEnum && x.Name.EndsWith("Unit")) - .ToArray(); - /// /// Convert between any two quantity units given a numeric value and two unit enum values. /// @@ -54,12 +40,12 @@ public static double Convert(QuantityValue fromValue, Enum fromUnitValue, Enum t public static bool TryConvert(QuantityValue fromValue, Enum fromUnitValue, Enum toUnitValue, out double convertedValue) { convertedValue = 0; - if (!Quantity.TryFrom(fromValue, fromUnitValue, out IQuantity from)) return false; + if (!Quantity.TryFrom(fromValue, fromUnitValue, out IQuantity fromQuantity)) return false; try { // We're not going to implement TryAs() in all quantities, so let's just try-catch here - convertedValue = from.As(toUnitValue); + convertedValue = fromQuantity.As(toUnitValue); return true; } catch @@ -94,7 +80,6 @@ public static bool TryConvert(QuantityValue fromValue, Enum fromUnitValue, Enum /// /// double centimeters = ConvertByName(5, "Length", "Meter", "Centimeter"); // 500 /// Output value as the result of converting to . - /// No quantities were found that match . /// No units match the abbreviation. /// More than one unit matches the abbreviation. public static double ConvertByName(QuantityValue fromValue, string quantityName, string fromUnit, string toUnit) @@ -150,13 +135,13 @@ public static bool TryConvertByName(QuantityValue inputValue, string quantityNam { result = 0d; - if (!TryGetUnitType(quantityName, out var unitType)) + if (!TryGetUnitType(quantityName, out Type unitType)) return false; - if (!TryParseUnit(unitType, fromUnit, out var fromUnitValue)) // ex: LengthUnit.Meter + if (!TryParseUnit(unitType, fromUnit, out Enum fromUnitValue)) // ex: LengthUnit.Meter return false; - if (!TryParseUnit(unitType, toUnit, out var toUnitValue)) // ex: LengthUnit.Centimeter + if (!TryParseUnit(unitType, toUnit, out Enum toUnitValue)) // ex: LengthUnit.Centimeter return false; result = Convert(inputValue, fromUnitValue, toUnitValue); @@ -221,7 +206,6 @@ public static double ConvertByAbbreviation(QuantityValue fromValue, string quant /// Culture to parse abbreviations with. /// double centimeters = ConvertByName(5, "Length", "m", "cm"); // 500 /// Output value as the result of converting to . - /// No quantity types match the . /// /// No unit types match the prefix of or no units /// are mapped to the abbreviation. @@ -229,24 +213,16 @@ public static double ConvertByAbbreviation(QuantityValue fromValue, string quant /// More than one unit matches the abbreviation. public static double ConvertByAbbreviation(QuantityValue fromValue, string quantityName, string fromUnitAbbrev, string toUnitAbbrev, string culture) { - if (!TryGetQuantityType(quantityName, out var quantityType)) - throw new QuantityNotFoundException($"The given quantity name was not found: {quantityName}"); - - if (!TryGetUnitType(quantityName, out var unitType)) + if (!TryGetUnitType(quantityName, out Type unitType)) throw new UnitNotFoundException($"The unit type for the given quantity was not found: {quantityName}"); var cultureInfo = string.IsNullOrWhiteSpace(culture) ? GlobalConfiguration.DefaultCulture : new CultureInfo(culture); - var fromUnitValue = UnitParser.Default.Parse(fromUnitAbbrev, unitType, cultureInfo); // ex: ("m", LengthUnit) => LengthUnit.Meter - var toUnitValue = UnitParser.Default.Parse(toUnitAbbrev, unitType, cultureInfo); // ex:("cm", LengthUnit) => LengthUnit.Centimeter - - var fromMethod = GetStaticFromMethod(quantityType, unitType); // ex: UnitsNet.Length.From(double inputValue, LengthUnit inputUnit) - var fromResult = fromMethod.Invoke(null, new object[] {fromValue, fromUnitValue}); // ex: Length quantity = UnitsNet.Length.From(5, LengthUnit.Meter) + var fromUnit = UnitParser.Default.Parse(fromUnitAbbrev, unitType, cultureInfo); // ex: ("m", LengthUnit) => LengthUnit.Meter + var fromQuantity = Quantity.From(fromValue, fromUnit); - var asMethod = GetAsMethod(quantityType, unitType); // ex: quantity.As(LengthUnit outputUnit) - var asResult = asMethod.Invoke(fromResult, new object[] {toUnitValue}); // ex: double outputValue = quantity.As(LengthUnit.Centimeter) - - return (double) asResult; + var toUnit = UnitParser.Default.Parse(toUnitAbbrev, unitType, cultureInfo); // ex:("cm", LengthUnit) => LengthUnit.Centimeter + return fromQuantity.As(toUnit); } /// @@ -314,72 +290,23 @@ public static bool TryConvertByAbbreviation(QuantityValue fromValue, string quan { result = 0d; - if (!TryGetQuantityType(quantityName, out var quantityType)) - return false; - - if (!TryGetUnitType(quantityName, out var unitType)) + if (!TryGetUnitType(quantityName, out Type unitType)) return false; var cultureInfo = string.IsNullOrWhiteSpace(culture) ? GlobalConfiguration.DefaultCulture : new CultureInfo(culture); - if (!UnitParser.Default.TryParse(fromUnitAbbrev, unitType, cultureInfo, out var fromUnitValue)) // ex: ("m", LengthUnit) => LengthUnit.Meter + if (!UnitParser.Default.TryParse(fromUnitAbbrev, unitType, cultureInfo, out Enum fromUnit)) // ex: ("m", LengthUnit) => LengthUnit.Meter return false; - if (!UnitParser.Default.TryParse(toUnitAbbrev, unitType, cultureInfo, out var toUnitValue)) // ex:("cm", LengthUnit) => LengthUnit.Centimeter + if (!UnitParser.Default.TryParse(toUnitAbbrev, unitType, cultureInfo, out Enum toUnit)) // ex:("cm", LengthUnit) => LengthUnit.Centimeter return false; - var fromMethod = GetStaticFromMethod(quantityType, unitType); // ex: UnitsNet.Length.From(double inputValue, LengthUnit inputUnit) - var fromResult = fromMethod.Invoke(null, new object[] {fromValue, fromUnitValue}); // ex: Length quantity = UnitsNet.Length.From(5, LengthUnit.Meter) - - var asMethod = GetAsMethod(quantityType, unitType); // ex: quantity.As(LengthUnit outputUnit) - var asResult = asMethod.Invoke(fromResult, new object[] {toUnitValue}); // ex: double outputValue = quantity.As(LengthUnit.Centimeter) - - result = (double) asResult; - return true; - } - - private static MethodInfo GetAsMethod(Type quantityType, Type unitType) - { - // Only a single As() method as of this writing, but let's safe-guard a bit for future-proofing - // ex: double result = quantity.As(LengthUnit outputUnit); - return quantityType.Wrap().GetDeclaredMethods() - .Single(m => m.Name == "As" && - !m.IsStatic && - m.IsPublic && - HasParameterTypes(m, unitType) && - m.ReturnType == typeof(double)); - } - - private static MethodInfo GetStaticFromMethod(Type quantityType, Type unitType) - { - // Want to match: Length l = UnitsNet.Length.From(double inputValue, LengthUnit inputUnit) - // Do NOT match : Length? UnitsNet.Length.From(double? inputValue, LengthUnit inputUnit) - return quantityType.Wrap().GetDeclaredMethods() - .Single(m => m.Name == "From" && - m.IsStatic && - m.IsPublic && - HasParameterTypes(m, typeof(QuantityValue), unitType) && - m.ReturnType == quantityType); - } - - private static bool HasParameterTypes(MethodInfo methodInfo, params Type[] expectedTypes) - { - var parameters = methodInfo.GetParameters(); - if (parameters.Length != expectedTypes.Length) - throw new ArgumentException($"The number of parameters {parameters.Length} did not match the number of types {expectedTypes.Length}."); - - for (var i = 0; i < parameters.Length; i++) - { - var p = parameters[i]; - var t = expectedTypes[i]; - if (p.ParameterType != t) - return false; - } + var fromQuantity = Quantity.From(fromValue, fromUnit); + result = fromQuantity.As(toUnit); return true; } - /// /// Parse a unit by the unit enum type and a unit enum value > /// @@ -402,19 +329,10 @@ private static bool TryParseUnit(Type unitType, string unitName, out Enum unitVa private static bool TryGetUnitType(string quantityName, out Type unitType) { - var unitTypeName = quantityName + "Unit"; // ex. LengthUnit - - unitType = UnitTypes.FirstOrDefault(x => - x.Name.Equals(unitTypeName, StringComparison.OrdinalIgnoreCase)); - - return unitType != null; - } - - private static bool TryGetQuantityType(string quantityName, out Type quantityType) - { - quantityType = QuantityTypes.FirstOrDefault(x => x.Name.Equals(quantityName, StringComparison.OrdinalIgnoreCase)); + var quantityInfo = Quantity.Infos.FirstOrDefault((info) => info.Name.Equals(quantityName, StringComparison.OrdinalIgnoreCase)); - return quantityType != null; + unitType = quantityInfo?.UnitType; + return quantityInfo != null; } } }