linq groupby Months and add any missing months to the grouped data - c#

I have created a linq statement which seems to be working ok. I may or maynot have written it correctly however its returning my expected results.
var grouped = RewardTransctions.GroupBy(t => new
{
t.PurchaseDate.Value.Month
}).Select(g => new TransactionDetail()
{
Month =
g.Where(w=>w.EntryType==1).Select(
(n =>
n.PurchaseDate.Value.Month))
.First(),
TransactionAmount = g.Count()
});
Now the results are returning 5 values grouped by months. Is it possible to add the 7 other missing months with a TransactionAmount = 0 to them?
The reason for my madness is I am trying to bind these values to a chart and having my x axis based on months. Currently its only showing the 5 months of records. If my data doesnt return any value for a month I some how want to add in the 0 value.
Any suggestions?

It's very simple if you use .ToLookup(...).
var lookup =
(from w in RewardTransctions
where w.EntryType == 1
select w).ToLookup(w => w.PurchaseDate.Value.Month);
var grouped =
from m in Enumerable.Range(1, 12)
select new TransactionDetail()
{
Month = m,
TransactionAmount = lookup[m].Count(),
};
How's that for a couple of simple LINQ queries?

When you're using LINQ to Objects, this query should do the trick:
var grouped =
from month in Enumerable.Range(1, 12)
select new TransactionDetail()
{
Month = month,
TransactionAmount = RewardTransactions
.Where(t => t.PurchaseDate.Value.Month == month).Count()
};
When RewardTransactions however is an IQueryable, you should first call AsEnumerable() on it.

Why not do it just like this:
var grouped =
RewardTransctions.GroupBy(t => t.PurchaseDate.Value.Month).Select(
g => new TransactionDetail { Month = g.Key, TransactionAmount = g.Count() }).ToList();
for (var i = 1; i <= 12; ++i)
{
if (grouped.Count(x => x.Month == i) == 0)
{
grouped.Add(new TransactionDetail { Month = i, TransactionAmount = 0 });
}
}
It's not entirely LINQ, but straight forward. I also simplified your LINQ query a bit ;-)

I guess If you do not use an anonymoustype(var), but create a custom type and do a .ToList() on your query that you can use .Add() on your list and bind the chart to the list.

Related

where clause not working in group by LINQ c sharp

I have a table "register_operation with fields"
[Key]
int id_registru_casa ,
DateTime data ,
int id_cont_sintetic ,
decimal suma ,
string tip
tip can take only 2 value :"receipts" and "payments"
"Groupby" work with no problem
but when I add "where" clause not working
(it doesn't show me any records)
(although there are recordings in database with day 19, month 9 and tip=receipts)
var centralizator_rc = db.register_operation
.Where(i => (i.data.Day == 19) && (i.data.Month == 9) && (tip=="receipts"))
.GroupBy(i => i.id_cont_sintetic)
.Select(g => new {
id_cont_sintetic = g.Key,
total_receipts = g.Sum(i=>i.suma),
}).ToList();
Thanks!
SOLVED!
I change code like this:
var centralizator_rc = db.registru_casa
.Where(crc=>(crc.data.Month==8) && (crc.data.Day==16) && (crc.tip=="receipts"))
.GroupBy(crc=> new
{
crc.id_cont_sintetic,
crc.data.Month,
crc.data.Day,
crc.tip
})
.Select(g => new {
data = ziuaOK,
id_cont_sintetic = g.Key.id_cont_sintetic,
total_incasare = g.Sum(i => i.suma),
}).ToList();

How to use GroupBy on an index in RavenDB?

I have this document, a post :
{Content:"blabla",Tags:["test","toto"], CreatedOn:"2019-05-01 01:02:01"}
I want to have a page that displays themost used tags since the last 30 days.
So far I tried to create an index like this
public class Toss_TagPerDay : AbstractIndexCreationTask<TossEntity, TagByDayIndex>
{
public Toss_TagPerDay()
{
Map = tosses => from toss in tosses
from tag in toss.Tags
select new TagByDayIndex()
{
Tag = tag,
CreatedOn = toss.CreatedOn.Date,
Count = 1
};
Reduce = results => from result in results
group result by new { result.Tag, result.CreatedOn }
into g
select new TagByDayIndex()
{
Tag = g.Key.Tag,
CreatedOn = g.Key.CreatedOn,
Count = g.Sum(i => i.Count)
};
}
}
And I query it like that
await _session
.Query<TagByDayIndex, Toss_TagPerDay>()
.Where(i => i.CreatedOn >= firstDay)
.GroupBy(i => i.Tag)
.OrderByDescending(g => g.Sum(i => i.Count))
.Take(50)
.Select(t => new BestTagsResult()
{
CountLastMonth = t.Count(),
Tag = t.Key
})
.ToListAsync()
But this gives me the error
Message: System.NotSupportedException : Could not understand expression: from index 'Toss/TagPerDay'.Where(i => (Convert(i.CreatedOn, DateTimeOffset) >= value(Toss.Server.Models.Tosses.BestTagsQueryHandler+<>c__DisplayClass3_0).firstDay)).GroupBy(i => i.Tag).OrderByDescending(g => g.Sum(i => i.Count)).Take(50).Select(t => new BestTagsResult() {CountLastMonth = t.Count(), Tag = t.Key})
---- System.NotSupportedException : GroupBy method is only supported in dynamic map-reduce queries
Any idea how can I make this work ? I could query for all the index data from the past 30 days and do the groupby / order / take in memory but this could make my app load a lot of data.
The results from the map-reduce index you created will give you the number of tags per day. You want to have the most popular ones from the last 30 days so you need to do the following query:
var tagCountPerDay = session
.Query<TagByDayIndex, Toss_TagPerDay>()
.Where(i => i.CreatedOn >= DateTime.Now.AddDays(-30))
.ToList();
Then you can the the client side grouping by Tag:
var mostUsedTags = tagCountPerDay.GroupBy(x => x.Tag)
.Select(t => new BestTagsResult()
{
CountLastMonth = t.Count(),
Tag = t.Key
})
.OrderByDescending(g => g.CountLastMonth)
.ToList();
#Kuepper
Based on your index definition. You can handle that by the following index:
public class TrendingSongs : AbstractIndexCreationTask<TrackPlayedEvent, TrendingSongs.Result>
{
public TrendingSongs()
{
Map = events => from e in events
where e.TypeOfTrack == TrackSubtype.song && e.Percentage >= 80 && !e.Tags.Contains(Podcast.Tags.FraKaare)
select new Result
{
TrackId = e.TrackId,
Count = 1,
Timestamp = new DateTime(e.TimestampStart.Year, e.TimestampStart.Month, e.TimestampStart.Day)
};
Reduce = results => from r in results
group r by new {r.TrackId, r.Timestamp}
into g
select new Result
{
TrackId = g.Key.TrackId,
Count = g.Sum(x => x.Count),
Timestamp = g.Key.Timestamp
};
}
}
and the query using facets:
from index TrendingSongs where Timestamp between $then and $now select facet(TrackId, sum(Count))
The reason for the error is that you can't use 'GroupBy' in a query made on an index.
'GroupBy' can be used when performing a 'dynamic query',
i.e. a query that is made on a collection, without specifying an index.
See:
https://ravendb.net/docs/article-page/4.1/Csharp/client-api/session/querying/how-to-perform-group-by-query
I solved a similar problem, by using AdditionalSources that uses dynamic values.
Then I update the index every morning to increase the Earliest Timestamp. await IndexCreation.CreateIndexesAsync(new AbstractIndexCreationTask[] {new TrendingSongs()}, _store);
I still have to try it in production, but my tests so far look like it's a lot faster than the alternatives. It does feel pretty hacky though and I'm surprised RavenDB does not offer a better solution.
public class TrendingSongs : AbstractIndexCreationTask<TrackPlayedEvent, TrendingSongs.Result>
{
public DateTime Earliest = DateTime.UtcNow.AddDays(-16);
public TrendingSongs()
{
Map = events => from e in events
where e.TypeOfTrack == TrackSubtype.song && e.Percentage >= 80 && !e.Tags.Contains(Podcast.Tags.FraKaare)
&& e.TimestampStart > new DateTime(TrendingHelpers.Year, TrendingHelpers.Month, TrendingHelpers.Day)
select new Result
{
TrackId = e.TrackId,
Count = 1
};
Reduce = results => from r in results
group r by new {r.TrackId}
into g
select new Result
{
TrackId = g.Key.TrackId,
Count = g.Sum(x => x.Count)
};
AdditionalSources = new Dictionary<string, string>
{
{
"TrendingHelpers",
#"namespace Helpers
{
public static class TrendingHelpers
{
public static int Day = "+Earliest.Day+#";
public static int Month = "+Earliest.Month+#";
public static int Year = "+Earliest.Year+#";
}
}"
}
};
}
}

How to return number of rows inserted each month from SQL using EF C#

I have a customers table and I want to return count of rows inserted each month or Customers registered each month. The following code returns only month and record count but I want to record 0 if say for Jan no customers where registered. Thank you.
The following code returns:
Month 2 Count 15
Month 5 Count 11
Month 9 Count 82
I also want to return
Month 1 count 0
Month 3 count 0
so on..
My code:
var query = (from customers in context.customers
group customers by customers.RegisterDateTime.Month into g
select new
{ Month = g.Key, Count = g.Count() }
).ToList();
foreach (var data in query)
{
Console.WriteLine( "Month "+ data.Month +" Count "+ data.Count);
}
var query = (from m in Enumerable.Range(1, 12)
join c in context.customers on m equals c.RegisteredDateTime.Month into monthGroup
select new { Month = m, Count = monthGroup.Count() }
).ToList();
I assume that you want the range from the lowest month and the highest month.
I see no possiblity (but maybe there is?) to do it inside your query directly. I would add the "0" after the query to fill the gaps between the range with zeros.
So I would add the following code line after your query:
var lowestKey = result.Min(x => x.Month);
var highestKey = result.Max(x => x.Month);
query = query.Union(
Enumerable.Range(lowestKey, highestKey - lowestKey)
.Where(e => !result.Any(r => r.Month == e))
.Select(s => new { Month = s, Count = 0 })
).OrderBy(o => o.Month).ToList();
Since I don't have your complete code, this query maybe need some adjustment.
If you need another range, than you can simple change it.
My complete example look like this:
static void Main(string[] args)
{
// Initialize the list
var result = new []
{
new { Month = 2, Count = 15 },
new { Month = 5, Count = 11 },
new { Month = 9, Count = 82 }
}.ToList();
// Generate a List with 0 in Range
var lowestKey = result.Min(x => x.Month);
var highestKey = result.Max(x => x.Month);
result = result.Union(
Enumerable.Range(lowestKey, highestKey - lowestKey)
.Where(e => !result.Any(r => r.Month == e))
.Select(s => new { Month = s, Count = 0 })
).OrderBy(o => o.Month).ToList();
foreach (var data in result)
{
Console.WriteLine("Month " + data.Month + " Count " + data.Count);
}
Console.ReadKey();
}
Hope it helps,
var query = (from customers in context.customers
group customers by customers.RegisterDateTime.Month into g
select new
{ Month = g.Key, Count = g.Count(x=>x!=null) }
).ToList();

Add missing dates to list

I have written a solution which basically adds missing date and sets the sales property for that date in my collection to 0 where it's missing like this:
int range = Convert.ToInt32(drange);
var groupedByDate = tr.Union(Enumerable.Range(1, Convert.ToInt32(range))
.Select(offset => new MyClassObject
{
Date = DateTime.Now.AddDays(-(range)).AddDays(offset),
Sales = 0
})).GroupBy(x => x.Date)
.Select(item => new MyClassObject
{
Sales = item.Sum(x => x.Sales),
Date = item.Key
})
.OrderBy(x => x.Date)
.ToList();
The first solution where the dates from DB were grouped by and they were missing looked like this:
var groupedByDate = tr
.GroupBy(x => x.TransactionDate.Date)
.Select(item => new MyClassObject
{
Sales = item.Sum(x => x.QuantityPurchased),
Date = item.Key.ToString("yyyy-MM-dd")
})
.OrderBy(x => x.Date)
.ToList();
I don't really like the way I did it in first solution, the code looks very messy and I honestly believe it can be written in a better manner..
Can someone help me out with this?
P.S. The first solution above that I've shown works just fine, but I would like to write something better which is more prettier to the eyes, and it looks quite messy (the first solution I wrote)...
How about generate the date range and then left join that with the result from your original query. And than set Sales to 0 when there is no match.
int range = 2;
var startDate = DateTime.Now;
var dates = Enumerable.Range(1, range)
.Select(offset => startDate.AddDays(-offset).Date);
var groupedByDate = from date in dates
join tmp in groupedByDate on date equals tmp.Date into g
from gr in g.DefaultIfEmpty()
select new MyClassObject
{
Sales = gr == null ? 0 : gr.Sales,
Date = date
};
Here is the easy way to do this:
var lookup = tr.ToLookup(x => x.TransactionDate.Date, x => x.QuantityPurchased);
var quantity = lookup[new DateTime(2017, 6, 29)].Sum();
If you want a range of dates then it's just this:
var startDate = new DateTime(2017, 6, 1)
var query =
from n in Enumerable.Range(0, 30)
let TransactionDate = startDate.AddDays(n)
select new
{
TransactionDate,
QuantityPurchases = lookup[TransactionDate].Sum(),
};
Simple.

Group by date range , count and sort within each group LINQ

I have a collection of dates stored in my object. This is sample data. In real time, the dates will come from a service call and I will have no idea what dates and how many will be returned:
var ListHeader = new List<ListHeaderData>
{
new ListHeaderData
{
EntryDate = new DateTime(2013, 8, 26)
},
new ListHeaderData
{
EntryDate = new DateTime(2013, 9, 11)
},
new ListHeaderData
{
EntryDate = new DateTime(2013, 1, 1)
},
new ListHeaderData
{
EntryDate = new DateTime(2013, 9, 15)
},
new ListHeaderData
{
EntryDate = new DateTime(2013, 9, 17)
},
new ListHeaderData
{
EntryDate = new DateTime(2013, 9, 5)
},
};
I now need to group by date range like so:
Today (1) <- contains the date 9/17/2013 and count of 1
within 2 weeks (3) <- contains dates 9/15,9/11,9/5 and count of 3
More than 2 weeks (2) <- contains dates 8/26, 1/1 and count of 2
this is my LINQ statement which doesn't achieve what I need but i think i'm in the ballpark (be kind if I'm not):
var defaultGroups = from l in ListHeader
group l by l.EntryDate into g
orderby g.Min(x => x.EntryDate)
select new { GroupBy = g };
This groups by individual dates, so I have 6 groups with 1 date in each. How do I group by date range , count and sort within each group?
Introduce array, which contains ranges you want to group by. Here is two ranges - today (zero days) and 14 days (two weeks):
var today = DateTime.Today;
var ranges = new List<int?> { 0, 14 };
Now group your items by range it falls into. If there is no appropriate range (all dates more than two weeks) then default null range value will be used:
var defaultGroups =
from h in ListHeader
let daysFromToday = (int)(today - h.EntryDate).TotalDays
group h by ranges.FirstOrDefault(range => daysFromToday <= range) into g
orderby g.Min(x => x.EntryDate)
select g;
UPDATE: Adding custom ranges for grouping:
var ranges = new List<int?>();
ranges.Add(0); // today
ranges.Add(7*2); // two weeks
ranges.Add(DateTime.Today.Day); // within current month
ranges.Add(DateTime.Today.DayOfYear); // within current year
ranges.Sort();
How about doing this?
Introduce a new property for grouping and group by that.
class ListHeaderData
{
public DateTime EntryDate;
public int DateDifferenceFromToday
{
get
{
TimeSpan difference = DateTime.Today - EntryDate.Date;
if (difference.TotalDays == 0)//today
{
return 1;
}
else if (difference.TotalDays <= 14)//less than 2 weeks
{
return 2;
}
else
{
return 3;//something else
}
}
}
}
Edit: as #servy pointed in comments other developers may confuse of int using a enum will be more readable.
So, modified version of your class would look something like this
class ListHeaderData
{
public DateTime EntryDate;
public DateRange DateDifferenceFromToday
{
get
{
//I think for this version no comments needed names are self explanatory
TimeSpan difference = DateTime.Today - EntryDate.Date;
if (difference.TotalDays == 0)
{
return DateRange.Today;
}
else if (difference.TotalDays <= 14)
{
return DateRange.LessThanTwoWeeks;
}
else
{
return DateRange.MoreThanTwoWeeks;
}
}
}
}
enum DateRange
{
None = 0,
Today = 1,
LessThanTwoWeeks = 2,
MoreThanTwoWeeks = 3
}
and use it like this
var defaultGroups = from l in ListHeader
group l by l.DateDifferenceFromToday into g // <--Note group by DateDifferenceFromToday
orderby g.Min(x => x.EntryDate)
select new { GroupBy = g };
Do you specifically want to achieve the solution in this way? Also do you really want to introduce spurious properties into your class to meet these requirements?
These three lines would achieve your requirements and for large collections willbe more performant.
var todays = listHeader.Where(item => item.EntryDate == DateTime.Today);
var twoWeeks = listHeader.Where(item => item.EntryDate < DateTime.Today.AddDays(-1)
&& item.EntryDate >= DateTime.Today.AddDays(-14));
var later = listHeader.Where(item => item.EntryDate < DateTime.Today.AddDays(-14));
also you then get the flexibility of different groupings without impacting your class.
[Edit: in response to ordering query]
Making use of the Enum supplied above you can apply the Union clause and OrderBy clause Linq extension methods as follows:
var ord = todays.Select(item => new {Group = DateRange.Today, item.EntryDate})
.Union(
twoWeeks.Select(item => new {Group = DateRange.LessThanTwoWeeks, item.EntryDate}))
.Union(
later.Select(item => new {Group = DateRange.MoreThanTwoWeeks, item.EntryDate}))
.OrderBy(item => item.Group);
Note that I'm adding the Grouping via a Linq Select and anonymous class to dynamically push a Group property again not effecting the original class. This produces the following output based on the original post:
Group EntryDate
Today 17/09/2013 00:00:00
LessThanTwoWeeks 11/09/2013 00:00:00
LessThanTwoWeeks 15/09/2013 00:00:00
LessThanTwoWeeks 05/09/2013 00:00:00
MoreThanTwoWeeks 26/08/2013 00:00:00
MoreThanTwoWeeks 01/01/2013 00:00:00
and to get grouped date ranges with count:
var ord = todays.Select(item => new {Group = DateRange.Today, Count=todays.Count()})
.Union(
twoWeeks.Select(item => new {Group = DateRange.LessThanTwoWeeks, Count=twoWeeks.Count()}))
.Union(
later.Select(item => new {Group = DateRange.MoreThanTwoWeeks, Count=later.Count()}))
.OrderBy(item => item.Group);
Output is:
Group Count
Today 1
LessThanTwoWeeks 3
MoreThanTwoWeeks 2
I suppose this depends on how heavily you plan on using this. I had/have a lot of reports to generate so I created a model IncrementDateRange with StartTime, EndTime and TimeIncrement as an enum.
The time increment handler has a lot of switch based functions spits out a list of times between the Start and End range based on hour/day/week/month/quarter/year etc.
Then you get your list of IncrementDateRange and in linq something like either:
TotalsList = times.Select(t => new RetailSalesTotalsListItem()
{
IncrementDateRange = t,
Total = storeSales.Where(s => s.DatePlaced >= t.StartTime && s.DatePlaced <= t.EndTime).Sum(s => s.Subtotal),
})
or
TotalsList = storeSales.GroupBy(g => g.IncrementDateRange.StartTime).Select(gg => new RetailSalesTotalsListItem()
{
IncrementDateRange = times.First(t => t.StartTime == gg.Key),
Total = gg.Sum(rs => rs.Subtotal),
}).ToList(),

Categories