Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

POC: Turn Currency into a class #84

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions NodaMoney.sln
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NodaMoney.Tests", "tests\No
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "_build", "build\_build.csproj", "{324D342A-2588-4422-AFB1-FA0359EE825D}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NodaMoney.Serialization.JsonNet", "src\NodaMoney.Serialization.JsonNet\NodaMoney.Serialization.JsonNet.csproj", "{EF8D8552-7B23-4D03-B2A9-03246087BE6B}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand All @@ -41,6 +43,10 @@ Global
{CC01DEB3-F862-49A1-9EB5-50A4E52B82E7}.Release|Any CPU.Build.0 = Release|Any CPU
{324D342A-2588-4422-AFB1-FA0359EE825D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{324D342A-2588-4422-AFB1-FA0359EE825D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{EF8D8552-7B23-4D03-B2A9-03246087BE6B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{EF8D8552-7B23-4D03-B2A9-03246087BE6B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{EF8D8552-7B23-4D03-B2A9-03246087BE6B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{EF8D8552-7B23-4D03-B2A9-03246087BE6B}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,14 @@
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<PackageId>NodaMoney.Serialization.AspNet</PackageId>
<PackageTags>Noda;Money;Currency;ExchangeRate;Serialization</PackageTags>
<TargetFrameworks>net40;net45</TargetFrameworks>
<TargetFrameworks>net45</TargetFrameworks>
<LangVersion>latest</LangVersion>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\NodaMoney\NodaMoney.csproj" />
</ItemGroup>

<ItemGroup Condition=" '$(TargetFramework)' == 'net40' ">
<Reference Include="System.Web.Extensions" />
<Reference Include="System" />
<Reference Include="Microsoft.CSharp" />
</ItemGroup>

<ItemGroup Condition=" '$(TargetFramework)' == 'net45' ">
<Reference Include="System.Web.Extensions" />
<Reference Include="System" />
Expand Down
23 changes: 23 additions & 0 deletions src/NodaMoney.Serialization.JsonNet/CurrencyJsonConverter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using System;
using System.ComponentModel;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;

namespace NodaMoney.Serialization.JsonNet
{

public class CurrencyJsonConverter : JsonConverter<Currency>
{
private static TypeConverter currencyConverter = TypeDescriptor.GetConverter(typeof(Currency));
public override Currency ReadJson(JsonReader reader, Type objectType, Currency existingValue, bool hasExistingValue, JsonSerializer serializer)
{
var code = reader.Value.ToString();
return (Currency)currencyConverter.ConvertFromString(code);
}

public override void WriteJson(JsonWriter writer, Currency value, JsonSerializer serializer)
{
writer.WriteValue(currencyConverter.ConvertToString(value));
}
}
}
19 changes: 19 additions & 0 deletions src/NodaMoney.Serialization.JsonNet/Extensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using System;
using Newtonsoft.Json;

namespace NodaMoney.Serialization.JsonNet
{
public static class Extensions
{
public static JsonSerializerSettings ConfigureForNodaMoney(this JsonSerializerSettings settings)
{
if (settings == null)
{
throw new ArgumentNullException(nameof(settings));
}
settings.Converters.Add(new MoneyJsonConverter());
settings.Converters.Add(new CurrencyJsonConverter());
return settings;
}
}
}
72 changes: 72 additions & 0 deletions src/NodaMoney.Serialization.JsonNet/MoneyJsonConverter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
using System;
using System.ComponentModel;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;

namespace NodaMoney.Serialization.JsonNet
{
public class MoneyJsonConverter : JsonConverter<Money>
{
private static TypeConverter currencyConverter = TypeDescriptor.GetConverter(typeof(Currency));
public override Money ReadJson(JsonReader reader, Type objectType, Money existingValue, bool hasExistingValue, JsonSerializer serializer)
{
decimal? amount = null;
string currencyCode = null;
var amountPropertyName = ResolvePropertyName(serializer, nameof(Money.Amount));
var currencyPropertyName = ResolvePropertyName(serializer, nameof(Money.Currency));

while (reader.Read())
{
if (reader.TokenType != JsonToken.PropertyName)
{
break;
}

var propertyName = (string)reader.Value;

if (!reader.Read())
{
break;
}

if (string.Equals(propertyName, amountPropertyName, StringComparison.OrdinalIgnoreCase))
{
amount = serializer.Deserialize<decimal?>(reader);
}

if (string.Equals(propertyName, currencyPropertyName, StringComparison.OrdinalIgnoreCase))
{
currencyCode = serializer.Deserialize<string>(reader);
}
}

if (amount == null)
{
throw new JsonSerializationException($"Unable to deserialize to {nameof(Money)}, since the property {amountPropertyName} was null or missing");
}

if (string.IsNullOrEmpty(currencyCode))
{
throw new JsonSerializationException($"Unable to deserialize to {nameof(Money)}, since the property {currencyPropertyName} was null or missing");
}

return new Money(amount.Value, (Currency)currencyConverter.ConvertFromString(currencyCode));
}

public override void WriteJson(JsonWriter writer, Money value, JsonSerializer serializer)
{
writer.WriteStartObject();

writer.WritePropertyName(ResolvePropertyName(serializer, nameof(Money.Amount)));
serializer.Serialize(writer, value.Amount);

writer.WritePropertyName(ResolvePropertyName(serializer, nameof(Money.Currency)));
writer.WriteValue(value.Currency.Code);

writer.WriteEndObject();
}

static string ResolvePropertyName(JsonSerializer serializer, string propertyName) =>
(serializer.ContractResolver as DefaultContractResolver)?.GetResolvedPropertyName(propertyName) ?? propertyName;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>netcoreapp3.1;netstandard2.0;netstandard2.1;net45;net461;net5</TargetFrameworks>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="12.0.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\NodaMoney\NodaMoney.csproj" />
</ItemGroup>
</Project>
53 changes: 34 additions & 19 deletions src/NodaMoney/Currency.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,14 @@ namespace NodaMoney
[Serializable]
[DebuggerDisplay("{Code}")]
[TypeConverter(typeof(CurrencyTypeConverter))]
public struct Currency : IEquatable<Currency>, IXmlSerializable, ISerializable
public sealed class Currency : IEquatable<Currency>, IXmlSerializable, ISerializable
{
/// <summary>Gets the currency sign (¤), a character used to denote the generic currency sign, when no currency sign is available.</summary>
/// <remarks>See https://en.wikipedia.org/wiki/Currency_sign_(typography). </remarks>
public const string GenericCurrencySign = "¤";

/// <summary>A singleton instance of the currencies registry.</summary>
[NonSerialized]
internal static CurrencyRegistry Registry = new CurrencyRegistry();
/// <summary>Gets a singleton instance of the currencies registry.</summary>
internal static CurrencyRegistry Registry { get; } = new CurrencyRegistry();

/// <summary>Initializes a new instance of the <see cref="Currency" /> struct.</summary>
/// <param name="code">The code.</param>
Expand Down Expand Up @@ -59,7 +58,6 @@ internal Currency(string code, string @namespace = "ISO-4217")
/// <exception cref="System.ArgumentNullException">code or number or englishName or symbol is null.</exception>
/// <exception cref="System.ArgumentOutOfRangeException">DecimalDigits must greater or equal to zero and smaller or equal to 28, or -1 if not applicable.</exception>
internal Currency(string code, string number, double decimalDigits, string englishName, string symbol, string @namespace = "ISO-4217", DateTime? validTo = null, DateTime? validFrom = null)
: this()
{
if (string.IsNullOrWhiteSpace(code))
throw new ArgumentException("Value cannot be null or whitespace.", nameof(code));
Expand All @@ -78,6 +76,10 @@ internal Currency(string code, string number, double decimalDigits, string engli
ValidFrom = validFrom;
}

private Currency()
{
}

#pragma warning disable CA1801 // Parameter context of method.ctor is never used.
private Currency(SerializationInfo info, StreamingContext context)
: this(info.GetString("code"), info.GetString("namespace"))
Expand All @@ -100,19 +102,19 @@ public static Currency CurrentCurrency
}

/// <summary>Gets the currency symbol.</summary>
public string Symbol { get; }
public string Symbol { get; private set; }

/// <summary>Gets the english name of the currency.</summary>
public string EnglishName { get; }
public string EnglishName { get; private set; }

/// <summary>Gets the three-character (ISO-4217) currency code.</summary>
public string Code { get; }
public string Code { get; private set; }

/// <summary>Gets the numeric (ISO-4217) currency number.</summary>
public string Number { get; }
public string Number { get; private set; }

/// <summary>Gets the namespace of the currency, like ISO-4217.</summary>
public string Namespace { get; }
public string Namespace { get; private set; }

/// <summary>Gets the number of digits after the decimal separator.</summary>
/// <remarks>
Expand All @@ -130,15 +132,15 @@ public static Currency CurrentCurrency
/// To represent this in decimal we do the following steps: 5 is 10 to the power of log(5) = 0.69897... ~ 0.7.
/// </para>
/// </remarks>
public double DecimalDigits { get; }
public double DecimalDigits { get; private set; }

/// <summary>Gets the date when the currency is valid from.</summary>
/// <value>The from date when the currency is valid.</value>
public DateTime? ValidFrom { get; internal set; }
public DateTime? ValidFrom { get; private set; }

/// <summary>Gets the date when the currency is valid to.</summary>
/// <value>The to date when the currency is valid.</value>
public DateTime? ValidTo { get; internal set; }
public DateTime? ValidTo { get; private set; }

/// <summary>Gets the major currency unit.</summary>
[SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification = "Member of Currency type! Implementation can change in the future.")]
Expand All @@ -164,7 +166,7 @@ public decimal MinorUnit
/// <param name="left">The left Currency.</param>
/// <param name="right">The right Currency.</param>
/// <returns>The result of the operator.</returns>
public static bool operator ==(Currency left, Currency right) => left.Equals(right);
public static bool operator ==(Currency left, Currency right) => object.Equals(left, right);

/// <summary>Implements the operator ==.</summary>
/// <param name="left">The left Currency.</param>
Expand Down Expand Up @@ -270,6 +272,11 @@ public static Currency FromRegion(string name)
[SuppressMessage("Microsoft.Design", "CA1062:Validate arguments of public methods", MessageId = "0", Justification = "Calling override method")]
public bool Equals(Currency other)
{
if (other == null)
{
return false;
}

return Code == other.Code
&& Namespace == other.Namespace
&& DecimalDigits == other.DecimalDigits
Expand Down Expand Up @@ -323,25 +330,33 @@ public bool IsValidOn(DateTime date)
/// produced by the <see cref="IXmlSerializable.WriteXml(XmlWriter)" /> method and
/// consumed by the <see cref="IXmlSerializable.ReadXml(XmlReader)" /> method.
/// </returns>
public XmlSchema GetSchema() => null;
XmlSchema IXmlSerializable.GetSchema() => null;

/// <summary>Generates an object from its XML representation.</summary>
/// <param name="reader">The <see cref="XmlReader" /> stream from which the object is deserialized.</param>
/// <exception cref="ArgumentNullException">The value of 'reader' cannot be null. </exception>
public void ReadXml(XmlReader reader)
void IXmlSerializable.ReadXml(XmlReader reader)
{
if (reader == null)
throw new ArgumentNullException(nameof(reader));

this = FromCode(reader["Currency"]);
var fromCode = FromCode(reader["Currency"]);
Code = fromCode.Code;
Namespace = fromCode.Namespace;
DecimalDigits = fromCode.DecimalDigits;
EnglishName = fromCode.EnglishName;
Number = fromCode.Number;
Symbol = fromCode.Symbol;
ValidFrom = fromCode.ValidFrom;
ValidTo = fromCode.ValidTo;
}

/// <summary>
/// Converts an object into its XML representation.
/// </summary>
/// <param name="writer">The <see cref="XmlWriter" /> stream to which the object is serialized.</param>
/// <exception cref="ArgumentNullException">The value of 'writer' cannot be null.</exception>
public void WriteXml(XmlWriter writer)
void IXmlSerializable.WriteXml(XmlWriter writer)
{
if (writer == null)
throw new ArgumentNullException(nameof(writer));
Expand All @@ -353,7 +368,7 @@ public void WriteXml(XmlWriter writer)
/// <param name="info">The <see cref="SerializationInfo" /> to populate with data. </param>
/// <param name="context">The destination (see <see cref="StreamingContext" />) for this serialization. </param>
/// <exception cref="System.Security.SecurityException">The caller does not have the required permission. </exception>
public void GetObjectData(SerializationInfo info, StreamingContext context)
void ISerializable.GetObjectData(SerializationInfo info, StreamingContext context)
{
if (info == null)
throw new ArgumentNullException(nameof(info));
Expand Down
3 changes: 3 additions & 0 deletions src/NodaMoney/CurrencyBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,9 @@ public void Save(string fileName)
/// <param name="currency">The object whose properties will be used.</param>
public void LoadDataFromCurrency(Currency currency)
{
if (currency is null)
throw new ArgumentNullException(nameof(currency));

EnglishName = currency.EnglishName;
Symbol = currency.Symbol;
ISONumber = currency.Number;
Expand Down
2 changes: 1 addition & 1 deletion src/NodaMoney/CurrencyRegistry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ public bool TryGet(string code, out Currency currency)
}

currency = found.FirstOrDefault(); // TODO: If more than one, sort by prio.
return !currency.Equals(default);
return currency != null;
}

/// <summary>Tries the get <see cref="Currency"/> of the given code and namespace.</summary>
Expand Down
10 changes: 10 additions & 0 deletions src/NodaMoney/Money.Parsable.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ public static Money Parse(string value, Currency currency)
{
if (string.IsNullOrWhiteSpace(value))
throw new ArgumentNullException(nameof(value));
if (currency is null)
throw new ArgumentNullException(nameof(currency));

return Parse(value, NumberStyles.Currency, GetFormatProvider(currency, null), currency);
}
Expand All @@ -53,6 +55,8 @@ public static Money Parse(string value, NumberStyles style, IFormatProvider prov
{
if (string.IsNullOrWhiteSpace(value))
throw new ArgumentNullException(nameof(value));
if (currency is null)
throw new ArgumentNullException(nameof(currency));

decimal amount = decimal.Parse(value, style, GetFormatProvider(currency, provider));
return new Money(amount, currency);
Expand Down Expand Up @@ -101,6 +105,9 @@ public static bool TryParse(string value, out Money result)
/// <remarks>See <see cref="decimal.TryParse(string, out decimal)"/> for more info and remarks.</remarks>
public static bool TryParse(string value, Currency currency, out Money result)
{
if (currency is null)
throw new ArgumentNullException(nameof(currency));

return TryParse(value, NumberStyles.Currency, GetFormatProvider(currency, null), currency, out result);
}

Expand All @@ -118,6 +125,9 @@ public static bool TryParse(string value, Currency currency, out Money result)
/// <remarks>See <see cref="decimal.TryParse(string, NumberStyles, IFormatProvider, out decimal)"/> for more info and remarks.</remarks>
public static bool TryParse(string value, NumberStyles style, IFormatProvider provider, Currency currency, out Money result)
{
if (currency is null)
throw new ArgumentNullException(nameof(currency));

bool isParsingSuccessful = decimal.TryParse(value, style, GetFormatProvider(currency, provider), out decimal amount);
if (isParsingSuccessful)
{
Expand Down
Loading