Related
There are two entities, for example, job and solution.
Each of them has a date field and a level field and a quantity field.
It is necessary to combine them so that they are grouped first by level, then by month, and at the same time, their quantity must be summed up.
I tried different options, but nothing comes out at all. The main problem is grouping by months and summing the numbers in the enclosed sheets.
That is, the output should be one sequence of summed numbers, grouped by level, and then by month.
For example:
var jobs = new List<Job>()
{
new Job { Level = 1, Date = new DateTime(2019, 1, 1), Quantity = 111 },
new Job { Level = 1, Date = new DateTime(2019, 1, 20), Quantity = 222 },
new Job { Level = 2, Date = new DateTime(2019, 2, 1), Quantity = 333 },
new Job { Level = 2, Date = new DateTime(2019, 2, 20), Quantity = 444 }
};
var solutions = new List<Solution>()
{
new Solution { Level = 1, Date = new DateTime(2019, 2, 1), Quantity = 555 },
new Solution { Level = 2, Date = new DateTime(2019, 2, 20), Quantity = 666 },
new Solution { Level = 1, Date = new DateTime(2019, 1, 1), Quantity = 777 },
new Solution { Level = 2, Date = new DateTime(2019, 1, 20), Quantity = 888 }
};
Output:
Level 1 -> 1 Jan 2019 -> 1110 (111 + 222 + 777)
Level 1 -> 1 Feb 2019 -> 555
Level 2 -> 1 Jan 2019 -> 888
Level 2 -> 1 Feb 2019 -> 1443 (333 + 444 + 666)
And so on. And yes, all this is in EF6.
Try following which uses Concat. I create a class for the merging. It can also be done anonymously.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace ConsoleApplication116
{
class Program
{
static void Main(string[] args)
{
var jobs = new List<Job>()
{
new Job { Level = 1, Date = new DateTime(2019, 1, 1), Quantity = 111 },
new Job { Level = 1, Date = new DateTime(2019, 1, 20), Quantity = 222 },
new Job { Level = 2, Date = new DateTime(2019, 2, 1), Quantity = 333 },
new Job { Level = 2, Date = new DateTime(2019, 2, 20), Quantity = 444 }
};
var solutions = new List<Solution>()
{
new Solution { Level = 1, Date = new DateTime(2019, 2, 1), Quantity = 555 },
new Solution { Level = 2, Date = new DateTime(2019, 2, 20), Quantity = 666 },
new Solution { Level = 1, Date = new DateTime(2019, 1, 1), Quantity = 777 },
new Solution { Level = 2, Date = new DateTime(2019, 1, 20), Quantity = 888 }
};
List<LevelDateQuantity> concat = jobs.Select(x => new LevelDateQuantity() { Date = x.Date, Level = x.Level, Quantity = x.Quantity})
.Concat( solutions.Select(x => new LevelDateQuantity() { Date = x.Date, Level = x.Level, Quantity = x.Quantity})).ToList();
List<LevelDateQuantity> results = concat.OrderBy(x => x.Level).ThenBy(x => x.Date)
.GroupBy(x => new { level = x.Level, date = new DateTime(x.Date.Year, x.Date.Month,1)})
.Select(x => new LevelDateQuantity() { Level = x.Key.level, Date = x.Key.date, Quantity = x.Sum(y => y.Quantity)})
.ToList();
}
}
public class LevelDateQuantity
{
public int Level { get; set; }
public DateTime Date { get; set; }
public int Quantity { get; set; }
}
public class Job : LevelDateQuantity
{
public int Level { get; set; }
public DateTime Date { get; set; }
public int Quantity { get; set; }
}
public class Solution : LevelDateQuantity
{
public int Level { get; set; }
public DateTime Date { get; set; }
public int Quantity { get; set; }
}
}
Oath, because we can not see your poco class structure we don't know if the two tables are seperate or has a one to many relation to a master table, so by the code you have provided I would do this ;
var jobs = new List<Job>()
{
new Job { Level = 1, Date = new DateTime(2019, 1, 1), Quantity = 111 },
new Job { Level = 1, Date = new DateTime(2019, 1, 20), Quantity = 222 },
new Job { Level = 2, Date = new DateTime(2019, 2, 1), Quantity = 333 },
new Job { Level = 2, Date = new DateTime(2019, 2, 20), Quantity = 444 }
};
var solutions = new List<Solution>()
{
new Solution { Level = 1, Date = new DateTime(2019, 2, 1), Quantity = 555 },
new Solution { Level = 2, Date = new DateTime(2019, 2, 20), Quantity = 666 },
new Solution { Level = 1, Date = new DateTime(2019, 1, 1), Quantity = 777 },
new Solution { Level = 2, Date = new DateTime(2019, 1, 20), Quantity = 888 }
};
foreach (var sol in solutions)
{
var jb = new Job();
jb.Level = sol.Level;
jb.Date = sol.Date ;
jb.Quantity= sol.Quantity;
jobs.Add(jb);
}
var result = Jobs.GroupBy(x=> new { x.Level, x.Date}).Select(x=> new
{
level = x.Key.Level,
date = x.Key.Date,
sumQ = x.Sum(y => y.Quantity )
});
I haven't tested the code and not wrote in in a compiler so there might be some typeerrors apart from that this should solve your problem.
I'm working on the following problem:
I want to fill a two-dimensional [365,2] Array. The first value is supposed to hold the date: starting with January 1st and ending with December the 31st. The second value is supposed to hold the corresponding Zodiac Sign to each date:
e.g. array[0, 0] holds 101 and array[0, 1] holds Aries and so on.
I've written a function:
public static void fill_array(string[,] year_zodiac, int days, string zodiac, string startdate, int starting_day)
{
//function to fill array with date and zodiac
int startdate_int = 0;
for (int i = starting_day; i <= days; i++)
{
year_zodiac[i, 0] = startdate;
year_zodiac[i, 1] = zodiac;
startdate_int = Int32.Parse(startdate);
startdate_int++;
startdate = startdate_int.ToString();
}
and call it like this:
fill_array(main_array, 18, "Aries", "101", 0);
This has to be done for every Zodiac sign. I circumvented the month break problem by simply calling fill_array twice (i.e. I call it once for the part of Aries in December and once for the part of aries in January).
This Solution works, but it seems incredibly crude to me.
Can anybody give me some pointers towards a more elegant solution?
Here is a class that does what you want, it has been tested. I did not fill out all signs on the first example, but when I re-factored it I did. I suggest you test this well as I only tested a few cases and might have missed some edge cases.
As has been pointed out to me by #ClickRick, I did start with his array/enum design, but found that lacking when using Linq and moved to a List. I also had to fix his data array so it would compile. I'm giving credit as is due.
public class Signs
{
static List<string> SignList = new List<string>() { "Aquarius", "Pisces", "Aries", "Taurus", "Not Found"};
static DateTime[] startDates = {
new DateTime(DateTime.Now.Year,1,21),
new DateTime(DateTime.Now.Year,2,20),
new DateTime(DateTime.Now.Year,3,21),
new DateTime(DateTime.Now.Year,4,21),
new DateTime(DateTime.Now.Year,12,31) };
static public string Sign(DateTime inDate)
{
return SignList.Zip(startDates, (s, d) => new { sign = s, date = d })
.Where(x => (inDate.Month*100)+inDate.Day <= (x.date.Month*100)+x.date.Day)
.Select(x => x.sign)
.First();
}
}
re-factored (this is clearer with the example above first)
public class Signs
{
static List<string> SignList = new List<string>() {
"Capricorn", "Aquarius", "Pisces", "Aries", "Taurus", "Gemini", "Cancer", "Leo", "Virgo", "Libra", "Scorpio", "Sagittarius", "Capricorn", "Not Found" };
static List<int> startDates = new List<int>() {
// month * 100 + day of month
120, 219, 320, 420, 521, 621, 722, 821, 923, 1023, 1122, 1222, 1232, 9999 // edge marker
};
static public string Sign(DateTime inDate)
{
return SignList[startDates.TakeWhile(d => (inDate.Month*100)+inDate.Day > d).Count()];
}
}
Why do you specifically want an array? Surely a more sophisticated data structure would help to encapsulate the functionality and help you to isolate your remaining code from knowing about the workings of it, and would also let you take account in the future of more subtle variations in the exact dates where they vary by year if you ever want to do that.
For example:
public class SO23182879
{
public enum StarSign
{
Aquarius, Pisces, Aries, Taurus, Gemini, Cancer,
Leo, Virgo, Libra, Scorpio, Sagittarius, Capricorn
};
static DateTime[] starSignStartDates = new DateTime[]
{
new DateTime(DateTime.Now.Year, 1, 20),
new DateTime(DateTime.Now.Year, 2, 19),
new DateTime(DateTime.Now.Year, 3, 21),
new DateTime(DateTime.Now.Year, 4, 20),
new DateTime(DateTime.Now.Year, 5, 21),
new DateTime(DateTime.Now.Year, 6, 21),
new DateTime(DateTime.Now.Year, 7, 23),
new DateTime(DateTime.Now.Year, 8, 23),
new DateTime(DateTime.Now.Year, 9, 23),
new DateTime(DateTime.Now.Year, 10, 23),
new DateTime(DateTime.Now.Year, 11, 22),
new DateTime(DateTime.Now.Year, 12, 22),
new DateTime(DateTime.Now.Year, 1, 20),
};
private class StarSignDateRange
{
public StarSign Sign { get; set; }
public DateTime StartDate { get; set; }
public DateTime EndDate { get; set; }
}
private List<StarSignDateRange> signStartDates = new List<StarSignDateRange>();
public SO23182879()
{
int date = 0;
foreach (StarSign sign in Enum.GetValues(typeof(StarSign)))
{
signStartDates.Add(new StarSignDateRange
{
Sign = sign,
StartDate = starSignStartDates[date],
EndDate = starSignStartDates[date + 1]
});
++date;
}
}
public StarSign Sign(DateTime date)
{
return signStartDates.First(
sd => date.Month == sd.StartDate.Month && date.Day >= sd.StartDate.Day ||
date.Month == sd.EndDate.Month && date.Day < sd.EndDate.Day
).Sign;
}
public void Test()
{
IList<DateTime> testDates = new List<DateTime>
{
new DateTime(2014,1,1),
new DateTime(2014,1,19),
new DateTime(2014,1,20),
new DateTime(2014,4,19),
new DateTime(2014,4,20),
new DateTime(2014,12,21),
new DateTime(2014,12,22),
new DateTime(2014,12,31),
};
foreach (DateTime d in testDates)
Console.WriteLine(string.Format("{0} is in {1}", d, Sign(d)));
}
}
As you'll see, I have completed the code which I had started earlier, and added a Test() method for you to see that the edge conditions work. I am grateful to Hogan for pointing out the "year zero" problem and other similar "gotchas" in my earlier sketch.
I am trying to work out if the following can be done in a LINQ to Objects statement.
I have a dictionary with the key as a DateTime (keys are values that are on multiple days) and a double value. I have too much data to plot on a graph so would like to the average value of each 5 minutes.
Sample Input
01/01/2012 23:53 5
01/01/2012 23:54 2
01/01/2012 23:55 1
01/01/2012 23:56 3
01/01/2012 23:57 4
01/01/2012 23:58 5
01/01/2012 23:59 6
02/01/2012 00:00 2
02/01/2012 00:01 4
02/01/2012 00:02 5
Expected Output
01/01/2012 23:55 3
02/01/2012 00:00 4.4
Using this helper method:
static DateTime RoundToNearestInterval(DateTime dt, TimeSpan d)
{
int f=0;
double m = (double)(dt.Ticks % d.Ticks) / d.Ticks;
if (m >= 0.5)
f=1;
return new DateTime(((dt.Ticks/ d.Ticks)+f) * d.Ticks);
}
it's as simple as
var result = from kvp in data
let key = RoundToNearestInterval(kvp.Key, TimeSpan.FromMinutes(5))
group kvp by key into g
select new { g.Key, Value = g.Average(x => x.Value) };
or
var result = data.GroupBy(kvp => RoundToNearestInterval(kvp.Key, TimeSpan.FromMinutes(5)), kvp => kvp.Value)
.Select(g => new { g.Key, Value = g.Average() });
LINQPad example:
void Main()
{
var tmp = new Dictionary<string, int>
{
{"01/01/2012 23:53", 5},
{"01/01/2012 23:54", 2},
{"01/01/2012 23:55", 1},
{"01/01/2012 23:56", 3},
{"01/01/2012 23:57", 4},
{"01/01/2012 23:58", 5},
{"01/01/2012 23:59", 6},
{"02/01/2012 00:00", 2},
{"02/01/2012 00:01", 4},
{"02/01/2012 00:02", 5}
};
var data = tmp.ToDictionary(d => DateTime.Parse(d.Key), d=>d.Value);
var result = from kvp in data
let key = RoundToNearestInterval(kvp.Key, TimeSpan.FromMinutes(5))
group kvp by key into g
select new {g.Key, Value = g.Average (x => x.Value) };
result.ToDictionary(r => r.Key, v => v.Value).Dump();
}
Here's a LINQ query that will do what you want, you can test this in LINQPad:
void Main()
{
var points = new[]
{
new { dt = new DateTime(2012, 1, 1, 23, 53, 00), value = 5 },
new { dt = new DateTime(2012, 1, 1, 23, 54, 00), value = 2 },
new { dt = new DateTime(2012, 1, 1, 23, 55, 00), value = 1 },
new { dt = new DateTime(2012, 1, 1, 23, 56, 00), value = 3 },
new { dt = new DateTime(2012, 1, 1, 23, 57, 00), value = 4 },
new { dt = new DateTime(2012, 1, 1, 23, 58, 00), value = 5 },
new { dt = new DateTime(2012, 1, 1, 23, 59, 00), value = 6 },
new { dt = new DateTime(2012, 1, 2, 00, 00, 00), value = 2 },
new { dt = new DateTime(2012, 1, 2, 00, 01, 00), value = 4 },
new { dt = new DateTime(2012, 1, 2, 00, 01, 00), value = 5 }
};
var interval = TimeSpan.FromMinutes(5);
var averageByInterval =
from point in points
let intervalStart = new DateTime(((int)((point.dt.Ticks + interval.Ticks / 2) / interval.Ticks)) * interval.Ticks)
group point.value by intervalStart into g
select new { g.Key, average = g.Average() };
averageByInterval.Dump();
}
Output:
Looks like your dictionary contains the ordered elements so we can do something like this:
var firstDate = yourDict.First().Key;
var output = yourDict.GroupBy(e=> (int)(e.Key - firstDate).TotalMinutes / 5)
.ToDictionary(g => g.First().Key
.AddMinutes(g.Average(e=>(e.Key - g.First().Key).TotalMinutes)),
g => g.Average(e=>e.Value));
NOTE: The input data of the OP uses a different cutlure than en-US, the month goes after the day. That's the noticeable point to take some test. otherwise the test won't be correct.
Try this:
var results =
data
.GroupBy(
x => (x.Key.Ticks / TimeSpan.TicksPerMinute + 2) / 5,
x => x.Value)
.Select(x => new
{
Key = new DateTime(x.Key * TimeSpan.TicksPerMinute * 5),
Value = x.Average()
});
var data = new Dictionary<DateTime, double>();
data.Add(new DateTime(2012, 1, 1, 23, 53, 0), 5);
data.Add(new DateTime(2012, 1, 1, 23, 54, 0), 2);
data.Add(new DateTime(2012, 1, 1, 23, 55, 0), 1);
data.Add(new DateTime(2012, 1, 1, 23, 56, 0), 3);
data.Add(new DateTime(2012, 1, 1, 23, 57, 0), 4);
data.Add(new DateTime(2012, 1, 1, 23, 58, 0), 5);
data.Add(new DateTime(2012, 1, 1, 23, 59, 0), 6);
data.Add(new DateTime(2012, 1, 2, 0, 0, 0), 2);
data.Add(new DateTime(2012, 1, 2, 0, 1, 0), 4);
data.Add(new DateTime(2012, 1, 2, 0, 2, 0), 5);
var result = data.GroupBy(kvp =>
{
var dt = kvp.Key;
var nearest5 = (int)Math.Round(dt.Minute / 5.0) * 5;
//Add the minutes after inital date creation to deal with minutes=60
return new DateTime(dt.Year, dt.Month, dt.Day, dt.Hour, 0, 0).AddMinutes(nearest5);
})
.Select(g =>
{
return new KeyValuePair<DateTime, double>(g.Key, g.Average(row => row.Value));
});
foreach (var r in result)
{
Console.WriteLine(r.Key + " " + r.Value);
// 1/01/2012 11:55:00 PM 3
// 2/01/2012 12:00:00 AM 4.4
}
So the distinct is based on unique Month/Year, not just one distinct month (so I would want January of 2011 and January of 2012 to be distinct)
// Test set of data
List<DateTime> CompleteListOfDates = new List<DateTime>();
CompleteListOfDates.Add(new DateTime(2011, 1, 1));
CompleteListOfDates.Add(new DateTime(2011, 1, 5));
CompleteListOfDates.Add(new DateTime(2011, 3, 1));
CompleteListOfDates.Add(new DateTime(2011, 5, 1));
CompleteListOfDates.Add(new DateTime(2011, 5, 1));
CompleteListOfDates.Add(new DateTime(2012, 1, 1));
CompleteListOfDates.Add(new DateTime(2012, 2, 1));
List<DateTime> UniqueMonthYears = new List<DateTime>();
/* need distinct list of DateTimes that are distinct by Month and Year and should be in UniqueMonthYears
For example:
new DateTime(2011, 1, 1)
new DateTime(2011, 3, 1)
new DateTime(2011, 5, 1)
new DateTime(2012, 1, 1)
new DateTime(2012, 2, 1)
*/
List<DateTime> result = source
.Select(d => new DateTime(d.Year, d.Month, 1))
.Distinct()
.ToList();
Also useful:
ILookup<DateTime, Order> ordersByMonth = ordersSource
.ToLookup(order => new DateTime(order.OrderDate.Year, order.OrderDate.Month, 1));
You can create your own IEqualityComparer implementation and let Linq do the rest, like this:
// implement this
public class DateTimeComparer : IEqualityComparer<DateTime>
{
public bool Equals(DateTime x, DateTime y)
{
return x.Year == y.Year && x.Month == y.Month;
}
public int GetHashCode(DateTime obj)
{
return obj.Year * 100 + obj.Month;
}
}
// use it like this
UniqueMonthYears = CompleteListOfDates.Distinct(new DateTimeComparer()).ToList();
var uniqueMonthYears = CompleteListOfDates
.GroupBy(d => new{ d.Month, d.Year})
.Select(my => new DateTime(my.Key.Year, my.Key.Month, 1));
Will give you an IEnumerable<DateTime>.
Edit: David B's answer is better, but leaving this here as an option.
How about somthing like
completeListOfDates.Select(d => new {Year = d.Year, Month = d.Month}).Distinct()
Preserving the first occuring day part is essentially meaningless but you could do so like this.
completeListOfDates.Distinct(new YearMonthComparer());
private class YearMonthComparer : IEqualityComparer<DateTime>
{
public bool Equals(DateTime x, DateTime y)
{
if(x.Year != y.Year) return false;
if(x.Month != y.Month) return false;
return true;
}
public int GetHashCode(DateTime obj)
{
int hash = 13;
hash = (hash * 7) + obj.Year.GetHashCode();
hash = (hash * 7) + obj.Month.GetHashCode();
reutrn hash;
}
}
Here's the shortest code I'd know of:
List<DateTime> UniqueMonthYears =
CompleteListOfDates.Select(t => new DateTime(t.Year, t.Month, 1))
.Distinct()
.ToList();
Here's more structural but way less performant approach (for demo purpose only). What it give is the ability to set string GetHash(Date) and Date CreateFromHash(string) functions, thus making it more generic.
Code:
private void Form1_Load(object sender, EventArgs e)
{
List<DateTime> CompleteListOfDates = new List<DateTime>();
CompleteListOfDates.Add(new DateTime(2011, 1, 1));
CompleteListOfDates.Add(new DateTime(2011, 1, 5));
CompleteListOfDates.Add(new DateTime(2011, 3, 1));
CompleteListOfDates.Add(new DateTime(2011, 5, 1));
CompleteListOfDates.Add(new DateTime(2011, 5, 1));
CompleteListOfDates.Add(new DateTime(2012, 1, 1));
CompleteListOfDates.Add(new DateTime(2012, 2, 1));
List<DateTime> UniqueMonthYears =
CompleteListOfDates.Select(t =>
GetDateHash(t)).Distinct().Select(t =>
CreateFromDateHash(t)).ToList();
MessageBox.Show(UniqueMonthYears.Count.ToString());
}
private static string GetDateHash(DateTime date)
{
return date.ToString("MMM-yyyy");
}
private static DateTime CreateFromDateHash(string hash)
{
return DateTime.Parse("1-" + hash);
}
Aw! Everyone beat me to it. And LINQ questions are so much fun. ;-)
I didn't know how you wanted to select the unique date from the list, so instead of assuming you wanted to discard that, I used the First method in a subquery. Obviously you could change that using a sort of some kind as well as the predicate.
How about this then?
var UniqueMonthYears = (from date in CompleteListOfDates
let month = date.Month
let year = date.Year
let day = (CompleteListOfDates.First(d => d.Month == month && d.Year == year))
select day).Distinct();
Oh, and for bonus points... if you've not already, download Linqpad from http://www.linqpad.net/ You can try out this kind of stuff really easily with that.
Given a list of dates (which may not be sorted), I want to build a list of date ranges -
E.g. Assuming MM/DD format,
Input - 5/1, 5/5, 5/6, 5/15, 5/7, 5/8, 5/19,5/20, 5/23
Output -
Date Range 1: 5/1 to 5/1
Date Range 2: 5/5 to 5/8
Date Range 3: 5/15 to 5/15
Date Range 4: 5/19 to 5/20
Date Range 5: 5/23 to 5/23
Basically, a range should be continuous.
Sort the dates
Start a range containing the next date (to start with it will be the first one)
Is the second "valid" date the next date which would be in the range? If so, keep going. If not, close the current range and start a new one.
Repeat until you've run out of dates, at which point you close the current range and you're done.
You can create a list of DateTime (possibly using the same year for them all) and sort it.
It is then fairly easy to find if a day and the next day exist in the list (using DateTime.AddDays(1)).
public class DateRange
{
public DateTime Start { get; set; }
public DateTime End { get; set; }
}
[TestClass]
public class DateRangerTest
{
private List<DateRange> GetDateRanges(List<DateTime> dates)
{
if (dates == null || !dates.Any()) return null;
dates = dates.OrderBy(x => x.Date).ToList();
var dateRangeList = new List<DateRange>();
DateRange dateRange = null;
for (var i = 0; i < dates.Count; i++)
{
if (dateRange == null)
{
dateRange = new DateRange { Start = dates[i] };
}
if (i == dates.Count - 1 || dates[i].Date.AddDays(1) != dates[i + 1].Date)
{
dateRange.End = dates[i].Date;
dateRangeList.Add(dateRange);
dateRange = null;
}
}
return dateRangeList;
}
[TestMethod]
public void GetDateRanges_MultiDateRangeTest()
{
var dates = new List<DateTime>
{
new DateTime(1999,5,1),
new DateTime(1999,5,5),
new DateTime(1999,5,6),
new DateTime(1999,5,15),
new DateTime(1999,5,7),
new DateTime(1999,5,8),
new DateTime(1999,5,19),
new DateTime(1999,5,20),
new DateTime(1999,5,23)
};
var dateRanges = GetDateRanges(dates);
Assert.AreEqual(new DateTime(1999, 5, 1), dateRanges[0].Start);
Assert.AreEqual(new DateTime(1999, 5, 1), dateRanges[0].End);
Assert.AreEqual(new DateTime(1999, 5, 5), dateRanges[1].Start);
Assert.AreEqual(new DateTime(1999, 5, 8), dateRanges[1].End);
Assert.AreEqual(new DateTime(1999, 5, 15), dateRanges[2].Start);
Assert.AreEqual(new DateTime(1999, 5, 15), dateRanges[2].End);
Assert.AreEqual(new DateTime(1999, 5, 19), dateRanges[3].Start);
Assert.AreEqual(new DateTime(1999, 5, 20), dateRanges[3].End);
Assert.AreEqual(new DateTime(1999, 5, 23), dateRanges[4].Start);
Assert.AreEqual(new DateTime(1999, 5, 23), dateRanges[4].End);
}
}