diff --git a/UnitsNet.Tests/Helpers/TypeDescriptorContext.cs b/UnitsNet.Tests/Helpers/TypeDescriptorContext.cs new file mode 100644 index 0000000000..a4675599c3 --- /dev/null +++ b/UnitsNet.Tests/Helpers/TypeDescriptorContext.cs @@ -0,0 +1,80 @@ +// 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; +using System.Collections.Generic; +using System.ComponentModel; +using System.Text; + +namespace UnitsNet.Tests.Helpers +{ + /// + /// Is used to imitate e property with attributes + /// + public class TypeDescriptorContext : ITypeDescriptorContext + { + public class PropertyDescriptor_ : PropertyDescriptor + { + public PropertyDescriptor_(string name, Attribute[] attributes) : base(name, attributes) + { + } + + public override Type ComponentType => throw new NotImplementedException(); + + public override bool IsReadOnly => throw new NotImplementedException(); + + public override Type PropertyType => throw new NotImplementedException(); + + public override bool CanResetValue(object component) + { + throw new NotImplementedException(); + } + + public override object GetValue(object component) + { + throw new NotImplementedException(); + } + + public override void ResetValue(object component) + { + throw new NotImplementedException(); + } + + public override void SetValue(object component, object value) + { + throw new NotImplementedException(); + } + + public override bool ShouldSerializeValue(object component) + { + throw new NotImplementedException(); + } + } + + public TypeDescriptorContext(string name, Attribute[] attributes) + { + PropertyDescriptor = new PropertyDescriptor_(name, attributes); + } + + public IContainer Container => throw new NotImplementedException(); + + public object Instance => throw new NotImplementedException(); + + public PropertyDescriptor PropertyDescriptor { get; set; } + + public object GetService(Type serviceType) + { + throw new NotImplementedException(); + } + + public void OnComponentChanged() + { + throw new NotImplementedException(); + } + + public bool OnComponentChanging() + { + throw new NotImplementedException(); + } + } +} diff --git a/UnitsNet.Tests/QuantityTypeConverterTest.cs b/UnitsNet.Tests/QuantityTypeConverterTest.cs new file mode 100644 index 0000000000..94dbac7d04 --- /dev/null +++ b/UnitsNet.Tests/QuantityTypeConverterTest.cs @@ -0,0 +1,315 @@ +// 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; +using System.ComponentModel; +using System.Globalization; +using System.Reflection; +using UnitsNet.Tests.Helpers; +using Xunit; + +namespace UnitsNet.Tests +{ + public class QuantityTypeConverterTest + { + // https://stackoverflow.com/questions/3612909/why-is-this-typeconverter-not-working + private static Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args) + { + AppDomain domain = (AppDomain)sender; + foreach (Assembly asm in domain.GetAssemblies()) + { + if (asm.FullName == args.Name) + { + return asm; + } + } + return null; + } + + static QuantityTypeConverterTest() + { + // NOTE: After this, you can use your TypeConverter. + AppDomain.CurrentDomain.AssemblyResolve += new ResolveEventHandler(CurrentDomain_AssemblyResolve); + } + + /// + /// Is used for tests that are culture dependent + /// + private static CultureInfo culture = CultureInfo.GetCultureInfo("en-US"); + + [Theory] + [InlineData(typeof(string), true)] + [InlineData(typeof(double), false)] + [InlineData(typeof(object), false)] + [InlineData(typeof(float), false)] + [InlineData(typeof(Length), false)] + public void CanConvertFrom_GivenSomeTypes(Type value, bool expectedResult) + { + var converter = new QuantityTypeConverter(); + + bool canConvertFrom = converter.CanConvertFrom(value); + + Assert.Equal(expectedResult, canConvertFrom); + } + + [Theory] + [InlineData(typeof(string), true)] + [InlineData(typeof(double), false)] + [InlineData(typeof(object), false)] + [InlineData(typeof(float), false)] + [InlineData(typeof(Length), false)] + public void CanConvertTo_GivenSomeTypes(Type value, bool expectedResult) + { + var converter = new QuantityTypeConverter(); + + bool canConvertTo = converter.CanConvertTo(value); + + Assert.Equal(expectedResult, canConvertTo); + } + + [Theory] + [InlineData("1mm", 1, Units.LengthUnit.Millimeter)] + [InlineData("1m", 1, Units.LengthUnit.Meter)] + [InlineData("1", 1, Units.LengthUnit.Meter)] + [InlineData("1km", 1, Units.LengthUnit.Kilometer)] + public void ConvertFrom_GivenQuantityStringAndContextWithNoAttributes_ReturnsQuantityWithBaseUnitIfNotSpecified(string str, double expectedValue, Enum expectedUnit) + { + var converter = new QuantityTypeConverter(); + ITypeDescriptorContext context = new TypeDescriptorContext("SomeMemberName", new Attribute[] { }); + + var convertedValue = (Length)converter.ConvertFrom(context, culture, str); + + Assert.Equal(expectedValue, convertedValue.Value); + Assert.Equal(expectedUnit, convertedValue.Unit); + } + + [Theory] + [InlineData("1mm", 1, Units.LengthUnit.Millimeter)] + [InlineData("1m", 1, Units.LengthUnit.Meter)] + [InlineData("1", 1, Units.LengthUnit.Centimeter)] + [InlineData("1km", 1, Units.LengthUnit.Kilometer)] + public void ConvertFrom_GivenQuantityStringAndContextWithDefaultUnitAttribute_ReturnsQuantityWithGivenDefaultUnitIfNotSpecified(string str, double expectedValue, Enum expectedUnit) + { + var converter = new QuantityTypeConverter(); + ITypeDescriptorContext context = new TypeDescriptorContext("SomeMemberName", new Attribute[] + { + new DefaultUnitAttribute(Units.LengthUnit.Centimeter) + }); + + var convertedValue = (Length)converter.ConvertFrom(context, culture, str); + + Assert.Equal(expectedValue, convertedValue.Value); + Assert.Equal(expectedUnit, convertedValue.Unit); + } + + [Theory] + [InlineData("1mm", 0.001, Units.LengthUnit.Meter)] + [InlineData("1m", 1, Units.LengthUnit.Meter)] + [InlineData("1", 0.01, Units.LengthUnit.Meter)] + [InlineData("1km", 1000, Units.LengthUnit.Meter)] + public void ConvertFrom_GivenQuantityStringAndContextWithDefaultUnitAndConvertToUnitAttributes_ReturnsQuantityConvertedToUnit(string str, double expectedValue, Enum expectedUnit) + { + var converter = new QuantityTypeConverter(); + ITypeDescriptorContext context = new TypeDescriptorContext("SomeMemberName", new Attribute[] + { + new DefaultUnitAttribute(Units.LengthUnit.Centimeter), + new ConvertToUnitAttribute(Units.LengthUnit.Meter) + }); + + var convertedValue = (Length)converter.ConvertFrom(context, culture, str); + + Assert.Equal(expectedValue, convertedValue.Value); + Assert.Equal(expectedUnit, convertedValue.Unit); + } + + [Fact] + public void ConvertFrom_GivenEmptyString_ThrowsNotSupportedException() + { + var converter = new QuantityTypeConverter(); + ITypeDescriptorContext context = new TypeDescriptorContext("SomeMemberName", new Attribute[] { }); + + Assert.Throws(() => converter.ConvertFrom(context, culture, "")); + } + + [Fact] + public void ConvertFrom_GivenWrongQuantity_ThrowsArgumentException() + { + var converter = new QuantityTypeConverter(); + ITypeDescriptorContext context = new TypeDescriptorContext("SomeMemberName", new Attribute[] { }); + + Assert.Throws(() => converter.ConvertFrom(context, culture, "1m^2")); + } + + [Theory] + [InlineData(typeof(Length))] + [InlineData(typeof(IQuantity))] + [InlineData(typeof(object))] + public void ConvertTo_GivenWrongType_ThrowsNotSupportedException(Type value) + { + var converter = new QuantityTypeConverter(); + ITypeDescriptorContext context = new TypeDescriptorContext("SomeMemberName", new Attribute[] { }); + Length length = Length.FromMeters(1); + + Assert.Throws(() => converter.ConvertTo(length, value)); + } + + [Fact] + public void ConvertTo_GivenStringType_ReturnsQuantityString() + { + var converter = new QuantityTypeConverter(); + ITypeDescriptorContext context = new TypeDescriptorContext("SomeMemberName", new Attribute[] { }); + Length length = Length.FromMeters(1); + + string convertedQuantity = (string)converter.ConvertTo(length, typeof(string)); + + Assert.Equal("1 m", convertedQuantity); + } + + [Fact] + public void ConvertTo_GivenSomeQuantitysAndContextWithNoAttributes_ReturnsQuantityStringInUnitOfQuantity() + { + var converter = new QuantityTypeConverter(); + ITypeDescriptorContext context = new TypeDescriptorContext("SomeMemberName", new Attribute[] { }); + Length length = Length.FromMeters(1); + + string convertedQuantity = (string)converter.ConvertTo(context, culture, length, typeof(string)); + + Assert.Equal("1 m", convertedQuantity); + } + + [Fact] + public void ConvertTo_TestDisplayAsFormatting_ReturnsQuantityStringWithDisplayUnitDefaultFormating() + { + var converter = new QuantityTypeConverter(); + ITypeDescriptorContext context = new TypeDescriptorContext("SomeMemberName", new Attribute[] + { + new DisplayAsUnitAttribute(Units.LengthUnit.Decimeter) + }); + Length length = Length.FromMeters(1); + + string convertedQuantity = (string)converter.ConvertTo(context, culture, length, typeof(string)); + + Assert.Equal("10 dm", convertedQuantity); + } + + [Fact] + public void ConvertTo_TestDisplayAsFormatting_ReturnsQuantityStringWithDisplayUnitFormateAsValueOnly() + { + var converter = new QuantityTypeConverter(); + ITypeDescriptorContext context = new TypeDescriptorContext("SomeMemberName", new Attribute[] + { + new DisplayAsUnitAttribute(Units.LengthUnit.Decimeter, "v") + }); + Length length = Length.FromMeters(1); + + string convertedQuantity = (string)converter.ConvertTo(context, culture, length, typeof(string)); + + Assert.Equal("10", convertedQuantity); + } + + [Fact] + public void ConvertTo_TestDisplayAsFormattingWithoutDefinedUnit_ReturnsQuantityStringWithQuantetiesUnitAndFormatedAsValueOnly() + { + var converter = new QuantityTypeConverter(); + ITypeDescriptorContext context = new TypeDescriptorContext("SomeMemberName", new Attribute[] + { + new DisplayAsUnitAttribute(null, "v") + }); + Length length = Length.FromMeters(1); + + string convertedQuantity = (string)converter.ConvertTo(context, culture, length, typeof(string)); + + Assert.Equal("1", convertedQuantity); + } + + [Fact] + public void ConvertTo_GivenSomeQuantitysAndContextWithDisplayAsUnitAttributes_ReturnsQuantityStringInSpecifiedDisplayUnit() + { + var converter = new QuantityTypeConverter(); + ITypeDescriptorContext context = new TypeDescriptorContext("SomeMemberName", new Attribute[] + { + new DisplayAsUnitAttribute(Units.LengthUnit.Decimeter) + }); + Length length = Length.FromMeters(1); + + string convertedQuantityDefaultCulture = (string)converter.ConvertTo(length, typeof(string)); + string convertedQuantitySpecificCulture = (string)converter.ConvertTo(context, culture, length, typeof(string)); + + Assert.Equal("1 m", convertedQuantityDefaultCulture); + Assert.Equal("10 dm", convertedQuantitySpecificCulture); + } + + [Fact] + public void ConvertFrom_GivenDefaultUnitAttributeWithWrongUnitType_ThrowsArgumentException() + { + var converter = new QuantityTypeConverter(); + ITypeDescriptorContext context = new TypeDescriptorContext("SomeMemberName", new Attribute[] + { + new DefaultUnitAttribute(Units.VolumeUnit.CubicMeter) + }); + + Assert.Throws(() => converter.ConvertFrom(context, culture, "1")); + } + + [Fact] + public void ConvertFrom_GivenStringWithPower_1() + { + var converter = new QuantityTypeConverter(); + ITypeDescriptorContext context = new TypeDescriptorContext("SomeMemberName", new Attribute[] { }); + + Assert.Equal(Length.FromMeters(1), converter.ConvertFrom(context, culture, "1m")); + Assert.Equal(Length.FromMeters(1), converter.ConvertFrom(context, culture, "1m^1")); + } + + [Fact] + public void ConvertFrom_GivenStringWithPower_2() + { + var converter = new QuantityTypeConverter(); + ITypeDescriptorContext context = new TypeDescriptorContext("SomeMemberName", new Attribute[] { }); + + Assert.Equal(Area.FromSquareMeters(1), converter.ConvertFrom(context, culture, "1m²")); + Assert.Equal(Area.FromSquareMeters(1), converter.ConvertFrom(context, culture, "1m^2")); + } + + [Fact] + public void ConvertFrom_GivenStringWithPower_3() + { + var converter = new QuantityTypeConverter(); + ITypeDescriptorContext context = new TypeDescriptorContext("SomeMemberName", new Attribute[] { }); + + Assert.Equal(Volume.FromCubicMeters(1), converter.ConvertFrom(context, culture, "1m³")); + Assert.Equal(Volume.FromCubicMeters(1), converter.ConvertFrom(context, culture, "1m^3")); + } + + [Fact] + public void ConvertFrom_GivenStringWithPower_4() + { + var converter = new QuantityTypeConverter(); + ITypeDescriptorContext context = new TypeDescriptorContext("SomeMemberName", new Attribute[] { }); + + Assert.Equal(AreaMomentOfInertia.FromMetersToTheFourth(1), converter.ConvertFrom(context, culture, "1m⁴")); + Assert.Equal(AreaMomentOfInertia.FromMetersToTheFourth(1), converter.ConvertFrom(context, culture, "1m^4")); + } + + [Fact] + public void ConvertFrom_GivenStringWithPower_minus1() + { + var converter = new QuantityTypeConverter(); + ITypeDescriptorContext context = new TypeDescriptorContext("SomeMemberName", new Attribute[] { }); + + Assert.Equal(CoefficientOfThermalExpansion.FromInverseKelvin(1), converter.ConvertFrom(context, culture, "1K⁻¹")); + Assert.Equal(CoefficientOfThermalExpansion.FromInverseKelvin(1), converter.ConvertFrom(context, culture, "1K^-1")); + } + + [Fact] + public void ConvertFrom_GivenStringWithPower_minus2() + { + var converter = new QuantityTypeConverter(); + ITypeDescriptorContext context = new TypeDescriptorContext("SomeMemberName", new Attribute[] { }); + + Assert.Equal(MassFlux.FromKilogramsPerSecondPerSquareMeter(1), converter.ConvertFrom(context, culture, "1kg·s⁻¹·m⁻²")); + Assert.Equal(MassFlux.FromKilogramsPerSecondPerSquareMeter(1), converter.ConvertFrom(context, culture, "1kg·s^-1·m^-2")); + Assert.Equal(MassFlux.FromKilogramsPerSecondPerSquareMeter(1), converter.ConvertFrom(context, culture, "1kg*s^-1*m^-2")); + } + } +} diff --git a/UnitsNet/QuantityTypeConverter.cs b/UnitsNet/QuantityTypeConverter.cs new file mode 100644 index 0000000000..0b0ab77a77 --- /dev/null +++ b/UnitsNet/QuantityTypeConverter.cs @@ -0,0 +1,263 @@ +// 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; +using System.ComponentModel; +using System.Globalization; + +namespace UnitsNet +{ + /// + /// Is the base class for all attributes that are related to + /// + [AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)] + public abstract class UnitAttributeBase : Attribute + { + /// + /// The unit enum type, such as + /// + public Enum UnitType { get; set; } + + /// + /// Initializes a new instance of the class. + /// + /// + public UnitAttributeBase(object unitType) + { + UnitType = unitType as Enum; + } + } + + /// + /// This attribute defines the default Unit to use if the string to convert only consists of digits + /// + public class DefaultUnitAttribute : UnitAttributeBase + { + /// + /// Initializes a new instance of the class. + /// + /// The unit the quantity gets when the string parsing dose only consist of digits + public DefaultUnitAttribute(object unitType) : base(unitType) { } + } + + /// + /// This attribute defines the Unit the quantity is converted to after it has been parsed. + /// + public class ConvertToUnitAttribute : DefaultUnitAttribute + { + /// + /// Initializes a new instance of the class. + /// + /// The unit the quantity is converted to when parsing from string + public ConvertToUnitAttribute(object unitType) : base(unitType) { } + } + + /// + /// This attribute defines the unit the quantity has when converting to string + /// + public class DisplayAsUnitAttribute : DefaultUnitAttribute + { + /// + /// The formating used when the quantity is converted to string. See + /// + public string Format { get; set; } + + /// + /// Initializes a new instance of the class. + /// + /// The unit the quantity should be displayed in + /// Formating string + public DisplayAsUnitAttribute(object unitType, string format = "") : base(unitType) + { + Format = format; + } + } + + /// + /// + /// Converts between IQuantity and string. + /// Implements a TypeConverter for IQuantitys. This allows eg the PropertyGrid to read and write properties of type IQuantity. + /// + /// For basic understanding of TypeConverters consult the .NET documentation. + /// + /// Quantity value type, such as or . + /// + /// + /// When a string is converted a Quantity the unit given by the string is used. + /// When no unit is given by the string the base unit is used. + /// The base unit can be overwritten by use of the . + /// The converted Quantity can be forced to be in a certain unit by use of the . + /// + /// + /// The displayed unit can be forced to a certain unit by use of the . + /// The provides the possibility to format the displayed Quantity. + /// + /// + /// + /// These examples show how to use this TypeConverter. + /// + /// + /// [TypeConverter(typeof(UnitsNetTypeConverter{Length}))] + /// Units.Length PropertyName { get; set; } + /// + /// + /// + /// [DisplayAsUnit(UnitsNet.Units.LengthUnit.Meter)] + /// [TypeConverter(typeof(UnitsNetTypeConverter{Length}))] + /// Units.Length Length { get; set; } + /// + /// + /// + /// [DisplayAsUnit(UnitsNet.Units.LengthUnit.Meter, "g")] + /// [TypeConverter(typeof(UnitsNetTypeConverter{Length}))] + /// Units.Length Length { get; set; } + /// + /// + /// + /// [ConvertToUnitAttribute(UnitsNet.Units.LengthUnit.Meter)] + /// [TypeConverter(typeof(UnitsNetTypeConverter{Length}))] + /// Units.Length Length { get; set; } + /// + /// + /// + /// [DefaultUnitAttribute(UnitsNet.Units.LengthUnit.Meter)] + /// [TypeConverter(typeof(UnitsNetTypeConverter{Length}))] + /// Units.Length Length { get; set; } + /// + /// + public class QuantityTypeConverter : TypeConverter where TQuantity : IQuantity + { + /// + /// Returns true if sourceType if of type + /// + /// An that provides a format context. + /// A that represents the type you want to convert from. + /// true if this converter can perform the conversion; otherwise, false. + public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) + { + return (sourceType == typeof(string)) || base.CanConvertFrom(context, sourceType); + } + + private static TAttribute GetAttribute(ITypeDescriptorContext context) where TAttribute : UnitAttributeBase + { + TAttribute attribute = null; + AttributeCollection ua = context?.PropertyDescriptor.Attributes; + + attribute = (TAttribute)ua?[typeof(TAttribute)]; + + if (attribute != null) + { + QuantityType expected = default(TQuantity).Type; + QuantityType actual = QuantityType.Undefined; + + if (attribute.UnitType != null) actual = Quantity.From(1, attribute.UnitType).Type; + if (actual != QuantityType.Undefined && expected != actual) + { + throw new ArgumentException($"The specified UnitType:'{attribute.UnitType}' dose not match QuantityType:'{expected}'"); + } + } + + return attribute; + } + + /// + /// Converts the given object, when it is of type to the type of this converter, using the specified context and culture information. + /// + /// An System.ComponentModel.ITypeDescriptorContext that provides a format context. + /// The System.Globalization.CultureInfo to use as the current culture. + /// The System.Object to convert. + /// An object. + /// The conversion cannot be performed. + /// Unit value is not a know unit enum type. + public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) + { + string stringValue = value as string; + object result = null; + + if (!string.IsNullOrEmpty(stringValue)) + { + if (double.TryParse(stringValue, NumberStyles.Any, culture, out double dvalue)) + { + DefaultUnitAttribute defaultUnit = GetAttribute(context) ?? new DefaultUnitAttribute(default(TQuantity).Unit); + + result = Quantity.From(dvalue, defaultUnit.UnitType); + } + else + { + // TODO this should not be part of QuantityTypeConverter. it should rather be part of the parse function + stringValue = stringValue.Replace("^-9", "⁻⁹"); + stringValue = stringValue.Replace("^-8", "⁻⁸"); + stringValue = stringValue.Replace("^-7", "⁻⁷"); + stringValue = stringValue.Replace("^-6", "⁻⁶"); + stringValue = stringValue.Replace("^-5", "⁻⁵"); + stringValue = stringValue.Replace("^-4", "⁻⁴"); + stringValue = stringValue.Replace("^-3", "⁻³"); + stringValue = stringValue.Replace("^-2", "⁻²"); + stringValue = stringValue.Replace("^-1", "⁻¹"); + stringValue = stringValue.Replace("^1", ""); + stringValue = stringValue.Replace("^2", "²"); + stringValue = stringValue.Replace("^3", "³"); + stringValue = stringValue.Replace("^4", "⁴"); + stringValue = stringValue.Replace("^5", "⁵"); + stringValue = stringValue.Replace("^6", "⁶"); + stringValue = stringValue.Replace("^7", "⁷"); + stringValue = stringValue.Replace("^8", "⁸"); + stringValue = stringValue.Replace("^9", "⁹"); + stringValue = stringValue.Replace("*", "·"); + + result = Quantity.Parse(culture, typeof(TQuantity), stringValue); + } + + ConvertToUnitAttribute convertToUnit = GetAttribute(context); + if (convertToUnit != null) + { + result = ((IQuantity)result).ToUnit(convertToUnit.UnitType); + } + } + + return result ?? base.ConvertFrom(context, culture, value); + } + + /// Returns true whether this converter can convert the to string, using the specified context. + /// true if this converter can perform the conversion; otherwise, false. + /// An that provides a format context. + /// A that represents the type you want to convert to. + public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType) + { + return (destinationType == typeof(string)) || base.CanConvertTo(context, destinationType); + } + + /// Converts the given object to , using the specified context and culture information. + /// An that represents the converted value. + /// An that provides a format context. + /// A . If null is passed, the current culture is assumed. + /// The to convert. + /// The to convert the parameter to. + /// The parameter is null. + /// The conversion cannot be performed. + public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType) + { + IQuantity qvalue = value as IQuantity; + object result = null; + DisplayAsUnitAttribute displayAsUnit = GetAttribute(context); + + if (destinationType == typeof(string) && qvalue != null && displayAsUnit != null) + { + if (displayAsUnit.UnitType != null) + { + result = qvalue.ToUnit(displayAsUnit.UnitType).ToString(displayAsUnit.Format, culture); + } + else + { + result = qvalue.ToString(displayAsUnit.Format, culture); + } + } + else + { + result = base.ConvertTo(context, culture, value, destinationType); + } + + return result; + } + } +}