C# median value among group calculation - c#

I have to calculate the median for a group of values in one of my C# function. I have used the formula from the site, mathisfun. Following is my data model and the code.
public class DataModel
{
public string Group { get; set; }
public long? Population { get; set; }
}
Sample dataList is as follows,
> dataList Count = 7
> [0]: {DataModel}
> Group: "16 to 24" Population: 39657245
> [1]: {DataModel} Group: "25 to 34" Population: 58957845
> [2]: {DataModel} Group: "35 to 44" Population: 12557845
> [3]: {DataModel} Group: "45 to 54" Population: 25698746
> [4]: {DataModel} Group: "55 to 64" Population: 325487
Following is the function logic which takes the dataList as a input and returns the median value as output.
public int CalculateMedianAge(IList<DataModel> dataList)
{
int median = 0;
var sum = 0;
var sumRange = 0;
DataModel medianGroup = new DataModel();
foreach (var item in dataList)
{
sum = sum + (int)item.Population;
}
int range = (sum + 1) / 2;
foreach(var entry in dataList)
{
sumRange = sumRange + (int)entry.Population;
if (range > sumRange)
continue;
else
{
medianGroup = entry;
break;
}
}
var lowerBoundary = int.Parse(medianGroup.Group.Split(' ')[0]) - 0.5;
var cumulativeFrequency = 0;
for (int s = 0; s< dataList.IndexOf(medianGroup); s++)
{
cumulativeFrequency = cumulativeFrequency + (int)dataList[s].Population;
}
var width = int.Parse(medianGroup.Group.Split(' ')[2]) - int.Parse(medianGroup.Group.Split(' ')[0]);
//L is the lower class boundary of the group containing the median - lowerBoundary
//n is the total number of values - sum
//B is the cumulative frequency of the groups before the median group - cumulativeFrequency
//G is the frequency of the median group - (int)lowerBoundary.Population
//w is the group width - width
//MedianAge = L + (((n/2) - B) / G) * W
median = (int)(lowerBoundary + (((sum/2) - cumulativeFrequency) / (int)medianGroup.Population) * width);
return median;
}
It is working fine and I can able to get the median value as well. But I am trying to re-factor it with LINQ. I don't want to keep it with Continue and Break statements.
Can anyone suggest/re-factor the above?

This definitely looks bad:
var population = dataList.Sum(x => x.pop);
var aggregate = 0;
var median = dataList
.Select(x => new
{
split = x.Group.Split(" to "),
pop = (int)x.Population
})
.Select(x => new
{
from = int.Parse(x.split[0]),
to = int.Parse(x.split[1]),
x.pop
})
//median calculation here VVVV
.SelectMany(x=>
Enumerable
.Range(x.from, x.to - x.from + 1)
.Select(y=> new
{
age = y,
pop = x.pop/(x.to - x.from + 1) //tail lost here, for small values will return incorrect values. Distribution through linq is bad idea here.
})
)
.OrderBy(x => x.age)
.First(x => (aggregate+= x.pop) >= population/2)
.age;
PS: Not tested this. Your approach through for is good. Linq is bad to use here.

Related

How to find all possible values of 100 dollar bill [closed]

Closed. This question needs details or clarity. It is not currently accepting answers.
Want to improve this question? Add details and clarify the problem by editing this post.
Closed 2 months ago.
Improve this question
banknotes provided = 10 Dollar, 50 Dollar, 100 Dollar
Expected Result for 100 dollar bill with above bank notes.
I want to show the denomination exactly like below
5 x 10 + 1x 50
2 x 50
1 x 100
10 x 10
As you can see all the banknotes denomination total to 100 above
Let's split the problem in two:
Solve the exchange problem of total given notes (in descending order), starting from nominal at index.
Provide the solution in required format
Code:
private static IEnumerable<int[]> Solutions(int total, int[] notes, int index) {
int value = notes[index];
if (index == notes.Length - 1) {
if (total % value == 0)
yield return new int[] { total / value };
}
else
for (int i = total / value; i >= 0; --i)
foreach (int[] rest in Solutions(total - value * i, notes, index + 1))
yield return rest.Prepend(i).ToArray();
}
private static IEnumerable<string> Solve(int total, int[] notes) {
var items = notes.OrderByDescending(item => item).ToArray();
foreach (int[] solution in Solutions(total, items, 0))
yield return string.Join(" + ", solution
.Zip(items, (count, note) => (count, note))
.Where(pair => pair.count > 0)
.Select(pair => $"{pair.count} x {pair.note}"));
}
Demo:
int total = 170;
int[] notes = new int[] {10, 50, 100};
Console.Write(string.Join(Environment.NewLine, Solve(total, notes)));
Output:
1 x 100 + 1 x 50 + 2 x 10
1 x 100 + 7 x 10
3 x 50 + 2 x 10
2 x 50 + 7 x 10
1 x 50 + 12 x 10
17 x 10
Fiddle
Here is another (way less compact and cool than Dmitry's) solution, that uses recursion as you said. Basically I create a tree for each possible sum of the available banknotes that starts with the given amount at the root. For example, for your given amount of 100, and the given notes 10, 50, 100, I create the following pictured trees (and more), where the leaf values represent the individual banknotes that make up the total amount.
The leaf values then get transformed to your output, and possible duplicates (since different trees can be created from the same bank notes) are not printed out.
The following input:
int[] availableBanknotes = { 10, 50, 100 };
int amount = 100;
SolveRecursive(amount, availableBanknotes);
prints out:
10 x 10
1 x 50 + 5 x 10
2 x 50
1 x 100
The following input:
int[] availableBanknotes = { 10, 50, 100 };
int amount = 170;
SolveRecursive(amount, availableBanknotes);
prints out:
17 x 10
1 x 50 + 12 x 10
2 x 50 + 7 x 10
1 x 100 + 7 x 10
3 x 50 + 2 x 10
1 x 100 + 1 x 50 + 2 x 10
Here is the code:
private static List<string> Solutions = new List<string>();
private static void SolveRecursive(int amount, int[] availableBanknotes)
{
List<int> denomination = new List<int>();
for (int i = 0; i < availableBanknotes.Length; i++)
{
if (amount >= availableBanknotes[i])
{
ProvidedBanknotes(amount, availableBanknotes[i], availableBanknotes, new List<int>(denomination));
}
}
Solutions = new List<string>();
}
private static void ProvidedBanknotes(int amount, int currentBankNote, int[] availableBanknotes, List<int> denomination)
{
amount = amount - currentBankNote;
denomination.Add(currentBankNote);
if (amount == 0)
{
PrintSolution(denomination, availableBanknotes);
return;
}
for (int i = 0; i < availableBanknotes.Length; i++)
{
if(amount >= availableBanknotes[i])
{
ProvidedBanknotes(amount, availableBanknotes[i], availableBanknotes, new List<int>(denomination));
}
}
}
private static void PrintSolution(List<int> denomination, int[] availableBanknotes)
{
string solution = "";
for(int i = availableBanknotes.Length - 1; i >= 0; i--)
{
int currentVal = availableBanknotes[i];
int count = denomination.Where(temp => temp.Equals(currentVal))
.Select(temp => temp)
.Count();
if (count != 0)
{
if(solution.Equals(""))
{
solution = $"{count} x {currentVal}";
}
else
{
solution = solution + $" + {count} x {currentVal}";
}
}
}
if(!Solutions.Contains(solution))
{
Solutions.Add(solution);
Console.WriteLine(solution);
}
}
Given
var notes = new[] { 10, 50, 100 };
var total = 100;
You can use a LINQ Extension Method to create all possible combinations of notes and filter them to just combinations that could be used to create the total. I sort the notes in the combination descending so that the final answer will be output in descending order:
var possibleCombos = notes.Where(n => n <= total)
.AllCombinations()
.Select(c => c.OrderByDescending(n => n).ToList())
.Where(c => c.Sum() <= total && c.Aggregate(total-c.Sum(), (r, n) => r % n) == 0)
.OrderBy(c => c.Last());
While it doesn't apply to the example, the Aggregate expression ensures that impossible combinations aren't selected, such as trying to make { 50, 20 } yield 100.
For each possible combination, you can start with the total and then take away one of each note to ensure at least one of each note is in the answer. It then computes the number of additional notes needed for each denomination, starting with the largest. For each combination it outputs the number of each note in the format requested:
foreach (var combo in possibleCombos) {
var numberOfNotes = Enumerable.Repeat(1, combo.Count).ToList(); // one of each denomination
var remainder = total - combo.Sum(); // start by taking away one of each denomination
for (int j1 = 0; j1 < combo.Count; ++j1) {
numberOfNotes[j1] += remainder / combo[j1]; // take away all possible of each denomination
remainder = remainder % combo[j1];
}
Console.WriteLine(String.Join(" + ", numberOfNotes.Select((numberOfNote, j1) => $"{numberOfNote} x {combo[j1]}")));
}
The extension method is defined as:
public static class IEnumerableExt {
public static IEnumerable<IEnumerable<T>> AllCombinations<T>(this IEnumerable<T> start) {
IEnumerable<IEnumerable<T>> HelperCombinations(IEnumerable<T> items) {
if (items.IsEmpty())
yield return items;
else {
var head = items.First();
var tail = items.Skip(1);
foreach (var sequence in HelperCombinations(tail)) {
yield return sequence; // Without first
yield return sequence.Prepend(head);
}
}
}
return HelperCombinations(start).Skip(1); // don't return the empty set
}
}
NOTE: This only produces one possible solution for each combination of bills, the one that uses the maximum possible number of the largest denomination.
Here's my take on this:
public IEnumerable<List<int>> GetDenominations(List<int> denominations, int value) =>
GetDenominations(new List<int>(), denominations, value);
private IEnumerable<List<int>> GetDenominations(List<int> bills, List<int> denominations, int value)
{
int sum = bills.Sum();
if (sum == value)
{
yield return bills;
}
else if (sum < value && denominations.Any())
{
for (int i = 0; i < denominations.Count; i++)
{
List<int> denominations2 = denominations.Skip(i).ToList();
List<int> bills2 = bills.Append(denominations2.First()).ToList();
foreach (var result in GetDenominations(bills2, denominations2, value))
{
yield return result;
}
}
}
}
That produces all valid combinations of the denominations that add up the value passed.
I get the results like this:
IEnumerable<List<int>> results = GetDenominations(new List<int>() { 10, 50, 100 }, 170);
And formatting is just a little LINQ work:
string output =
String
.Join(
Environment.NewLine,
results
.Reverse()
.Select(xs =>
String
.Join(
" + ",
xs
.OrderByDescending(x => x)
.GroupBy(x => x)
.Select(x => $"{x.Count()} x {x.Key}"))));
That gives me:
1 x 100 + 1 x 50 + 2 x 10
3 x 50 + 2 x 10
1 x 100 + 7 x 10
2 x 50 + 7 x 10
1 x 50 + 12 x 10
17 x 10

How to perform operation on grouped records?

This is my records:
Id EmpName Stats
1 Abc 1000
1 Abc 3000
1 Abc 2000
2 Pqr 4000
2 Pqr 5000
2 Pqr 6000
2 Pqr 7000
I am trying to group by on Id fields and after doing group by i want output like this:
Expected output:
Id EmpName Stats
1 Abc 3000
2 Pqr 3000
For 1st output record calculation is like this:
3000 - 1000=2000 (i.e subtract highest - lowest from 1st and 2nd records)
3000 - 2000=1000 (i.e subtract highest - lowest from 2nd and 3rd records)
Total=2000 + 1000 =3000
For 2nd output record calculation is like this:
5000 - 4000=1000 (i.e subtract highest - lowest from first two records)
6000 - 5000=1000
7000 - 6000=1000
total=1000 + 1000=2000
This is 1 sample fiddle i have created:Fiddle
So far i have manage to group records by id but now how do i perform this calculation on group records??
You can use the Aggregate method overload that allows you to maintain custom accumulator state.
In your case, we'll be maintaining the following:
decimal Sum; // Current result
decimal Prev; // Previous element Stats (zero for the first element)
int Index; // The index of the current element
The Index is basically needed just to avoid accumulating the first element Stats into the result.
And here is the query:
var result = list.GroupBy(t => t.Id)
.Select(g => new
{
ID = g.Key,
Name = g.First().EmpName,
Stats = g.Aggregate(
new { Sum = 0m, Prev = 0m, Index = 0 },
(a, e) => new
{
Sum = (a.Index < 2 ? 0 : a.Sum) + Math.Abs(e.Stats - a.Prev),
Prev = e.Stats,
Index = a.Index + 1
}, a => a.Sum)
}).ToList();
Edit: As requested in the comments, here is the foreach equivalent of the above Aggregate usage:
static decimal GetStats(IEnumerable<Employee> g)
{
decimal sum = 0;
decimal prev = 0;
int index = 0;
foreach (var e in g)
{
sum = (index < 2 ? 0 : sum) + Math.Abs(e.Stats - prev);
prev = e.Stats;
index++;
}
return sum;
}
Firstly, like mentioned in my comment, this can be done using a single linq query but would have many complications, one being unreadable code.
Using a simple foreach on the IGrouping List,
Updated (handle dynamic group length):
var list = CreateData();
var groupList = list.GroupBy(t => t.Id);
var finalList = new List<Employee>();
//Iterate on the groups
foreach(var grp in groupList){
var part1 = grp.Count()/2;
var part2 = (int)Math.Ceiling((double)grp.Count()/2);
var firstSet = grp.Select(i=>i.Stats).Take(part2);
var secondSet = grp.Select(i=>i.Stats).Skip(part1).Take(part2);
var total = (firstSet.Max() - firstSet.Min()) + (secondSet.Max() - secondSet.Min());
finalList.Add(new Employee{
Id = grp.Key,
EmpName = grp.FirstOrDefault().EmpName,
Stats = total
});
}
*Note -
You can optimize the logic used in getting the data for calculation.
More complicated logic is to divide the group into equal parts in case it is not fixed.
Updated Fiddle
The LinQ way,
var list = CreateData();
var groupList = list.GroupBy(t => t.Id);
var testLinq = (from l in list
group l by l.Id into grp
let part1 = grp.Count()/2
let part2 = (int)Math.Ceiling((double)grp.Count()/2)
let firstSet = grp.Select(i=>i.Stats).Take(part2)
let secondSet = grp.Select(i=>i.Stats).Skip(part1).Take(part2)
select new Employee{
Id = grp.Key,
EmpName = grp.FirstOrDefault().EmpName,
Stats = (firstSet.Max() - firstSet.Min()) + (secondSet.Max() - secondSet.Min())
}).ToList();

Split a List into several Lists based on criteria using LINQ

I have a list of integers, which I would like to split into 2 or more lists based upon meeting a certain criteria. For example:
List<int> myList = new List<int>();
myList.Add(100);
myList.Add(200);
myList.Add(300);
myList.Add(400);
myList.Add(200);
myList.Add(500);
I would like to split the list into several lists, each of which contains all items which total <= 600. In the above, it would then result in 3 separate List objects.
List 1 would contain 100, 200 300
List 2 would contain 400, 200
List 3 would contain 500
Ideally, I'd like it to be a single LINQ statement.
Although doable, this is an excellent example of what LINQ is not for. Check yourself.
Having
var myList = new List<int> { 100, 200, 300, 400, 200, 500, };
int maxSum = 600;
"Pure" LINQ (the power of Aggregate)
var result = myList.Aggregate(
new { Sum = 0, List = new List<List<int>>() },
(data, value) =>
{
int sum = data.Sum + value;
if (data.List.Count > 0 && sum <= maxSum)
data.List[data.List.Count - 1].Add(value);
else
data.List.Add(new List<int> { (sum = value) });
return new { Sum = sum, List = data.List };
},
data => data.List)
.ToList();
A normal (non LINQ) implementation of the above
var result = new List<List<int>>();
int sum = 0;
foreach (var value in myList)
{
if (result.Count > 0 && (sum += value) <= maxSum)
result[result.Count - 1].Add(value);
else
result.Add(new List<int> { (sum = value) });
}
For completeness (and some fun), a "Hackish" LINQ (the power of closures and C# operators)
int sum = 0, key = -1;
var result = myList.GroupBy(x => key >= 0 && (sum += x) <= maxSum ? key : ++key + (sum = x) * 0, (k, e) => e.ToList()).ToList();
Here is the solution to your problem. I am not sure if that is the best case solver but it will surely do the job:
List<int> First = myList.Where(x => x <= 300).ToList();
List<int> Second = myList.Where(x => x == 400 || x == 200).ToList();
List<int> Third = myList.Where(x => x == 500).ToList();
It does query through the list and checks for values that meets the requirements then it will convert IEnumerable into the List.
This will do what you want for a list any size but maybe not as short as you are looking for. You would have to write a LINQ extension method to shorten it but then it become a bit more complicated.
List<int> myList = new List<int>();
myList.Add(100);
myList.Add(200);
myList.Add(300);
myList.Add(400);
myList.Add(200);
myList.Add(500);
var result = new List<List<int>>();
var skip = 0;
while (skip < myList.Count)
{
var sum = 0;
result.Add(myList.Skip(skip).TakeWhile(x =>
{
sum += x;
return sum <= 600;
}).ToList());
skip += result.Last().Count();
}

how to arrange the item of a list into series arrangement

I have a list of data which contains of random data with combination of string and number:
List<String> Data1 = new List<String>()
{
"1001A",
"1002A",
"1003A",
"1004A",
"1015A",
"1016A",
"1007A",
"1008A",
"1009A",
};
I want this data to arrange into series like this:
1001A - 1004A, 1007A - 1009A, 1015A, 1016A
for every more than 2 counts of data series the output shall be have "-" between the first count and the last count of series, the other non series data will be just added to the last part and all together will separated by ",".
I'd already made some codes only to arrange the data series by the last char of it:
string get_REVISIONMARK = "A";
var raw_serries = arrange_REVISIONSERIES.Where(p => p[p.Length - 1].ToString() == get_REVISIONMARK) .OrderBy(p => p[p.Length - 1) .ThenBy(p => p.Substring(0, p.Length - 1)).ToList();
just ignore the last char I'd already have function for that, and my problem only about the arrangement of the numbers, the length of data is not fixed. for other example of output "1001A - 1005A, 301A, 32A"
I had another sample of my codes this works fine to me, but for me its so lazy code.
for (int c1 = 0; c1 < list_num.Count; c1++)
{
if (list_num[c1] != 0)
{
check1 = list_num[c1];
for (int c2 = 0; c2 < list_num.Count; c2++)
{
if (check1 == list_num[c2])
{
list_num[c2] = 0;
check1 += 1;
list_series.Add(arrange_REVISIONSERIES[c2]);
}
}
check1 = 0;
if (list_series.Count > 2)
{
res_series.Add(list_series[0] + " to " +list_series[list_series.Count - 1]);
list_series.Clear();
}
else
{
if (list_series.Count == 1)
{
res_series.Add(list_series[0]);
list_series.Clear();
}
else
{
res_series.Add(list_series[0] + "," + list_series[1]);
list_series.Clear();
}
}
}
}
var combine_res = String.Join(",", res_series);
MessageBox.Show(combine_res);
this codes work fine for the series number ...
A possible solution (working with current set of values), Please follow the steps below
Declare a class level string list as
public List<String> data_result = new List<string>();
Create a function to iterate through input string list (input string declared inside, named 'data')
public void ArrangeList()
{
List<String> data = new List<string>() { "1001A", "1002A", "1003A",
"1004A", "1015A", "1016A", "1007A", "1008A", "1009A", "1017A" };
List<int> data_int = data.Select(a => Convert.ToInt32(a.Substring(0,
a.Length - 1))).OrderBy(b => b).ToList();
int initializer = 0, counter = 0;
int finalizer = 0;
foreach (var item in data_int)
{
if (initializer == 0)
{ initializer = item; continue; }
else
{
counter++;
if (item == initializer + counter)
finalizer = item;
else
{
LogListing(initializer, finalizer);
initializer = item;
finalizer = item;
counter = 0;
}
}
}
LogListing(initializer, finalizer);
}
Create a function which just logs the result into data_result string list.
public void LogListing(int initializer, int finalizer)
{
if (initializer != finalizer)
{
if (finalizer == initializer + 1)
{
data_result.Add(initializer + "A");
data_result.Add(finalizer + "A");
}
else
data_result.Add(initializer + "A - " + finalizer + "A");
}
else
data_result.Add(initializer + "A");
}
It perfectly generates the result list as
Thumb-up if you like
A linqy solution:
char get_REVISIONMARK = 'A';
var res = arrange_REVISIONSERIES.Select(s => new { Rev = s[s.Length - 1], Value = int.Parse(s.Substring(0, s.Length - 1)), Org = s })
.Where(d => d.Rev == get_REVISIONMARK).OrderBy(d => d.Value)
.Select((val, ind) => new { Index = ind, Org = val.Org, Value = val.Value }).GroupBy(a => a.Value - a.Index)
.Select(gr=>gr.ToList()).OrderBy(l=>l.Count > 2 ? 0 : 1 ).Aggregate(new List<string>(), (list, sublist) =>
{
if (sublist.Count > 2)
list.Add(sublist[0].Org + " - " + sublist[sublist.Count - 1].Org);
else
list.AddRange(sublist.Select(a => a.Org));
return list;
});
The first lines are basically the same as the code you already have (filter on revision and sort), but with the difference that the subvalues are stored in an anonymous type. You could do the same on the pre ordered list, but since splitting the string would be done twice I've included it in the total.
Then a select with index (.Select((val, ind) =>) is made to get value/index pairs. This is done to be able to get the sequences based on an old t-sql row_number trick: for each 'group' the difference between value and index is the same .GroupBy(a => a.Value - a.Index)
After that, normally you'd be as good as done, but since you only want to make sequences of 2 and longer, we make sublists out of the groupby values and do the ordering beforehand to make sure the ranges come for the eventual single elements .Select(gr=>gr.ToList()).OrderBy(l=>l.Count > 2 ? 0 : 1 )
Finally, the list is created of the groups. Several options, but I like to use Aggregate for that. The seed is the resulting list, and the aggregate simply adds to that (where subranges > 2 are cummulated and for single elements and pairs, the single elements are added)
I'm making two assumptions:
The list is already ordered
The non-numeric characters can be ignored
You will get the results in the results variable:
void Main()
{
List<String> Data1 = new List<String>()
{
"1001A",
"1002A",
"1003A",
"1004A",
"1015A",
"1016A",
"1007A",
"1008A",
"1009A",
};
var accu = new List<List<Tuple<int, string>>>();
foreach (var data in Data1)
{
if (accu.Any(t => t.Any(d => d.Item1 == (ToInt(data) - 1))))
{
var item = accu.First(t => t.Any(d => d.Item1 == (ToInt(data) - 1)));
item.Add(new Tuple<int, string>(ToInt(data), data));
}
else
{
accu.Add(new List<Tuple<int, string>>{ new Tuple <int, string>(ToInt(data), data)});
}
}
var results = new List<string>();
results.AddRange(accu.Where(g => g.Count > 2).Select(g => string.Format("{0} - {1}", g.First().Item2, g.Last().Item2)));
results.AddRange(accu.Where(g => g.Count <= 2).Aggregate(new List<string>(), (total, current) => { total.AddRange(current.Select(i => i.Item2)); return total; } ));
}
private static Regex digitsOnly = new Regex(#"[^\d]");
public static int ToInt(string literal)
{
int i;
int.TryParse(digitsOnly.Replace(literal, ""), out i);
return i;
}
So given your starting data:
List<String> arrange_REVISIONSERIES = new List<String>()
{
"1001A",
"1002A",
"1003A",
"1004A",
"1015A",
"1016A",
"1007A",
"1008A",
"1009A",
};
I do this first:
var splits =
arrange_REVISIONSERIES
.Select(datum => new
{
value = int.Parse(datum.Substring(0, datum.Length - 1)),
suffix = datum.Substring(datum.Length - 1, 1),
})
.OrderBy(split => split.suffix)
.ThenBy(split => split.value)
.ToArray();
That's basically the same as your raw_serries, but orders the number part as a number. It seems to me that you need it as a number to make the range part work.
I then do this to compute the groupings:
var results =
splits
.Skip(1)
.Aggregate(
new[]
{
new
{
start = splits[0].value,
end = splits[0].value,
suffix = splits[0].suffix
}
}.ToList(),
(a, s) =>
{
if (a.Last().suffix == s.suffix && a.Last().end + 1 == s.value)
{
a[a.Count - 1] = new
{
start = a.Last().start,
end = s.value,
suffix = s.suffix
};
}
else
{
a.Add(new
{
start = s.value,
end = s.value,
suffix = s.suffix
});
}
return a;
})
.Select(r => r.start == r.end
? String.Format("{0}{1}", r.end, r.suffix)
: (r.start + 1 == r.end
? String.Format("{0}{2}, {1}{2}", r.start, r.end, r.suffix)
: String.Format("{0}{2} - {1}{2}", r.start, r.end, r.suffix)))
.ToArray();
And finally, this to create a single string:
var result = String.Join(", ", results);
That gives me:
1001A - 1004A, 1007A - 1009A, 1015A, 1016A
This code nicely works with data containing different suffixes.

C# algorithm refactor splitting an array into 3 parts?

I have an IEnumerable and I wanted to split the data across 3 columns using the following business logic. if 3 or less items, 1 item per column, anything else I wanted to divide the total items by 3 split the leftovers (either 1 or 2 items) between the first two columns. Now this is pretty ugly but it does the job. I'm looking for tips to leverage linq a little better or possibly eliminate the switch statement. Any advice or tips that improve the code are appreciated.
var numItems = items.Count;
IEnumerable<JToken> col1Items,
col2Items,
col3Items;
if(numItems <=3)
{
col1Items = items.Take(1);
col2Items = items.Skip(1).Take(1);
col3Items = items.Skip(2).Take(1);
} else {
int remainder = numItems % 3,
take = numItems / 3,
col1Take,
col2Take,
col3Take;
switch(remainder)
{
case 1:
col1Take = take + 1;
col2Take = take;
col3Take = take;
break;
case 2:
col1Take = take + 1;
col2Take = take + 1;
col3Take = take;
break;
default:
col1Take = take;
col2Take = take;
col3Take = take;
break;
}
col1Items = items.Take(col1Take);
col2Items = items.Skip(col1Take).Take(col2Take);
col3Items = items.Skip(col1Take + col2Take).Take(col3Take);
Ultimately I am using these in a mvc Razor view
<div class="widgetColumn">
#Html.DisplayFor(m => col1Items, "MenuColumn")
</div>
<div class="widgetColumn">
#Html.DisplayFor(m => col2Items, "MenuColumn")
</div>
<div class="widgetColumn">
#Html.DisplayFor(m => col3Items, "MenuColumn")
</div>
In my first attempt I want to get rid of the colNItems and colNTake variables but i can't figure out the correct algorithm to make it work the same.
for (int i = 1; i <= 3; i++ )
{
IEnumerable<JToken> widgets = new List<JToken>();
var col = i;
switch(col)
{
case 1:
break;
case 2:
break;
case 3:
break;
}
}
Are the columns fixed-width? If so, then there's no need to do anything special with your collection. Just rely on the browser to do it for you. Have an outer container that has the overall width of the 3 columns, then just fill it with a div for each item (and float left). Set your inner containers to have a width exactly 1/3 of the outer container.
Here's a quick fiddle
Here's a quick hint at the style
div#outer{
width:300px;
}
div#outer > div{
width:100px;
float:left;
}
You could generalize:
int cols = 3;
IEnumerable<JToken> colItems[3]; // you can make this dynamic of course
int rem = numItems % cols;
int len = numItems / cols;
for (int col=0; col<cols; col++){
int colTake = len;
if (col < rem) colTake++;
colItems[col] = items.Skip(col*len).Take(colTake);
}
Haven't tested, but this should work for any number of columns.
Also whenever you need variables col1, col2, col3 think of col[0], col[1], col[2].
Can't you just do something like?
int len = numItems / 3;
int rem = numItems % 3;
int col1Take = len + (rem > 0 ? 1 : 0);
int col2Take = len + (rem > 1 ? 1 : 0);
int col3Take = len;
Edit:
A more generic solution that works for any number of columns (COLUMNS) would be:
int len = numItems / COLUMNS;
int rem = numItems % COLUMNS;
foreach (var i in Enumerable.Range(0, COLUMNS)) {
colTake[i] = len + (rem > i ? 1 : 0);
}
So you want the first n/3 items in the first column, next n/3 items in the 2nd column, etc.
var concreteList = items.ToList();
var count = concreteList.Count;
var take1 = count/3 + (count % 3 > 0 ? 1 : 0);
var take2 = count/3 + (count % 3 > 1 ? 1 : 0);
var col1 = concreteList.Take(take1);
var col2 = concreteList.Skip(take1).Take(take2);
var col3 = concreteList.Skip(take1 + take2);
I make a concrete list in order to avoid iterating the Enumerable multiple times. For example, if you had:
items = File.ReadLines("foo.txt");
Then you wouldn't be able to iterate it multiple times.
If you want to fill the columns round-robin you can use:
int numColumns = 3;
var result = Enumerable.Range(1,numColumns).Select(c =>
items.Where((x,ix) => ix % numColumns == c-1).ToArray()
);
This isn't fast, but it'll do the trick:
var col1Items = items.Select((obj, index) => new { Value = obj, Index = index })
.Where(o => o.Index % 3 == 0).Select(o => o.Value);
var col2Items = items.Select((obj, index) => new { Value = obj, Index = index })
.Where(o => o.Index % 3 == 1).Select(o => o.Value);
var col3Items = items.Select((obj, index) => new { Value = obj, Index = index })
.Where(o => o.Index % 3 == 2).Select(o => o.Value);
It uses the version of Select that includes an index parameter. You could use a GroupBy to speed this up a bit at the cost of a few lines of code.
If you want to go across then down see answer below this one, if you want to go down then across you can do it like this (use this code with the test below to see it working instead of the var result line there.:
var curCol = 0;
var iPer = items.Count() / 3;
var iLeft = items.Count() % 3;
var result = items.Aggregate(
// object that will hold items
new {
cols = new List<ItemElement>[3] { new List<ItemElement>(),
new List<ItemElement>(),
new List<ItemElement>(), },
},
(o, n) => {
o.cols[curCol].Add(n);
if (o.cols[curCol].Count() > iPer + (iLeft > (curCol+1) ? 1:0))
curCol++;
return new {
cols = o.cols
};
});
You can do this with aggregate. It would look like this:
void Main()
{
List<ItemElement> items = new List<ItemElement>() {
new ItemElement() { aField = 1 },
new ItemElement() { aField = 2 },
new ItemElement() { aField = 3 },
new ItemElement() { aField = 4 },
new ItemElement() { aField = 5 },
new ItemElement() { aField = 6 },
new ItemElement() { aField = 7 },
new ItemElement() { aField = 8 },
new ItemElement() { aField = 9 }
};
var result =
items.Aggregate(
// object that will hold items
new {
cols = new List<ItemElement>[3] { new List<ItemElement>(),
new List<ItemElement>(),
new List<ItemElement>(), },
next = 0 },
// aggregate
(o, n) => {
o.cols[o.next].Add(n);
return new {
cols = o.cols,
next = (o.next + 1) % 3
};
});
result.Dump();
}
public class ItemElement
{
public int aField { get; set; }
}
You end up with an object with an array of 3 lists (one for each column).
This example will run as is in linqPad. I recomment linqPad for these kind of POC tests. (linqPad.com)
it might help
IEnumerable<object> items = new Object[]{ "1", "2", "3", "4", "5", "6", "7","8", "9", "10", "11", "12","13", "14" };
IEnumerable<object> col1Items = new List<object>(),
col2Items = new List<object>(),
col3Items = new List<object>();
Object[] list = new Object[]{col1Items, col2Items, col3Items};
int limit = items.Count()/3;
int len = items.Count();
int col;
for (int i = 0; i < items.Count(); i++ )
{
if (len == 3) col = i;
else col = i / limit;
if (col >= 3) col = i%limit ;
((IList<object>)(list[col])).Add( items.ElementAt(i));
}
LinqLib (nuget: LinqExtLibrary) has an overload of ToArray() that does it:
using System.Collections.Generic;
using System.Linq;
using LinqLib.Array;
...
public void TakeEm(IEnumerable<int> data)
{
var dataAry = data as int[] ?? data.ToArray();
var rows = (dataAry.Length/3) + 1;
//var columns = Enumerable.Empty<int>().ToArray(3, rows);
// vvv These two lines are the ones that re-arrange your array
var columns = dataAry.ToArray(3, rows);
var menus = columns.Slice();
}

Categories

Resources