Skip to content

Commit 0368bf9

Browse files
authored
Merge pull request #20 from PandaTechAM/development
Extreme performance boost with backward compatability
2 parents 2e4d2d5 + 53c3255 commit 0368bf9

File tree

10 files changed

+230
-167
lines changed

10 files changed

+230
-167
lines changed

Readme.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ period validations remain precise and reliable.
2323
- **Configurable** - The library is highly configurable, allowing you to define your own commission ranges and
2424
commission types.
2525
- **Robust Testing** - Achieves 99% code coverage, ensuring reliability.
26-
- **Optimized Performance** - Delivers high performance, capable of over 1.5 million calculations per second.
26+
- **Optimized Performance** - Delivers high performance, capable of over 5 million calculations per second with 0 ram
27+
allocation.
2728

2829
## 1.3. Installation
2930

Lines changed: 47 additions & 128 deletions
Original file line numberDiff line numberDiff line change
@@ -1,146 +1,80 @@
11
using CommissionCalculator.DTO;
2+
using CommissionCalculator.Internal;
3+
using static CommissionCalculator.Internal.FastPath;
24

35
namespace CommissionCalculator;
46

57
public static class Commission
68
{
7-
private const decimal DecimalEpsilon = 1e-28M; //smallest decimal value that is greater than zero
8-
9+
// Principal-based
910
public static decimal ComputeCommission(decimal principalAmount, CommissionRule rule)
1011
{
11-
decimal commission;
12-
13-
if (rule.CalculationType == CalculationType.Proportional)
14-
{
15-
commission = CalculateProportionalCommission(principalAmount, rule);
16-
return Math.Round(commission, rule.DecimalPlace);
17-
}
18-
19-
commission = CalculateAbsoluteCommission(principalAmount, rule);
20-
return Math.Round(commission, rule.DecimalPlace);
21-
}
22-
23-
private static decimal CalculateAbsoluteCommission(decimal principalAmount, CommissionRule rule)
24-
{
25-
rule = ConvertCommissionRanges(rule);
12+
var nr = CommissionRuleCache.Get(rule);
2613

27-
var range = rule.CommissionRangeConfigs.FirstOrDefault(r =>
28-
principalAmount >= r.RangeStart && principalAmount < r.RangeEnd);
14+
var commission = nr.CalcType == CalculationType.Proportional
15+
? CalculateProportional(principalAmount, nr)
16+
: CalculateAbsolute(principalAmount, nr);
2917

30-
return ComputeRangeCommission(range!.Type,
31-
range.CommissionAmount,
32-
range.MinCommission,
33-
range.MaxCommission,
34-
principalAmount);
18+
return Math.Round(commission, nr.DecimalPlaces);
3519
}
3620

21+
// Selector-based (selector chooses range; commission is applied to principal)
3722
public static decimal ComputeCommission(decimal principalAmount, decimal selectorValue, CommissionRule rule)
3823
{
39-
if (rule.CalculationType == CalculationType.Proportional)
24+
var nr = CommissionRuleCache.Get(rule);
25+
if (nr.CalcType == CalculationType.Proportional)
4026
{
4127
throw new InvalidOperationException(
4228
"Selector-based overload is incompatible with Proportional rules. Use Absolute.");
4329
}
4430

45-
var converted = ConvertCommissionRanges(rule);
31+
var idx = FindRangeIndex(nr, selectorValue);
32+
var r = nr.Ranges[idx];
4633

47-
var range = converted.CommissionRangeConfigs.FirstOrDefault(r =>
48-
selectorValue >= r.RangeStart && selectorValue < r.RangeEnd);
34+
var commission = ComputeRangeCommission(r.Type, r.Amount, r.Min, r.Max, principalAmount);
35+
return Math.Round(commission, nr.DecimalPlaces);
36+
}
4937

50-
var commission = ComputeRangeCommission(
51-
range!.Type,
52-
range.CommissionAmount,
53-
range.MinCommission,
54-
range.MaxCommission,
55-
principalAmount
56-
);
38+
// ===== Fast paths using normalized rules =====
5739

58-
return Math.Round(commission, converted.DecimalPlace);
40+
private static decimal CalculateAbsolute(decimal principalAmount, NormalizedCommissionRule nr)
41+
{
42+
var idx = FindRangeIndex(nr, principalAmount);
43+
var r = nr.Ranges[idx];
44+
return ComputeRangeCommission(r.Type, r.Amount, r.Min, r.Max, principalAmount);
5945
}
6046

61-
private static decimal CalculateProportionalCommission(decimal principalAmount, CommissionRule rule)
47+
private static decimal CalculateProportional(decimal principalAmount, NormalizedCommissionRule nr)
6248
{
63-
rule = ConvertCommissionRanges(rule);
49+
// Find the current tier
50+
var idx = FindRangeIndex(nr, principalAmount);
51+
var r = nr.Ranges[idx];
6452

65-
decimal commission = 0;
53+
// Sum of fully completed prior tiers
54+
var sum = nr.ProportionalPrefix.Length == 0 ? 0 : nr.ProportionalPrefix[idx];
6655

56+
// Partial of the current tier
57+
var portion = principalAmount - r.Start;
58+
sum += ComputeRangeCommission(r.Type, r.Amount, r.Min, r.Max, portion);
6759

68-
foreach (var range in rule.CommissionRangeConfigs)
69-
{
70-
if (principalAmount >= range.RangeStart && principalAmount < range.RangeEnd)
71-
{
72-
var portionOfPrincipal = principalAmount - range.RangeStart;
73-
74-
commission += ComputeRangeCommission(range.Type,
75-
range.CommissionAmount,
76-
range.MinCommission,
77-
range.MaxCommission,
78-
portionOfPrincipal);
79-
}
80-
81-
if (principalAmount < range.RangeEnd)
82-
{
83-
continue;
84-
}
85-
86-
{
87-
var portionOfPrincipal = range.RangeEnd - range.RangeStart - DecimalEpsilon;
88-
89-
commission += ComputeRangeCommission(range.Type,
90-
range.CommissionAmount,
91-
range.MinCommission,
92-
range.MaxCommission,
93-
portionOfPrincipal);
94-
}
95-
}
96-
97-
return commission;
60+
return sum;
9861
}
9962

100-
private static decimal ComputeRangeCommission(CommissionType commissionType,
101-
decimal commission,
102-
decimal minimum,
103-
decimal maximum,
104-
decimal principalAmount)
63+
// ===== Validation (public contract) =====
64+
65+
public static bool ValidateRule(CommissionRule rule)
10566
{
106-
if (commissionType == CommissionType.FlatRate)
67+
try
10768
{
108-
return commission;
69+
ValidateCommissionRule(rule);
70+
return true;
10971
}
110-
111-
var computedCommission = principalAmount * commission;
112-
if (computedCommission < minimum)
72+
catch
11373
{
114-
return minimum;
74+
return false;
11575
}
116-
117-
return computedCommission > maximum ? maximum : computedCommission;
118-
}
119-
120-
private static CommissionRule ConvertCommissionRanges(CommissionRule rule)
121-
{
122-
ValidateCommissionRule(rule);
123-
124-
var convertedRanges = rule.CommissionRangeConfigs
125-
.Select(c => new CommissionRangeConfigs
126-
{
127-
RangeStart = c.RangeStart,
128-
RangeEnd = c.RangeEnd == 0 ? decimal.MaxValue : c.RangeEnd,
129-
Type = c.Type,
130-
CommissionAmount = c.CommissionAmount,
131-
MinCommission = c.MinCommission,
132-
MaxCommission = c.MaxCommission == 0 ? decimal.MaxValue : c.MaxCommission
133-
})
134-
.ToList();
135-
return new CommissionRule
136-
{
137-
CalculationType = rule.CalculationType,
138-
DecimalPlace = rule.DecimalPlace,
139-
CommissionRangeConfigs = convertedRanges
140-
};
14176
}
14277

143-
14478
private static void ValidateCommissionRule(CommissionRule rule)
14579
{
14680
if (rule == null || rule.CommissionRangeConfigs.Count == 0)
@@ -157,21 +91,21 @@ private static void ValidateCommissionRule(CommissionRule rule)
15791

15892
if (rule.CommissionRangeConfigs.Count == 1)
15993
{
160-
if (rule.CommissionRangeConfigs[0].RangeStart != 0 || rule.CommissionRangeConfigs[0].RangeEnd != 0)
94+
var only = rule.CommissionRangeConfigs[0];
95+
if (only.RangeStart != 0 || only.RangeEnd != 0)
16196
{
16297
throw new InvalidOperationException("In case of one range, both 'From' and 'To' should be 0.");
16398
}
16499

165-
if (rule.CommissionRangeConfigs[0].MaxCommission != 0 && rule.CommissionRangeConfigs[0].MaxCommission <
166-
rule.CommissionRangeConfigs[0].MinCommission)
100+
if (only.MaxCommission != 0 && only.MaxCommission < only.MinCommission)
167101
{
168102
throw new InvalidOperationException("MaxCommission should be greater than or equal to MinCommission.");
169103
}
104+
105+
return;
170106
}
171-
else
172-
{
173-
ValidateEachRange(rule);
174-
}
107+
108+
ValidateEachRange(rule);
175109
}
176110

177111
private static void ValidateEachRange(CommissionRule rule)
@@ -188,7 +122,6 @@ private static void ValidateEachRange(CommissionRule rule)
188122
}
189123

190124
var verifiedRules = 1;
191-
192125
var lastTo = startRule.RangeEnd;
193126

194127
while (true)
@@ -215,7 +148,6 @@ private static void ValidateEachRange(CommissionRule rule)
215148
}
216149

217150
verifiedRules++;
218-
219151
lastTo = nextRule!.RangeEnd;
220152
}
221153

@@ -224,17 +156,4 @@ private static void ValidateEachRange(CommissionRule rule)
224156
throw new InvalidOperationException("There is some nested or gap ranges in the rules.");
225157
}
226158
}
227-
228-
public static bool ValidateRule(CommissionRule rule)
229-
{
230-
try
231-
{
232-
ValidateCommissionRule(rule);
233-
return true;
234-
}
235-
catch (Exception)
236-
{
237-
return false;
238-
}
239-
}
240159
}

src/CommissionCalculator/CommissionCalculator.csproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,14 @@
77
<PackageIcon>pandatech.png</PackageIcon>
88
<PackageReadmeFile>Readme.md</PackageReadmeFile>
99
<Copyright>MIT</Copyright>
10-
<Version>4.0.2</Version>
10+
<Version>4.1.0</Version>
1111
<Authors>Pandatech</Authors>
1212
<PackageId>Pandatech.CommissionCalculator</PackageId>
1313
<Title>Pandatech.CommissionCalculator</Title>
1414
<PackageTags>Pandatech, library, calculator, commission, fee, bank, fintech</PackageTags>
1515
<Description>PandaTech.CommissionCalculator is a .NET library simplifying common fintech commission calculations.</Description>
1616
<RepositoryUrl>https://github.com/PandaTechAM/be-lib-commission-calculator.git</RepositoryUrl>
17-
<PackageReleaseNotes>Nuget updates and new overload added</PackageReleaseNotes>
17+
<PackageReleaseNotes>Extreme performance boost with backward compatability</PackageReleaseNotes>
1818
</PropertyGroup>
1919
<ItemGroup>
2020
<None Include="..\..\pandatech.png" Pack="true" PackagePath="\"/>

src/CommissionCalculator/Helper/DateTimeOverlapChecker.cs

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,7 @@ public static class DateTimeOverlapChecker
66
{
77
public static bool HasOverlap(List<DateTimePair> firstPairs, List<DateTimePair> secondPairs)
88
{
9-
foreach (var firstPair in firstPairs)
10-
{
11-
foreach (var secondPair in secondPairs)
12-
{
13-
if (IsOverlapping(firstPair, secondPair))
14-
{
15-
return true;
16-
}
17-
}
18-
}
19-
20-
return false;
9+
return firstPairs.Any(firstPair => secondPairs.Any(secondPair => IsOverlapping(firstPair, secondPair)));
2110
}
2211

2312
private static bool IsOverlapping(DateTimePair firstPair, DateTimePair secondPair)
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
using System.Runtime.CompilerServices;
2+
using CommissionCalculator.DTO;
3+
4+
namespace CommissionCalculator.Internal;
5+
6+
internal static class CommissionRuleCache
7+
{
8+
private static readonly ConditionalWeakTable<CommissionRule, NormalizedCommissionRule> Cache = new();
9+
10+
public static NormalizedCommissionRule Get(CommissionRule rule) => Cache.GetValue(rule, Normalize);
11+
12+
private static NormalizedCommissionRule Normalize(CommissionRule rule)
13+
{
14+
// Reuse public validation contract (throws if invalid)
15+
if (!Commission.ValidateRule(rule))
16+
{
17+
throw new ArgumentException("Invalid commission rule.");
18+
}
19+
20+
// Convert once: map 0 => +∞ for RangeEnd/Max, then sort by Start
21+
var src = rule.CommissionRangeConfigs;
22+
var list = new List<NormalizedRange>(src.Count);
23+
foreach (var c in src)
24+
{
25+
var end = c.RangeEnd == 0 ? decimal.MaxValue : c.RangeEnd;
26+
var max = c.MaxCommission == 0 ? decimal.MaxValue : c.MaxCommission;
27+
list.Add(new NormalizedRange(c.RangeStart, end, c.Type, c.CommissionAmount, c.MinCommission, max));
28+
}
29+
30+
list.Sort((a, b) => a.Start.CompareTo(b.Start));
31+
var ranges = list.ToArray();
32+
33+
// Precompute proportional prefix (full-range contributions). Last +∞ contributes 0 (never fully consumed).
34+
decimal[] prefix;
35+
if (rule.CalculationType == CalculationType.Proportional)
36+
{
37+
prefix = new decimal[ranges.Length + 1];
38+
for (var i = 0; i < ranges.Length; i++)
39+
{
40+
var r = ranges[i];
41+
if (r.End == decimal.MaxValue)
42+
{
43+
prefix[i + 1] = prefix[i];
44+
continue;
45+
}
46+
47+
var width = r.End - r.Start; // finite
48+
prefix[i + 1] = prefix[i] + FastPath.ComputeRangeCommission(r.Type, r.Amount, r.Min, r.Max, width);
49+
}
50+
}
51+
else
52+
{
53+
prefix = [];
54+
}
55+
56+
return new NormalizedCommissionRule(rule.CalculationType, rule.DecimalPlace, ranges, prefix);
57+
}
58+
}

0 commit comments

Comments
 (0)