Skip to content
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
2 changes: 1 addition & 1 deletion .github/workflows/pull_request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ jobs:
uses: actions/setup-dotnet@v4
with:
dotnet-version: 9

- name: Build
run: dotnet build Engine/Quokka.slnx --configuration Release

Expand Down
2 changes: 1 addition & 1 deletion Engine/Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,6 @@
</PropertyGroup>

<PropertyGroup>
<VersionPrefix>8.3.0</VersionPrefix>
<VersionPrefix>8.4.0</VersionPrefix>
</PropertyGroup>
</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// // Copyright 2022 Mindbox Ltd
// //
// // Licensed under the Apache License, Version 2.0 (the "License");
// // you may not use this file except in compliance with the License.
// // You may obtain a copy of the License at
// //
// // http://www.apache.org/licenses/LICENSE-2.0
// //
// // Unless required by applicable law or agreed to in writing, software
// // distributed under the License is distributed on an "AS IS" BASIS,
// // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// // See the License for the specific language governing permissions and
// // limitations under the License.

using System.Globalization;

namespace Mindbox.Quokka
{
internal static class CurrencyAmountFormats
{
public const int DefaultDecimalPlaces = 2;

private static readonly string[] formatsByDecimalPlaces = ["N0", "N1", "N2", "N3", "N4"];

public static readonly NumberFormatInfo NumberFormat = NumberFormatInfo.ReadOnly(
new NumberFormatInfo
{
NumberGroupSeparator = ",",
NumberDecimalSeparator = ".",
NumberGroupSizes = [3]
});

public static string ForDecimalPlaces(int decimalPlaces) => formatsByDecimalPlaces[decimalPlaces];
}
}
63 changes: 63 additions & 0 deletions Engine/Quokka.Core/Functions/Standard/Money/CurrencyDisplayMode.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// // Copyright 2022 Mindbox Ltd
// //
// // Licensed under the Apache License, Version 2.0 (the "License");
// // you may not use this file except in compliance with the License.
// // You may obtain a copy of the License at
// //
// // http://www.apache.org/licenses/LICENSE-2.0
// //
// // Unless required by applicable law or agreed to in writing, software
// // distributed under the License is distributed on an "AS IS" BASIS,
// // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// // See the License for the specific language governing permissions and
// // limitations under the License.

using System;

namespace Mindbox.Quokka
{
internal enum CurrencyDisplayMode
{
NarrowSymbol,
Symbol,
Code
}

internal static class CurrencyDisplayModes
{
public const string NarrowSymbolName = "narrowSymbol";
public const string SymbolName = "symbol";
public const string CodeName = "code";

public static bool TryParse(string value, out CurrencyDisplayMode mode)
{
var name = value?.Trim();
if (string.IsNullOrEmpty(name) || Matches(name, NarrowSymbolName))
{
mode = CurrencyDisplayMode.NarrowSymbol;
return true;
}

if (Matches(name, SymbolName))
{
mode = CurrencyDisplayMode.Symbol;
return true;
}

if (Matches(name, CodeName))
{
mode = CurrencyDisplayMode.Code;
return true;
}

mode = CurrencyDisplayMode.NarrowSymbol;
return false;
}

public static CurrencyDisplayMode ParseOrDefault(string value) =>
TryParse(value, out var mode) ? mode : CurrencyDisplayMode.NarrowSymbol;

private static bool Matches(string value, string name) =>
string.Equals(value, name, StringComparison.OrdinalIgnoreCase);
}
}
38 changes: 38 additions & 0 deletions Engine/Quokka.Core/Functions/Standard/Money/CurrencyFormat.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// // Copyright 2022 Mindbox Ltd
// //
// // Licensed under the Apache License, Version 2.0 (the "License");
// // you may not use this file except in compliance with the License.
// // You may obtain a copy of the License at
// //
// // http://www.apache.org/licenses/LICENSE-2.0
// //
// // Unless required by applicable law or agreed to in writing, software
// // distributed under the License is distributed on an "AS IS" BASIS,
// // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// // See the License for the specific language governing permissions and
// // limitations under the License.

namespace Mindbox.Quokka
{
internal sealed class CurrencyFormat
{
public CurrencyFormat(string narrowSymbol, string symbol, int decimalPlaces, bool spaceAfterSymbol = false)
{
NarrowSymbol = narrowSymbol;
Symbol = symbol;
DecimalPlaces = decimalPlaces;
SpaceAfterSymbol = spaceAfterSymbol;
AmountFormat = CurrencyAmountFormats.ForDecimalPlaces(decimalPlaces);
}

public string NarrowSymbol { get; }

public string Symbol { get; }

public int DecimalPlaces { get; }

public bool SpaceAfterSymbol { get; }

public string AmountFormat { get; }
}
}
42 changes: 42 additions & 0 deletions Engine/Quokka.Core/Functions/Standard/Money/CurrencyFormats.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// // Copyright 2022 Mindbox Ltd
// //
// // Licensed under the Apache License, Version 2.0 (the "License");
// // you may not use this file except in compliance with the License.
// // You may obtain a copy of the License at
// //
// // http://www.apache.org/licenses/LICENSE-2.0
// //
// // Unless required by applicable law or agreed to in writing, software
// // distributed under the License is distributed on an "AS IS" BASIS,
// // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// // See the License for the specific language governing permissions and
// // limitations under the License.

using System;
using System.Collections.Concurrent;
using NodaMoney;

namespace Mindbox.Quokka
{
internal static class CurrencyFormats
{
private static readonly ConcurrentDictionary<string, CurrencyFormat> knownFormats =
new ConcurrentDictionary<string, CurrencyFormat>(StringComparer.OrdinalIgnoreCase);

public static CurrencyFormat TryGet(string currencyCode)
{
if (string.IsNullOrEmpty(currencyCode))
return null;

if (knownFormats.TryGetValue(currencyCode, out var knownFormat))
return knownFormat;

if (!CurrencyInfo.TryFromCode(currencyCode, out var currency))
return null;

var format = new CurrencyFormat(currency.Symbol, currency.InternationalSymbol, currency.DecimalDigits);
knownFormats[currencyCode] = format;
return format;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// // Copyright 2022 Mindbox Ltd
// //
// // Licensed under the Apache License, Version 2.0 (the "License");
// // you may not use this file except in compliance with the License.
// // You may obtain a copy of the License at
// //
// // http://www.apache.org/licenses/LICENSE-2.0
// //
// // Unless required by applicable law or agreed to in writing, software
// // distributed under the License is distributed on an "AS IS" BASIS,
// // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// // See the License for the specific language governing permissions and
// // limitations under the License.

using Mindbox.Quokka.Abstractions;

namespace Mindbox.Quokka
{
internal class FormatMoneyTemplateFunction : ScalarTemplateFunction<decimal, string, string>
{
public FormatMoneyTemplateFunction()
: base(
"formatMoney",
new DecimalFunctionArgument("amount", allowsNull: true),
new StringFunctionArgument("currencyCode", allowsNull: true))
{
}

public override string Invoke(RenderSettings settings, decimal amount, string currencyCode)
{
return MoneyFormatter.Format(amount, currencyCode, CurrencyDisplayMode.NarrowSymbol);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// // Copyright 2022 Mindbox Ltd
// //
// // Licensed under the Apache License, Version 2.0 (the "License");
// // you may not use this file except in compliance with the License.
// // You may obtain a copy of the License at
// //
// // http://www.apache.org/licenses/LICENSE-2.0
// //
// // Unless required by applicable law or agreed to in writing, software
// // distributed under the License is distributed on an "AS IS" BASIS,
// // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// // See the License for the specific language governing permissions and
// // limitations under the License.

using Mindbox.Quokka.Abstractions;

namespace Mindbox.Quokka
{
internal class FormatMoneyWithDisplayModeTemplateFunction : ScalarTemplateFunction<decimal, string, string, string>
{
public FormatMoneyWithDisplayModeTemplateFunction()
: base(
"formatMoney",
new DecimalFunctionArgument("amount", allowsNull: true),
new StringFunctionArgument("currencyCode", allowsNull: true),
new StringFunctionArgument("displayMode", allowsNull: true, valueValidator: ValidateDisplayMode))
{
}

public override string Invoke(RenderSettings settings, decimal amount, string currencyCode, string displayMode)
{
return MoneyFormatter.Format(amount, currencyCode, CurrencyDisplayModes.ParseOrDefault(displayMode));
}

private static ArgumentValueValidationResult ValidateDisplayMode(string displayMode)
{
return CurrencyDisplayModes.TryParse(displayMode, out _)
? ArgumentValueValidationResult.Valid
: new ArgumentValueValidationResult(
false,
$"Display mode should be one of: {CurrencyDisplayModes.NarrowSymbolName}, "
+ $"{CurrencyDisplayModes.SymbolName}, {CurrencyDisplayModes.CodeName}");
}
}
}
54 changes: 54 additions & 0 deletions Engine/Quokka.Core/Functions/Standard/Money/MoneyFormatter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// // Copyright 2022 Mindbox Ltd
// //
// // Licensed under the Apache License, Version 2.0 (the "License");
// // you may not use this file except in compliance with the License.
// // You may obtain a copy of the License at
// //
// // http://www.apache.org/licenses/LICENSE-2.0
// //
// // Unless required by applicable law or agreed to in writing, software
// // distributed under the License is distributed on an "AS IS" BASIS,
// // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// // See the License for the specific language governing permissions and
// // limitations under the License.

using System;

namespace Mindbox.Quokka
{
internal static class MoneyFormatter
{
private const string MinusSign = "-";
private const string Space = " ";

public static string Format(decimal amount, string currencyCode, CurrencyDisplayMode displayMode)
{
var code = currencyCode?.Trim();
var format = CurrencyFormats.TryGet(code);
var decimalPlaces = format?.DecimalPlaces ?? CurrencyAmountFormats.DefaultDecimalPlaces;
var absolute = Math.Round(Math.Abs(amount), decimalPlaces, MidpointRounding.AwayFromZero);

var formattedAmount = absolute.ToString(
format?.AmountFormat ?? CurrencyAmountFormats.ForDecimalPlaces(decimalPlaces),
CurrencyAmountFormats.NumberFormat);

var sign = amount < 0 && absolute != decimal.Zero ? MinusSign : string.Empty;

if (format == null)
return string.IsNullOrEmpty(code)
? sign + formattedAmount
: sign + formattedAmount + Space + code.ToUpperInvariant();

if (displayMode == CurrencyDisplayMode.Code)
return sign + code.ToUpperInvariant() + Space + formattedAmount;

var symbol = displayMode == CurrencyDisplayMode.Symbol ? format.Symbol : format.NarrowSymbol;
var lastCharacter = symbol[symbol.Length - 1];
var separator = format.SpaceAfterSymbol || char.IsLetter(lastCharacter) || lastCharacter == '.'
? Space
: string.Empty;

return sign + symbol + separator + formattedAmount;
}
}
}
1 change: 1 addition & 0 deletions Engine/Quokka.Core/Mindbox.Quokka.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Antlr4.Runtime.Standard" Version="4.13.1" />
<PackageReference Include="NodaMoney" Version="2.7.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Mindbox.Quokka.Abstractions\Mindbox.Quokka.Abstractions.csproj" />
Expand Down
2 changes: 2 additions & 0 deletions Engine/Quokka.Core/Template.cs
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,8 @@ internal static IEnumerable<TemplateFunction> GetStandardFunctions()
yield return new ToLowerTemplateFunction();
yield return new ReplaceIfEmptyTemplateFunction();
yield return new FormatDecimalTemplateFunction();
yield return new FormatMoneyTemplateFunction();
yield return new FormatMoneyWithDisplayModeTemplateFunction();
yield return new FormatDateTimeTemplateFunction();
yield return new FormatTimeTemplateFunction();
yield return new IfTemplateFunction();
Expand Down
Loading
Loading