Ranking in Linq - linq

There's a generic list of numbers, say
{980, 850,700, 680}---n nos.
I try to compare the above list with a decimal no. say 690., the O/p I need is,to get the ranking of the number which I'm gonna input("692). i,e the desired O/P should be Ranking ="4"
How can I get the O/p for above scenario..??

Following on from Alex's post I think you are looking for
var numbers = new List<int>() { 980, 850, 700, 680 };
var dec = new Decimal(692.0);
var temp = numbers.Count(x => x > dec) + 1;
this will return the position you are looking for

If you want to look for an exact match of a decimal input to a int on the list,you can use FindIndex.
var numbers = new List<int>() { 980, 850, 700, 680 };
var dec = new Decimal(680.0);
var res = numbers.FindIndex(x => x == dec);
It returns the 0-based position of the match.

Your question is not clear, i'm not sure what role 690 is playing.
Assuming that the user can ernter a number and you want to find the rank(index) of the number in the list when it would be inserted. Assuming also that your list should be sorted descending since you want the position of the new int according to it's value:
var input = 692;
var numbers = new List<int>() { 980, 850, 700, 680 };
var firstLower = numbers.OrderByDescending(i => i)
.Select((i, index) => new { Value = i, Index = index })
.FirstOrDefault(x => x.Value < input);
var rank = firstLower == null ? numbers.Count + 1 : firstLower.Index + 1;
Note that the OrderByDescending might be redundant if your list is already sorted, but i assume that your sample data is only sorted accidentally.

Related

How to insert two rows for every three rows of number in google sheet using apps script

picture of add rows example
As the picture shows, there are 11 rows of numbers in Column A,
I want to add rows to it and make it shows as the numbers in column D
You see for every 3 rows of number in A, I want to add a new row to its up and bottom, the new row in the top with value "0", and the bottom row with the sum value of these three rows of numbers
for example
the first three rows are 1,2,3, and their sum is "1+2+3=6"
the second three rows are 4,5,6, and the sum is "4+5+6=15"
the third three rows are 7,8,9, and the sum is "7+8+9=24" as picture shows
and you see the last part of the column are "10 and 12", which are two rows
(less than three rows), but in this case, it still need to add two new rows, one row in top with"0" and one row in bottom with its sum "10+12=22"
Right now, I wrote two for loop function to insert the rows
function insertRows() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName("sheet1");
var numRows = SpreadsheetApp.getActiveSpreadsheet().getSheetByName ("sheet1").getLastRow();
for (var i=1; i <= numRows+2; i+=4) {
var addrow = sheet.insertRowsBefore(i,1);
}
}
function insertRows2() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName("sheet1");
var numRows = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("sheet1").getLastRow();
for (var j=4; j <= numRows+2; j+=5) {
var rowadd= sheet.insertRowsAfter(j,1)
}
}
"insertRows()" is used to insert the head row, "insertRows2()" is used to insert the bottom row,
so far these two functions are working good to insert the rows in right place, but I don't know how to fill in the top rows with "0"s in the loop and how to fill the sum value of each three rows of numbers in the bottom rows.
Do you guys know how to fill in the rows with the right values?
How about this sample script? I think that there are several answers for your situation. So please think of this as one of them.
Flow :
Retrieve values from "A1:A11" of sheet1.
Using the retrieved values, it creates an array for the result.
At first, it separates the values, while inserts 0 to the top and the sum of the end.
As an important point, the array is created to put the spreadsheet as 2 dimensional array.
Put the result values to "D1:19" of sheet1.
Sample script :
function insertRows() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName("sheet1");
var value = sheet.getRange(1, 1, sheet.getLastRow(), 1).getValues().filter(Number);
var result = [];
var temp = [[0]];
var sum = 0;
for (var i = 0; i < value.length; i++) {
temp.push(value[i]);
sum += value[i][0];
if ((i + 1) % 3 == 0 || i == value.length - 1) {
temp.push([sum]);
result = result.concat(temp);
temp = [[0]];
sum = 0;
}
}
sheet.getRange(1, 4, result.length, 1).setValues(result);
}
Note :
When run this sample script, the lastrow of the sheet is changed. So I used filter() to var value = sheet.getRange(1, 1, sheet.getLastRow(), 1).getValues().
If this was not what you want, please tell me. I would like to modify my answer.

tick format produces overlapping labels

I am currently using:
var formatValue = d3.format("s");
and then
var axisX = d3.svg.axis()
.scale( someXscale )
.tickFormat (function( d ) { return formatValue(d); } );
This code produces the following when I zoom (from the highest zoom to the lowest):
The values on this axis can go up to 3,100,000,000.
I don't like the fact that the values fall on top of each other and less important I would like the labels to have Giga, Mega, Kilo.
I would appreciate any suggestions.
A good way can be drawn from this comment by M. Bostock:
var formatAbbreviation = d3.format(".2s");
formatAbbreviation(5000000000); // 5.00G
formatAbbreviation(5000000); // 5.00M
formatAbbreviation(5000); // 5.00k
In the post you see him actually customising the behaviour, changing G to B.
I actually created my own custom implementation in the past, maybe it can be useful.
Here's an example I've made:
var format = function(num) {
var numToStr = num + '';
var ext = ['', 'K', 'M', 'G']; // Add extensions as needed
var size = 3;
var val = 0;
var max = 5; // how many digit maximum we want on screen
var compress = function(str) {
var len = str.length;
if (len <= size) {
return str + ext[val];
}
if (val + 1 === ext.length) {
return str.slice(0, max) + ext[val]; // <= what to do when the max number of digits is reached, change as needed
}
val++;
return compress(str.slice(0, str.length - size));
}
return compress(numToStr);
}
console.log(format(1)) // => 1
console.log(format(12)) // => 12
console.log(format(123)) // => 123
console.log(format(1234)) // => 1K
console.log(format(12345)) // => 12K
console.log(format(123456)) // => 123K
console.log(format(1234567)) // => 1M
console.log(format(12345678)) // => 12M
console.log(format(123456789)) // => 123M
console.log(format(1234567890)) // => 1G
console.log(format(12345678901)) // => 12G
console.log(format(12345678902321312)) // => 12345G
// used more or less like so in d3
var axisX = d3.svg.axis()
.scale( someXscale )
.tickFormat (function( d ) { return format(d); } );
As you notice we can define the maximum number of digits you want on screen - in this case 5 - and handle that case as we think it's best for the particular situation (every case is different).

Default values for empty groups in Linq GroupBy query

I have a data set of values that I want to summarise in groups. For each group, I want to create an array big enough to contain the values of the largest group. When a group contains less than this maximum number, I want to insert a default value of zero for the empty key values.
Dataset
Col1 Col2 Value
--------------------
A X 10
A Z 15
B X 9
B Y 12
B Z 6
Desired result
X, [10, 9]
Y, [0, 12]
Z, [15, 6]
Note that value "A" in Col1 in the dataset has no value for "Y" in Col2. Value "A" is first group in the outer series, therefore it is the first element that is missing.
The following query creates the result dataset, but does not insert the default zero values for the Y group.
result = data.GroupBy(item => item.Col2)
.Select(group => new
{
name = group.Key,
data = group.Select(item => item.Value)
.ToArray()
})
Actual result
X, [10, 9]
Y, [12]
Z, [15, 6]
What do I need to do to insert a zero as the missing group value?
Here is how I understand it.
Let say we have this
class Data
{
public string Col1, Col2;
public decimal Value;
}
Data[] source =
{
new Data { Col1="A", Col2 = "X", Value = 10 },
new Data { Col1="A", Col2 = "Z", Value = 15 },
new Data { Col1="B", Col2 = "X", Value = 9 },
new Data { Col1="B", Col2 = "Y", Value = 12 },
new Data { Col1="B", Col2 = "Z", Value = 6 },
};
First we need to determine the "fixed" part
var columns = source.Select(e => e.Col1).Distinct().OrderBy(c => c).ToList();
Then we can process with the normal grouping, but inside the group we will left join the columns with group elements which will allow us to achieve the desired behavior
var result = source.GroupBy(e => e.Col2, (key, elements) => new
{
Key = key,
Elements = (from c in columns
join e in elements on c equals e.Col1 into g
from e in g.DefaultIfEmpty()
select e != null ? e.Value : 0).ToList()
})
.OrderBy(e => e.Key)
.ToList();
It won't be pretty, but you can do something like this:
var groups = data.GroupBy(d => d.Col2, d => d.Value)
.Select(g => new { g, count = g.Count() })
.ToList();
int maxG = groups.Max(p => p.count);
var paddedGroups = groups.Select(p => new {
name = p.g.Key,
data = p.g.Concat(Enumerable.Repeat(0, maxG - p.count)).ToArray() });
You can do it like this:-
int maxCount = 0;
var result = data.GroupBy(x => x.Col2)
.OrderByDescending(x => x.Count())
.Select(x =>
{
if (maxCount == 0)
maxCount = x.Count();
var Value = x.Select(z => z.Value);
return new
{
name = x.Key,
data = maxCount == x.Count() ? Value.ToArray() :
Value.Concat(new int[maxCount - Value.Count()]).ToArray()
};
});
Code Explanation:-
Since you need to append default zeros in case when you have less items in any group, I am storing the maxCount (which any group can produce in a variable maxCount) for this I am ordering the items in descending order. Next I am storing the maximum count which the item can producr in maxCount variable. While projecting I am simply checking if number of items in the group is not equal to maxCount then create an integer array of size (maxCount - x.Count) i.e. maximum count minus number of items in current group and appending it to the array.
Working Fiddle.

Linq - return index of collection using conditional logic

I have a collection
List<int> periods = new List<int>();
periods.Add(0);
periods.Add(30);
periods.Add(60);
periods.Add(90);
periods.Add(120);
periods.Add(180);
var overDueDays = 31;
I have a variable over due days. When the vale is between 0 to 29 then I want to return the index of 0. When between 30 - 59 I want to return index 1. The periods list is from db so its not hard coded and values can be different from what are here. What is the best way to to it using LINQ in one statement.
It's not really what Linq is designed for, but (assuming that the range is not fixed) you could do the following to get the index
List<int> periods = new List<int>();
periods.Add(0);
periods.Add(30);
periods.Add(60);
periods.Add(90);
periods.Add(120);
periods.Add(180);
var overDueDays = 31;
var result = periods.IndexOf(periods.First(n => overDueDays < n)) - 1;
You can use .TakeWhile():
int periodIndex = periods.TakeWhile(p => p <= overDueDays).Count() - 1;
how about this ?
var qPeriods = periods.Where(v => v <= overDueDays)
.Select((result, i) => new { index = i })
.Last();
Assuming that periods is sorted, you can use the following approach:
var result = periods.Skip(1)
.Select((o, i) => new { Index = i, Value = o })
.FirstOrDefault(o => overDueDays < o.Value);
if (result != null)
{
Console.WriteLine(result.Index);
}
else
{
Console.WriteLine("Matching range not found!");
}
The first value is skipped since we're interested in comparing with the upper value of the range. By skipping it, the indices fall into place without the need to subtract 1. FirstOrDefault is used in case overDueDays doesn't fall between any of the available ranges.

LINQ Grouping: Is there a cleaner way to do this without a for loop

I am trying to create a very simple distribution chart and I want to display the counts of tests score percentages in their corresponding 10's ranges.
I thought about just doing the grouping on the Math.Round((d.Percentage/10-0.5),0)*10 which should give me the 10's value....but I wasn't sure the best way to do this given that I would probably have missing ranges and all ranges need to appear even if the count is zero. I also thought about doing an outer join on the ranges array but since I'm fairly new to Linq so for the sake of time I opted for the code below. I would however like to know what a better way might be.
Also note: As I tend to work with larger teams with varying experience levels, I'm not all that crazy about ultra compact code unless it remains very readable to the average developer.
Any suggestions?
public IEnumerable<TestDistribution> GetDistribution()
{
var distribution = new List<TestDistribution>();
var ranges = new int[] { 0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110 };
var labels = new string[] { "0%'s", "10%'s", "20%'s", "30%'s", "40%'s", "50%'s", "60%'s", "70%'s", "80%'s", "90%'s", "100%'s", ">110% "};
for (var n = 0; n < ranges.Count(); n++)
{
var count = 0;
var min = ranges[n];
var max = (n == ranges.Count() - 1) ? decimal.MaxValue : ranges[n+1];
count = (from d in Results
where d.Percentage>= min
&& d.Percentage<max
select d)
.Count();
distribution.Add(new TestDistribution() { Label = labels[n], Frequency = count });
}
return distribution;
}
// ranges and labels in a list of pairs of them
var rangesWithLabels = ranges.Zip(labels, (r,l) => new {Range = r, Label = l});
// create a list of intervals (ie. 0-10, 10-20, .. 110 - max value
var rangeMinMax = ranges.Zip(ranges.Skip(1), (min, max) => new {Min = min, Max = max})
.Union(new[] {new {Min = ranges.Last(), Max = Int32.MaxValue}});
//the grouping is made by the lower bound of the interval found for some Percentage
var resultsDistribution = from c in Results
group c by
rangeMinMax.FirstOrDefault(r=> r.Min <= c.Percentage && c.Percentage < r.Max).Min into g
select new {Percentage = g.Key, Frequency = g.Count() };
// left join betweem the labels and the results with frequencies
var distributionWithLabels =
from l in rangesWithLabels
join r in resultsDistribution on l.Range equals r.Percentage
into rd
from r in rd.DefaultIfEmpty()
select new TestDistribution{
Label = l.Label,
Frequency = r != null ? r.Frequency : 0
};
distribution = distributionWithLabels.ToList();
Another solution if the ranges and labels can be created in another way
var ranges = Enumerable.Range(0, 10)
.Select(c=> new {
Min = c * 10,
Max = (c +1 )* 10,
Label = (c * 10) + "%'s"})
.Union(new[] { new {
Min = 100,
Max = Int32.MaxValue,
Label = ">110% "
}});
var resultsDistribution = from c in Results
group c by ranges.FirstOrDefault(r=> r.Min <= c.Percentage && c.Percentage < r.Max).Min
into g
select new {Percentage = g.Key, Frequency = g.Count() };
var distributionWithLabels =
from l in ranges
join r in resultsDistribution on l.Min equals r.Percentage
into rd
from r in rd.DefaultIfEmpty()
select new TestDistribution{
Label = l.Label,
Frequency = r != null ? r.Frequency : 0
};
This works
public IEnumerable<TestDistribution> GetDistribution()
{
var range = 12;
return Enumerable.Range(0, range).Select(
n => new TestDistribution
{
Label = string.Format("{1}{0}%'s", n*10, n==range-1 ? ">" : ""),
Frequency =
Results.Count(
d =>
d.Percentage >= n*10
&& d.Percentage < ((n == range - 1) ? decimal.MaxValue : (n+1)*10))
});
}

Resources