Time series LINQ query - c#

I have a central repository for IoT device logs. So as the logs arrive they have a timestamp. The problem I want to solve is, over a given time span, the same device might send multiple logs regarding its interaction with a specific catalyst. I want to consider that set of logs as a single event and not 5 disparate logs. I want to count the number of interactions. and not the number of logs.
Data Set
public class Data
{
public Guid DeviceId {get; set;}
public DateTime StartTime { get; set; }
public DateTime EndDateTime { get; set; }
public int Id { get; set; }
public int Direction { get; set;}
}
Data d1 = new Data();// imagine it's populated
Data d2 = new Data();// imagine it's populated
I am looking for a LINQ query that would yield something along the lines of
If ((d1.DeviceId == d2.DeviceId ) && (d1.Id == d2.Id) && (d1.Direction == d2.Direction) && (d1.StartTime - d2.StartTime < 15 minutes ))
If i know that the same IoT device is interacting with the same Id (catalyst) and the Direction is the same, and all of those logs occur within a 15 minute time span, It can be presumed that they correspond to the same catalyst event.
I do not control the log creation so ... no i cannot update the data to include "something" that would indicate the relationship.
Data per request... nothing fancy. I am sure most people suspect that I have 30+ properties and I only provide the one impacted by the calculation, but this is a simple set of possibilities
class SampleData
{
public List<Data> GetSampleData()
{
Guid device1 = Guid.NewGuid();
List<Data> dataList = new List<Data>();
Data data1 = new Data();
data1.DeviceId = device1;
data1.Id = 555;
data1.Direction = 1;
data1.StartTime = new DateTime(2010, 8, 18, 16, 32, 0);
data1.EndDateTime = new DateTime(2010, 8, 18, 16, 32, 30);
dataList.Add(data1);
//so this data point should be excluded in the final result
Data data2 = new Data();
data1.DeviceId = device1;
data1.Id = 555;
data1.Direction = 1;
data1.StartTime = new DateTime(2010, 8, 18, 16, 32, 32);
data1.EndDateTime = new DateTime(2010, 8, 18, 16, 33, 30);
dataList.Add(data2);
//Should be included because ID is different
Data data3 = new Data();
data1.DeviceId = device1;
data1.Id = 600;
data1.Direction = 1;
data1.StartTime = new DateTime(2010, 8, 18, 16, 32, 2);
data1.EndDateTime = new DateTime(2010, 8, 18, 16, 32, 35);
dataList.Add(data3);
//exclude due to time
Data data4 = new Data();
data1.DeviceId = device1;
data1.Id = 600;
data1.Direction = 1;
data1.StartTime = new DateTime(2010, 8, 18, 16, 32, 37);
data1.EndDateTime = new DateTime(2010, 8, 18, 16, 33, 40);
dataList.Add(data4);
//include because time > 15 minutes
Data data5 = new Data();
data1.DeviceId = device1;
data1.Id = 600;
data1.Direction = 1;
data1.StartTime = new DateTime(2010, 8, 18, 16, 58, 42);
data1.EndDateTime = new DateTime(2010, 8, 18, 16, 58, 50);
dataList.Add(data5);
return dataList;
}

This turned out to be more complex than I hoped for.
I used a custom LINQ extension method I have called ScanPair which is a variation of my Scan method, which is an version of the APL scan operator (which is like Aggregate, but returns the intermediate results). ScanPair returns the intermediate results of the operation along with each original value. I think I need to think about how to make all of these more general purpose, as the pattern is used by a bunch of other extension methods I have for grouping by various conditions (e.g. sequential, runs, while test is true or false).
public static class IEnumerableExt {
public static IEnumerable<(TKey Key, T Value)> ScanPair<T, TKey>(this IEnumerable<T> src, Func<T, TKey> seedFn, Func<(TKey Key, T Value), T, TKey> combineFn) {
using (var srce = src.GetEnumerator()) {
if (srce.MoveNext()) {
var seed = (seedFn(srce.Current), srce.Current);
while (srce.MoveNext()) {
yield return seed;
seed = (combineFn(seed, srce.Current), srce.Current);
}
yield return seed;
}
}
}
}
Now, you can use a tuple as an intermediate result to track the initial timestamp and the group number, and increment to the next (timestamp, group number) when the interval goes over 15 minutes. If you first group by the interaction, and then count the less than 15-minute groups per interaction, you get the answer:
var ans = interactionLogs.GroupBy(il => new { il.DeviceId, il.Id, il.Direction })
.Select(ilg => new {
ilg.Key,
Count = ilg.OrderBy(il => il.Timestamp)
.ScanPair(il => (firstTimestamp: il.Timestamp, groupNum: 1), (kvp, cur) => (cur.Timestamp - kvp.Key.firstTimestamp).TotalMinutes <= 15 ? kvp.Key : (cur.Timestamp, kvp.Key.groupNum + 1))
.GroupBy(ilkvp => ilkvp.Key.groupNum, ilkvp => ilkvp.Value)
.Count()
});
Here is a portion of a sample of intermediate results from ScanPair - the actual result is a ValueTuple with two fields, where the Key is the intermediate result (which is the ValueTuple of firstTimestamp,groupNum) and Value is the corresponding source (log) item. Using the function seeded version puts the first source item into the seed function to begin the process.
Key_firstTimestamp Key_groupNum Timestamp
7:58 PM 1 7:58 PM
7:58 PM 1 8:08 PM
7:58 PM 1 8:12 PM
8:15 PM 2 8:15 PM
8:15 PM 2 8:20 PM

Related

How to group one collection and apply same grouping to other, same-length collections?

I have multiple identical length collections, one timestamp collection of type List<DateTime> and several data collections of type List<double>. The values at each index position in the List<double correspond to the respective index position in List<DateTime>.
I want to be able to compress the data in all data collections by a TimeSpan that is applied to the List<DateTime> and groups the timestamps into TimeSpan bins and applies the same grouping to each data collection.
Here is how I currently "compress" a time series of time stamps:
var someTimeStamps = new List<DateTime>();
var compression = TimeSpan.FromHours(1).Ticks;
var compressedTimeStamps = from rawData in someTimeStamps
group rawData by rawData.Ticks / numberTicks
into tickData
select new DateTime(tickData.Key * compression);
How can I adjust the code in order to have the same groupings apply to the data collections List<double> as well? I want to apply a grouping logic of averaging the values within each data group. I am aiming for computational efficiency, memory consumption is not an issue I look to optimize at this point.
For example:
List<DateTime> items: (for simplicity purpose the order of the values below is (year, month, day, hour, minute, second):
(1) 2018, 8, 14, 08, 20, 05
(2) 2018, 8, 14, 08, 45, 25
(3) 2018, 8, 14, 09, 02, 53
(4) 2018, 8, 14, 09, 34, 12
(5) 2018, 8, 14, 09, 44, 12
List<value> items:
(1) 12
(2) 15
(3) 27
(4) 03
(5) 12
Applying a compression of TimeSpan.FromHours(1) the desired outcome for both collections is :
List<DateTime> items:
(1) 2018, 8, 14, 08, 00, 00
(2) 2018, 8, 14, 09, 00, 00
List<double> items (averaging is applied to the items in each group)
(1) 13.5 (avg of 12 and 15)
(2) 14 (avg of 27, 3, and 12)
You can do it by below code
List<DateTime> dateTimes = new List<DateTime>();
dateTimes.Add(new DateTime(2018, 8, 14, 08, 20, 05));
dateTimes.Add(new DateTime(2018, 8, 14, 08, 45, 25));
dateTimes.Add(new DateTime(2018, 8, 14, 09, 02, 53));
dateTimes.Add(new DateTime(2018, 8, 14, 09, 34, 12));
dateTimes.Add(new DateTime(2018, 8, 14, 09, 44, 12));
List<int> ints = new List<int>();
ints.Add(12);
ints.Add(15);
ints.Add(27);
ints.Add(03);
ints.Add(12);
var averages = dateTimes.Select((k, v) => new { k, v })
.GroupBy(x => new DateTime(x.k.Year, x.k.Month, x.k.Day, x.k.Hour, 0, 0))
.ToDictionary(g => g.Key, g => g.Select(x => ints.ElementAt(x.v)).Average());
Output:
Edit:
If you want your data to be separated into two list like List<DateTime> and List<double> then you can project above dictionary to separated list of keys and values. like
List<DateTime> dateTimeList = averages.Keys.ToList();
List<double> valuesList = averages.Values.ToList();
If I understood you correctly
expand that problem to one time stamp series but multiple data series
var grouped = dateTimes
.Zip(ints, (k, v) => new { k, v })
.GroupBy(g => new DateTime(g.k.Year, g.k.Month, g.k.Day, g.k.Hour, 0, 0), g => g.v);
The above code gives you the compression of your datetime and wrt mulptiple data series
Try once may it help you.
I decided to go with a classic iteration over each data point as it only requires one single iteration regardless of the number of data collections (credits to a friend of mine who suggested to profile this approach):
public void CompressData(TimeSpan compression)
{
//declare generic buffer value function (bid/ask here)
var bufferFunction = new Func<int, double>(index => (Bid[index] + Ask[index]) / 2);
Open = new List<double>();
High = new List<double>();
Low = new List<double>();
Close = new List<double>();
var lastCompTs = -1L;
var dataBuffer = new List<double>();
var timeStamps = new List<DateTime>();
for (int i = 0; i < TimeStamps.Count; ++i)
{
var compTs = TimeStamps[i].Ticks / compression.Ticks;
if (compTs == lastCompTs)
{
//same timestamp -> add to buffer
dataBuffer.Add(bufferFunction(i));
}
else
{
if (dataBuffer.Count > 0)
{
timeStamps.Add(new DateTime(compTs * compression.Ticks));
Open.Add(dataBuffer.First());
High.Add(dataBuffer.Max());
Low.Add(dataBuffer.Min());
Close.Add(dataBuffer.Last());
}
lastCompTs = compTs;
dataBuffer.Clear();
}
}
if (dataBuffer.Count > 0)
{
timeStamps.Add(new DateTime(lastCompTs * compression.Ticks));
Open.Add(dataBuffer.First());
High.Add(dataBuffer.Max());
Low.Add(dataBuffer.Min());
Close.Add(dataBuffer.Last());
}
//assign time series collection
TimeStamps = timeStamps;
}

Insert Data into middle of range

I have the following model
public class DailyRoutine
{
public int Id { get; set; }
public DateTime Date { get; set; }
public string Description { get; set; }
}
Scenario:
When is created at initial time with 5 records which means 5 entries are entered for each day. Take an example May 1 to May 5 of 2017. Description have any string.
User can add a new record in the middle so that the following records should be moved and changed to next days.
Expected Output:
Example, user can give a date and description in input and submit. If the input date is '5/3/2017' (May 3), the entry should be added after May 2 record and the existing May 3 record changed to May 4, May 4 to May 5 etc. So the out is like May 1 to May 6 and the given input is updated on May 3.
Please help me to this with out degrading performance
This approach will work:
List<DailyRoutine> d = new List<DailyRoutine>()
{
new DailyRoutine() { Date = new DateTime(2017, 7, 1)},
new DailyRoutine() { Date = new DateTime(2017, 7, 2)},
new DailyRoutine() { Date = new DateTime(2017, 7, 3)},
new DailyRoutine() { Date = new DateTime(2017, 7, 4)},
new DailyRoutine() { Date = new DateTime(2017, 7, 5)}
};
DailyRoutine newDr = new DailyRoutine() { Date = new DateTime(2017, 7, 2) };
DailyRoutine oldDr = d.Where(dr => dr.Date == newDr.Date).FirstOrDefault();
if (oldDr != null)
{
int idx = d.IndexOf(oldDr);
List<DailyRoutine> changeList = d.Where((dr, i) => i >= idx).ToList();
foreach (DailyRoutine i in changeList)
{
i.Date = i.Date.AddDays(1);
}
d.Insert((int)idx, newDr);
}
else
{
d.Add(newDr);
}

C# merge multiple lists based on timestamp

I have a data set that comes as a list of objects in C#, looking something below.
public class MotorDataModel
{
public DateTime timestamp { set; get; }
public decimal MotorSpeed { set; get; }
public decimal MotorTemp { set; get; }
public decimal MotorKw{ set; get; }
}
public class MotorModel
{
public string MotorName { set; get; }
public List<MotorDataModel> MotorData { set; get; }
}
When I do the query, I will have 1 or more MotorModel records coming back (say motor 1, 2, 3, ...), each with their own timestamps, and various data points at those time stamps.
I am then sending this data to a javascript charting library, which takes the data in as a data table (e.g. spreadsheet like format), such as:
TimeStamp | Motor1:kW | Motor1:Speed | Motor1:Temp | Motor2:kW |Motor2:Speed ...
with the data following in rows. The data will be grouped on the timestamp, which should be the within a couple minutes of each other, in a consistent increment (say 15 minutes).
The plan is to transform the data in C#, convert it to JSON, and and send it to the chart library (Google Chart).
I don't have to format this in C#, and could convert the Object list data in C# to JSON, and reformat it in javascript on the client, but it seems better to transform it at the server.
Either way, I am struggling on how to transform the data from a multiple list of objects to a "datatable" like view.
This answer via LINQ seems to be close, but I have multiple lists of equipment, not a defined number.
I have also looked at just looping through and building the data table (or array), but unsure of what structure makes the most sense.
So, if anyone has done something similar, or has any feedback, it would be much appreciated.
Suggested format for providing sample data
Below is some sample data provided by BlueMonkMN. Please update the question providing sample data representative of your actual question.
List<MotorModel> allData = new List<MotorModel>() {
new MotorModel() {MotorName="Motor1", MotorData = new List<MotorDataModel> {
new MotorDataModel(){timestamp=new DateTime(2016, 9, 18, 2, 56, 0), MotorSpeed=20.0M, MotorTemp=66.2M, MotorKw=5.5M},
new MotorDataModel(){timestamp=new DateTime(2016, 9, 18, 3, 10, 30), MotorSpeed=10.0M, MotorTemp=67.0M, MotorKw=5.5M},
new MotorDataModel(){timestamp=new DateTime(2016, 9, 18, 3, 25, 45), MotorSpeed=17.5M, MotorTemp=66.1M, MotorKw=5.8M},
new MotorDataModel(){timestamp=new DateTime(2016, 9, 18, 3, 40, 23), MotorSpeed=22.2M, MotorTemp=65.8M, MotorKw=5.4M}
}},
new MotorModel() {MotorName="Motor2", MotorData = new List<MotorDataModel> {
new MotorDataModel(){timestamp=new DateTime(2016, 9, 18, 2, 58, 0), MotorSpeed=21.0M, MotorTemp=67.2M, MotorKw=5.6M},
new MotorDataModel(){timestamp=new DateTime(2016, 9, 18, 3, 11, 30), MotorSpeed=11.0M, MotorTemp=68.0M, MotorKw=5.6M},
new MotorDataModel(){timestamp=new DateTime(2016, 9, 18, 3, 24, 45), MotorSpeed=18.5M, MotorTemp=67.1M, MotorKw=5.9M},
new MotorDataModel(){timestamp=new DateTime(2016, 9, 18, 3, 39, 23), MotorSpeed=23.2M, MotorTemp=66.8M, MotorKw=5.5M}
}}
};
One possibility is to iterate through all the data, and build the table, as you suggest. I would suggest using a Dictionary, with the timestamp as the key. For each timestamp there can be multiple MotorData's, so it could have a list, like this:
Dictionary<DateTime, List<MotorDataModel>>
A code snippet to build this table would look like this:
List<MotorModel> motorModels; // filled in previously
// build result structure in this dictionary:
Dictionary<DateTime, List<MotorDataModel>> table = new Dictionary<DateTime, List<MotorDataModel>>();
// iterate through all motors and their data, and fill in the table
foreach(MotorModel m in motorModels)
{
foreach(MotorDataModel md in m.MotorData)
{
DateTime ts = md.timestamp;
// if this is the first occurance of the timestamp, create new 'row'
if (!table.ContainsKey(ts)) table[ts] = new List<MotorDataModel>();
// add the data to the 'row' of this timestamp
table[ts].Add(md);
}
}
// output the table
foreach(DateTime ts in table.Keys)
{
...
foreach(MotorDataModel md in table[ts])
{
...
}
}
I'd use Json.NET from NewtonSoft.
JObject o = new JObject();
foreach (MotorModel mm in allData) {
foreach (MotorDataModel mdm : mm.MotorData()) {
string key = mdm.TimeStamp.ToString(); // Or do your own format
o[key][mm.MotorName + ":kW"] = mdm.MotorKw;
o[key][mm.MotorName + ":Speed"] = mdm.MotorSpeed;
o[key][mm.MotorName + ":TEmp"] = mdm.MotorTemp;
}
}
Could you try something like this to compute your data:
var motorData = allData.SelectMany(x => x.MotorData).ToArray();
var starting = motorData.Min(x => x.timestamp);
var ending = motorData.Max(x => x.timestamp);
var duration = ending.Subtract(starting);
var blocks = (int)Math.Ceiling(duration.TotalMinutes / 15.0);
var query =
from b in Enumerable.Range(0, blocks)
let s = starting.AddMinutes(b * 15.0)
let e = starting.AddMinutes((b + 1.0) * 15.0)
select new
{
Timestamp = s,
MotorSpeedAverage =
motorData
.Where(x => x.timestamp >= s && x.timestamp < e)
.Average(x => x.MotorSpeed),
};
I get this result:

Reorganize values in 3 arrays based off a unique combination of values in 2 of the arrays in C#

I have 3 arrays with the data types below:
string[] arrID = {"111", "222", "333", "444", "555", "666", "777"};
DateTime[] arrDates = new DateTime[]
{
new DateTime(2015, 03, 20),
new DateTime(2015, 03, 20),
new DateTime(2015, 03, 20),
new DateTime(2015, 03, 21),
new DateTime(2015, 03, 21),
new DateTime(2015, 03, 20),
new DateTime(2015, 03, 20)
};
string[] arrTime = {"8:20", "8:40", "8:20", "9:10", "8:20", "9:10", "8:20"};
I have added sample elements into the 3 arrays to simulate the types of data we will be having in these arrays.
Each index number in each of these elements contain values related to one record.
Example: The values in each of the array with index 3 contains values that make up one record.
I need to put values in these 3 arrays into the following 3 arrays: arrNEWID, arrNEWDate and arrNEWTime based on the following conditions:
For each unique date and time combination, a separate element needs to be added to each of the arrays.
If a unique date and time combination has more than one ID, then the IDs need to be separated by a comma.
Expected output in the 3 NEW arrays
--------------------------------------------------
| index | arrNEWDate | arrNEWTime | arrNEWID |
--------------------------------------------------
| 0 | 03/20/2015 | 8:20 | 111,333,777 |
| 1 | 03/20/2015 | 8:40 | 222 |
| 2 | 03/20/2015 | 9:10 | 666 |
| 3 | 03/21/2015 | 8:20 | 555 |
| 4 | 03/21/2015 | 9:10 | 444 |
Notes:
The data types on the 3 NEW arrays need to be string
The input and output need to remain as arrays. Lists may be created for processing
Here is the code I have used already:
string[] arrID = { "111", "222", "333", "444", "555", "666", "777" };
DateTime[] arrDates = new DateTime[]
{
new DateTime(2015, 03, 20),
new DateTime(2015, 03, 20),
new DateTime(2015, 03, 20),
new DateTime(2015, 03, 21),
new DateTime(2015, 03, 21),
new DateTime(2015, 03, 20),
new DateTime(2015, 03, 20)
};
string[] arrTime = { "8:20", "8:40", "8:20", "9:10", "8:20", "9:10", "8:20" };
string str_arrNEWID = "";
string str_arrNEWDate = "";
string str_arrNEWTime = "";
string[] arrNEWID = "".Split('~');
string[] arrNEWDate = "".Split('~');
string[] arrNEWTime = "".Split('~');
for (int i = 0; i <= arrID.GetUpperBound(0); i++)
{
int intExists = 0;
for (int j = 0; j <= arrNEWDate.GetUpperBound(0); j++)
{
//check if date matches for the current index being checked
if (arrNEWDate[j].ToString() == arrDates[i].Date.ToString("MM/dd/yyyy"))
{
//check if time matches for the same index
if (arrNEWTime[j].ToString() == arrTime[i].ToString())
{
//existing record
intExists = 1;
arrNEWID[j] = arrNEWID[j] + "," + arrID[i];
str_arrNEWID = string.Join("~", arrNEWID);
}
}
}
if (intExists == 0)
{
//new record
str_arrNEWDate = str_arrNEWDate + "~" + arrDates[i].Date.ToString("MM/dd/yyyy") ;
arrNEWDate = str_arrNEWDate.Split('~');
str_arrNEWTime = str_arrNEWTime + "~" + arrTime[i].ToString() ;
arrNEWTime = str_arrNEWTime.Split('~');
str_arrNEWID = str_arrNEWID + "~" + arrID[i];
arrNEWID = str_arrNEWID.Split('~');
}
}
The main challenge is that the compiler that needs to be used to compile this code doesn't support the lambda operator (=>)
I need to know:
If there is a way to do it so that I don't have to use the temporary string variables and split function to refresh the arrays each time
If there is any way to get rid of the multiple loops
arrays make everything more difficult.
using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Collections.Generic;
using System.Linq;
namespace UnitTestProject1
{
[TestClass]
public class UnitTest1
{
[TestMethod]
public void TestMethod1()
{
string[] arrID = { "111", "222", "333", "444", "555", "666", "777" };
DateTime[] arrDates = new DateTime[]
{
new DateTime(2015, 03, 20),
new DateTime(2015, 03, 20),
new DateTime(2015, 03, 20),
new DateTime(2015, 03, 21),
new DateTime(2015, 03, 21),
new DateTime(2015, 03, 20),
new DateTime(2015, 03, 20)
};
string[] arrTime = { "8:20", "8:40", "8:20", "9:10", "8:20", "9:10", "8:20" };
int i = -1;
int index = 0;
List<ewansObject> l2 = arrID.Select(o => new ewansObject() { date = arrDates[++i].ToString(), time = arrTime[i], id = arrID[i] }).GroupBy(k => k.date + k.time).Select(o => new ewansObject() { index = index++.ToString(), date = o.First().date, time = o.First().time, id = String.Join(",", o.Select(x => x.id)) }).ToList(); ;
string[] arrNEWID = l2.Select(x => x.id).ToArray();
string[] arrNEWDate = l2.Select(x => x.date).ToArray();
string[] arrNEWTime = l2.Select(x => x.time).ToArray();
Console.WriteLine(String.Join("|", arrNEWID));
Console.WriteLine(String.Join("|", arrNEWDate));
Console.WriteLine(String.Join("|", arrNEWTime));
}
public class ewansObject
{
public string index;
public string date;
public string time;
public string id;
}
}
}
maybe I should change my name to homework4points
The kind of processing you are looking for is very easily handled by the LINQ features in .NET. In particular, LINQ includes the very handy group...by and orderby which are directly applicable to your scenario. Unfortunately, the normal LINQ syntax also relies heavily on lambda syntax, which you say you cannot use.
However, note that the lambda syntax is just a "syntactic sugar", i.e. a way of expressing to the compiler something you could write yourself just in a more verbose way. In particular, the lambda syntax as used here generates anonymous methods, which you can of course just write as named methods.
Anonymous methods also allow "capturing" of variables, but in your scenario we can avoid needing to capture any variables by doing two things:
Put the data in class fields, where the named method can access them
Use the Enumerable.Select() overload that passes an index to the selector method, so that the named method can correlated the source items with the elements in the other two arrays that relate to each one
Likewise, while anonymous types would be handy here, effective use of those relies on anonymous methods and inferring types of the method parameters for those anonymous methods. In lieu of that, the Tuple class will serve just as well.
Putting all that together, we get something that looks like this:
class Program
{
static readonly string[] arrID = { "111", "222", "333", "444", "555", "666", "777" };
static readonly DateTime[] arrDates = new DateTime[]
{
new DateTime(2015, 03, 20),
new DateTime(2015, 03, 20),
new DateTime(2015, 03, 20),
new DateTime(2015, 03, 21),
new DateTime(2015, 03, 21),
new DateTime(2015, 03, 20),
new DateTime(2015, 03, 20)
};
static readonly string[] arrTime = { "8:20", "8:40", "8:20", "9:10", "8:20", "9:10", "8:20" };
static void Main(string[] args)
{
// Generate intermediate query with grouped, ordered data
Tuple<DateTime, string>[] q = arrID.Select(CreateRecord)
.GroupBy(GetKey)
.OrderBy(GetGroupKey)
.Select(FlattenGroup)
.ToArray();
// Final output arrays here
string[] str_arrNEWDate = new string[q.Length],
str_arrNEWTime = new string[q.Length],
str_arrNEWID = new string[q.Length];
for (int i = 0; i < q.Length; i++)
{
str_arrNEWDate[i] = q[i].Item1.ToString("MM/dd/yyyy");
str_arrNEWTime[i] = q[i].Item1.ToString("HH:mm");
str_arrNEWID[i] = q[i].Item2;
// Diagnostic for demonstration purposes
Console.WriteLine("{0}: {1}", q[i].Item1, q[i].Item2);
}
}
private static Tuple<DateTime, string> CreateRecord(string id, int index)
{
return Tuple.Create(arrDates[index] + TimeSpan.Parse(arrTime[index]), id);
}
private static DateTime GetKey(Tuple<DateTime, string> record)
{
return record.Item1;
}
private static DateTime GetGroupKey(IGrouping<DateTime, Tuple<DateTime, string>> group)
{
return group.Key;
}
private static Tuple<DateTime, string> FlattenGroup(IGrouping<DateTime, Tuple<DateTime, string>> group)
{
return Tuple.Create(group.Key, string.Join(",", group.Select(GetId)));
}
private static string GetId(Tuple<DateTime, string> record)
{
return record.Item2;
}
}
As you can see, everywhere that you would normally write a lambda expression, in the above instead we just have a regular static method to do the work. Note that if you were dealing with a non-static class and the method needed to access instance members, the methods can simply be made non-static instead.
Delegate type inference takes care of constructing the delegate instances that need to be passed to the LINQ methods.
Note: while you don't say so explicitly in your question, the nature of the specifications strongly hints that this is homework. As such, you should make very sure you take the time to actually understand the above code example. Otherwise you have learned nothing, wasting your time in the class. Please feel free to ask questions here about my answer, and of course you should feel comfortable asking your teacher to help you understand both the original assignment and this answer.

A better way to fill a two-dimensional array with date and zodiac sign

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.

Categories

Resources