Skip to content

Commit c85f4a8

Browse files
Create how-to-rank-categories-based-on-total-of-another-field-with-custom-aggregate.md
1 parent 20d8ceb commit c85f4a8

File tree

1 file changed

+176
-0
lines changed

1 file changed

+176
-0
lines changed
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
---
2+
title: How to Rank by total result/score/value
3+
description: Sort categories on total of a field with custom ranking aggregate
4+
type: how-to
5+
page_title: Rank category field based on total of another field with custom aggregate function
6+
slug: how-to-rank-categories-based-on-total-of-another-field-with-custom-aggregate
7+
position:
8+
tags:
9+
ticketid: 1363830
10+
res_type: kb
11+
---
12+
13+
## Environment
14+
<table>
15+
<tr>
16+
<td>Product</td>
17+
<td>Progress® Telerik® Reporting</td>
18+
</tr>
19+
</table>
20+
21+
22+
## Description
23+
Sometimes it is necessary to determine the rank of a category value (e.g. competitor ID) based on some aggregate value (e.g. total score from several competitions).
24+
25+
## Solution
26+
It is necessary to create a [Custom Aggregate](https://docs.telerik.com/reporting/expressions-user-aggregate-functions) that achieves such requirement. Here is a sample code :
27+
28+
```CSharp
29+
using System.Collections.Generic;
30+
using System.Linq;
31+
using Telerik.Reporting.Expressions;
32+
33+
namespace CustomAggregates
34+
{
35+
[AggregateFunction(Name = "RankByCategory",
36+
Description = "Defines an aggregate function to get the rank of a field based on total value of another field.")]
37+
38+
public class RankByCategoryAggregate : IAggregateFunction
39+
{
40+
private Dictionary<string, double> sums;
41+
private Ranks result;
42+
43+
/// <summary>
44+
/// Initializes the current aggregate function to its initial
45+
/// state ready to accumulate and merge values.
46+
/// </summary>
47+
/// <remarks>
48+
/// This method is called every time the accumulation of values
49+
/// must start over for a new subset of records from the data source.
50+
/// </remarks>
51+
public void Init()
52+
{
53+
this.sums = new Dictionary<string, double>();
54+
}
55+
56+
/// <summary>
57+
/// Accumulates new argument values to the current aggregate function.
58+
/// </summary>
59+
/// <remarks>
60+
/// This aggregate function accepts one argument:
61+
/// number - a numeric value to accumulate to the aggregate function;
62+
/// </remarks>
63+
64+
public void Accumulate(object[] values)
65+
{
66+
this.AccumulateCore((string)values[0], (double)values[1]);
67+
}
68+
69+
void AccumulateCore(string key, double value)
70+
{
71+
double currentValue;
72+
if (this.sums.TryGetValue(key, out currentValue))
73+
{
74+
this.sums[key] = currentValue + value;
75+
}
76+
else
77+
{
78+
this.sums[key] = value;
79+
}
80+
this.result = null;
81+
}
82+
83+
84+
/// <summary>
85+
/// Merges the specified aggregate function to the current one.
86+
/// </summary>
87+
/// <param name="Aggregate">
88+
/// Specifies an aggregate function to be merged to the current one.
89+
/// </param>
90+
/// <remarks>
91+
/// This method allows the reporting engine to merge two accumulated
92+
/// subsets of the same aggregate function into a single result.
93+
/// </remarks>
94+
public void Merge(IAggregateFunction aggregate)
95+
{
96+
// Accumulate the values of the specified aggregate function.
97+
foreach (KeyValuePair<string, double> pair in ((RankByCategoryAggregate)aggregate).sums)
98+
{
99+
this.AccumulateCore(pair.Key, pair.Value);
100+
}
101+
}
102+
103+
/// <summary>
104+
/// Returns the currently accumulated value of the aggregate function.
105+
/// </summary>
106+
/// <returns>
107+
/// The currently accumulated numeric value of the aggregate function.
108+
/// </returns>
109+
public object GetValue()
110+
{
111+
if (this.result == null)
112+
{
113+
this.result = Ranks.FromValues(this.sums);
114+
}
115+
116+
return this.result;
117+
}
118+
}
119+
120+
class Ranks
121+
{
122+
private Dictionary<string, int> ranks;
123+
124+
public Ranks(Dictionary<string, int> newRanks)
125+
{
126+
this.ranks = newRanks;
127+
}
128+
129+
public static Ranks FromValues(Dictionary<string, double> values)
130+
{
131+
List<KeyValuePair<string, double>> myList = values.ToList();
132+
133+
myList.Sort(
134+
delegate (KeyValuePair<string, double> firstPair,
135+
KeyValuePair<string, double> nextPair)
136+
{
137+
// Sort descending
138+
return nextPair.Value.CompareTo(firstPair.Value);
139+
});
140+
141+
int currentRank = 1;
142+
double currentValue = double.MaxValue;
143+
Dictionary<string, int> newRanks = new Dictionary<string, int>();
144+
for (int i = 0; i < myList.Count; i++)
145+
{
146+
// Same values will result in the same rank. Next ranks will be skipped (i.e. Values [2.2, 2.2, 0] -> Ranks [1, 1, 3])
147+
if (currentValue != myList[i].Value)
148+
{
149+
currentRank = i + 1;
150+
currentValue = myList[i].Value;
151+
}
152+
153+
newRanks.Add(myList[i].Key, currentRank);
154+
}
155+
156+
return new Ranks(newRanks);
157+
}
158+
159+
public int GetRank(string key)
160+
{
161+
return this.ranks[key];
162+
}
163+
}
164+
}
165+
```
166+
167+
Note that with the above implementation the equal values will be ranked equally and the next ranks will be skipped, i.e.
168+
- Values [2.2, 2.2, 0] -> Ranks [1, 1, 3]
169+
- Values [2, 2.2, 0] -> Ranks [2, 1, 3]
170+
171+
The aggregate function can be used in an [Expression](https://docs.telerik.com/reporting/report-expressions) like :
172+
```
173+
= Exec('crosstab1', RankByCategory(Fields.AssociateName, CDbl(Fields.CalculatedScore))).GetRank(Fields.AssociateName)
174+
```
175+
176+
The above expression will rank _Fields.AssociateName_ based on the sum of _Fields.CalculatedScore_ for the row groups of _crosstab1_ grouped by _Fields.AssociateName_.

0 commit comments

Comments
 (0)