Employee Time Clock Calculation - c#

I am hoping someone can help me with a optimal solution for this. I have a list of time logs. It contains a datatime entry for each clockin per employee per day.
I am trying to come up with a good solution to calculate the time worked per day. The time lapsed between each clock-in and clock-out for one employee.
The final result must give me the following.
Total Time Worked
Total Time Out
The time lapsed between each log / row to extract and show times worked and times out.
For example
ClockIn : 06:00
ClockedOut : 10:00
ClockedIn : 10:15
BreakTime = 00:15
and so on.
I am trying to avoid using to many for loops / foreach loops.
This is a sample piece of code representing what the list of time logs is. The type is. I = for Clock-In and O = Clock-Out. Each Shift has a Block Start Time and Block End Time.
var blockStart = new TimeSpan(0,6,0,0,0);
var blockEnd = new TimeSpan(0,17,0,0);
var listOfTimeLogs = new List<TimeLog>()
{
new TimeLog() {EntryDateTime = new DateTime(2016,05,20,6,0,0),Type = "I"},
new TimeLog() {EntryDateTime = new DateTime(2016,05,20,10,0,0),Type = "O"},
new TimeLog() {EntryDateTime = new DateTime(2016,05,20,10,15,0),Type = "I"},
new TimeLog() {EntryDateTime = new DateTime(2016,05,20,12,0,0),Type = "O"},
new TimeLog() {EntryDateTime = new DateTime(2016,05,20,12,30,0),Type = "I"},
new TimeLog() {EntryDateTime = new DateTime(2016,05,20,15,0,0),Type = "O"},
new TimeLog() {EntryDateTime = new DateTime(2016,05,20,15,15,0),Type = "I"},
new TimeLog() {EntryDateTime = new DateTime(2016,05,20,18,00,0),Type = "O"}
};
Hope this make sense. any help will be appreciated.
Thank you

Computers are made to do loops.
Here is a sample of how I would handle the problem.
Program.cs
using System;
using System.Collections.Generic;
using System.Linq;
namespace TimeClock
{
class Program
{
static void Main()
{
var blockStart = new TimeSpan(0, 6, 0, 0);
var blockEnd = new TimeSpan(0, 17, 0, 0);
var listOfTimeLogs = new List<TimeLog>
{
new TimeLog {EntryDateTime = new DateTime(2016,05,20,6,0,0),EntryType = EntryTypes.In},
new TimeLog {EntryDateTime = new DateTime(2016,05,20,10,0,0),EntryType = EntryTypes.Out},
new TimeLog {EntryDateTime = new DateTime(2016,05,20,10,15,0),EntryType = EntryTypes.In},
new TimeLog {EntryDateTime = new DateTime(2016,05,20,12,0,0),EntryType = EntryTypes.Out},
new TimeLog {EntryDateTime = new DateTime(2016,05,20,12,30,0),EntryType = EntryTypes.In},
new TimeLog {EntryDateTime = new DateTime(2016,05,20,15,0,0),EntryType = EntryTypes.Out},
new TimeLog {EntryDateTime = new DateTime(2016,05,20,15,15,0),EntryType = EntryTypes.In},
new TimeLog {EntryDateTime = new DateTime(2016,05,20,18,00,0),EntryType = EntryTypes.Out}
};
// You are going to have have for / for each unless you use Linq
// fist I would count clock in's versus the out's
var totalIn = listOfTimeLogs.Count(e => e.EntryType == EntryTypes.In);
var totalOut = listOfTimeLogs.Count() - totalIn;
// check if we have in the number of time entries
if (totalIn > totalOut)
{
Console.WriteLine("Employee didn't clock out");
}
// as I was coding this sample program, i thought of another way to store the time
// I would store them in blocks - we have to loop
var timeBlocks = new List<TimeBlock>();
for (var x = 0; x < listOfTimeLogs.Count; x += 2)
{
// create a new WORKING block based on the in/out time entries
timeBlocks.Add(new TimeBlock
{
BlockType = BlockTypes.Working,
In = listOfTimeLogs[x],
Out = listOfTimeLogs[x + 1]
});
// create a BREAK block based on gaps
// check if the next entry in a clock in
var breakOut = x + 2;
if (breakOut < listOfTimeLogs.Count)
{
var breakIn = x + 1;
// create a new BREAK block
timeBlocks.Add(new TimeBlock
{
BlockType = BlockTypes.Break,
In = listOfTimeLogs[breakIn],
Out = listOfTimeLogs[breakOut]
});
}
}
var breakCount = 0;
// here is a loop for displaying detail
foreach (var block in timeBlocks)
{
var lineTitle = block.BlockType.ToString();
// this is me trying to be fancy
if (block.BlockType == BlockTypes.Break)
{
if (block.IsBreak())
{
lineTitle = $"Break #{++breakCount}";
}
else
{
lineTitle = "Lunch";
}
}
Console.WriteLine($" {lineTitle,-10} {block} === Length: {block.Duration.ToString(#"hh\:mm")}");
}
// calculating total time for each block type
var workingTime = timeBlocks.Where(b => b.BlockType == BlockTypes.Working)
.Aggregate(new TimeSpan(0), (p, v) => p.Add(v.Duration));
var breakTime = timeBlocks.Where(b => b.BlockType == BlockTypes.Break)
.Aggregate(new TimeSpan(0), (p, v) => p.Add(v.Duration));
Console.WriteLine($"\nTotal Working Hours: {workingTime.ToString(#"hh\:mm")}");
Console.WriteLine($" Total Break Time: {breakTime.ToString(#"hh\:mm")}");
Console.ReadLine();
}
}
}
TimeBlock.cs
using System;
namespace TimeClock
{
public enum BlockTypes
{
Working,
Break
}
public class TimeBlock
{
public BlockTypes BlockType;
public TimeLog In;
public TimeLog Out;
public TimeSpan Duration
{
get
{
// TODO: Need error checking
return Out.EntryDateTime.Subtract(In.EntryDateTime);
}
}
public override string ToString()
{
return $"In: {In.EntryDateTime:HH:mm} - Out: {Out.EntryDateTime:HH:mm}";
}
}
// a little extension class
public static class Extensions
{
public static bool IsBreak(this TimeBlock block)
{
// if the length of the break period is less than 19 minutes
// we will consider it a break, the person could have clock IN late
return block.Duration.TotalMinutes < 19 ? true : false;
}
}
}
TimeLog.cs
using System;
namespace TimeClock
{
public class TimeLog
{
public DateTime EntryDateTime;
public EntryTypes EntryType;
}
}
EntryTypes.cs
namespace TimeClock
{
public enum EntryTypes
{
In,
Out
}
}

Related

Merge interval with merge distance in C#

Given a list of intervals [start, end] I have to merge them based on a specified merge distance. The intervals are arriving in a particular order, and as they arrive it should merge them according to the specified merge distance as each interval is received. Some of these intervals will be removed (in the arrival stream they will be marked as removed) – in that situation I've to treat the original interval as if it never existed. Example:
Merge distance is 7 – in the following example the input is arriving in order
I've tried the following algorithm to merge them but my Output isn't coming like the above example. Can somebody assists me, what I'm missing here!
Here is my code.
class Program
{
static void Main(string[] args)
{
var intervals = new List<Interval>
{
new Interval
{
start = 1,
end = 20
},
new Interval
{
start = 55,
end = 58
},
new Interval
{
start = 60,
end = 89
},
new Interval
{
start = 15,
end = 31
},
new Interval
{
start = 10,
end = 15
},
new Interval
{
start = 1,
end = 20
}
};
var mergedIntervals = Merge(intervals, 7);
foreach (var item in mergedIntervals)
{
Console.WriteLine($"[{item.start}, {item.end}]");
}
Console.ReadKey();
}
public static List<Interval> Merge(List<Interval> intervals, int mergeDistance)
{
var result = new List<Interval>();
for (int i = 0; i < intervals.Count; i++)
{
var newInterval = new Interval(intervals[i].start, intervals[i].end);
//while (i < intervals.Count - 1 && newInterval.end >= intervals[i + 1].start)
while (i < intervals.Count - 1 && newInterval.end <= mergeDistance) // intervals[i + 1].start)
{
newInterval.end = Math.Max(newInterval.end, intervals[i + 1].end);
i++;
}
result.Add(newInterval);
}
return result;
}
}
public class Interval
{
public int start { get; set; }
public int end { get; set; }
public Interval()
{
}
public Interval(int start, int end)
{
this.start = start;
this.end = end;
}
}
Can you please try this code, working demo code here
class Program
{
static void Main(string[] args)
{
var intervals = new List<Interval>
{
new Interval
{
start = 1,
end = 20,
isAdded = true
},
new Interval
{
start = 55,
end = 58,
isAdded = true
},
new Interval
{
start = 60,
end = 89,
isAdded = true
},
new Interval
{
start = 15,
end = 31,
isAdded = true
},
new Interval
{
start = 10,
end = 15,
isAdded = true
},
new Interval
{
start = 1,
end = 20,
isAdded = false
}
};
var mergedIntervals = Merge(intervals, 7);
foreach (var item in mergedIntervals)
{
Console.WriteLine($"[{item.start}, {item.end}]");
}
Console.ReadKey();
}
public static List<Interval> Merge(List<Interval> intervals, int mergeDistance)
{
var result = new List<IntervalGroup>();
var group = new IntervalGroup();
foreach (var item in intervals)
{
group = result.Where(c => c.Groups.Any(g =>
Math.Abs(g.end - item.start) <= mergeDistance ||
Math.Abs(g.end - item.end) <= mergeDistance)).FirstOrDefault();
if (group != null && item.isAdded)
{
group.Groups.Add(item);
}
else if(item.isAdded)
{
group = new IntervalGroup();
group.Groups = new List<Interval>();
result.Add(group);
group.Groups.Add(item);
}
else if(item.isAdded == false)
{
group.Groups.Remove(group.Groups.Where(c => c.start == item.start && c.end == item.end).First());
}
}
var finalResult = result.Select(s => new Interval { start = s.Groups.Min(min => min.start), end = s.Groups.Max(min => min.end) });
return finalResult.ToList();
}
}
public class Interval
{
public int start { get; set; }
public int end { get; set; }
public bool isAdded { get; set; }
public Interval()
{
}
public Interval(int start, int end, bool isAdded)
{
this.start = start;
this.end = end;
this.isAdded = isAdded;
}
}
public class IntervalGroup
{
public List<Interval> Groups { get; set; }
}
When a problem is beyond your grasp, it is very helpful to break it up into smaller pieces and code the bits that you do understand. Here's how I break it down, step by step.
First, I think it would be convenient to be able to check the length of an interval, so let's add a property.
class Interval
{
/* Prior code */
public int Length => this.end - this.start;
Now let's write a method that merges two intervals:
class Interval
{
/* Prior code */
static public Interval Merge(Interval a, Interval b )
{
return new Interval(Math.Min(a.start, b.start), Math.Max(a.end, b.end));
}
Now we need to write the code that decides if two intervals are capable of being merged. The prototype could look like this:
static public bool CanMerge(Interval a, Interval b, int mergeDistance)
What logic do we need inside? Well, we could check for overlaps and check the merge distance from both ends, but I know a shortcut. Given a merge A + B = C, the merge is allowed if and only if the length of C is less than or equal to the sum of A + B + the merge distance. So we can write this:
class Interval
{
/* Prior code */
static public bool CanMerge(Interval a, Interval b, int mergeDistance)
{
var merged = Merge(a,b);
var canMerge = merged.Length <= a.Length + b.Length + mergeDistance;
return canMerge;
}
From there you can add to a list by checking for mergeable items. Note that recursion is required because the act of merging an interval could result in another interval becoming mergeable.
void AddToList(List<Interval> list, Interval newInterval, int mergeDistance)
{
var target = list.FirstOrDefault( x => Interval.CanMerge(x, newInterval, mergeDistance) );
if (target == null)
{
list.Add(newInterval);
return;
}
list.Remove(target);
AddToList(list, Interval.Merge(target, newInterval), mergeDistance);
}

Get two teams greatest lead throughout game

I have a Game that involves 2 teams. The Game has a List of ScoreEvents. Each ScoreEvent is a 1 point for the Team that scored. I need to know what the Max lead score was for each team (0 if they never had the lead). The ScoreEvents List is ordered by TimeSinceStart.
public class ScoreEvent
{
public int TeamId { get; set; }
public TimeSpan TimeSinceStart { get; set; }
}
public void GetMaxScoreLead()
{
var ScoreEvents = new List<ScoreEvent>
{
new ScoreEvent { TeamId = 0, TimeSinceStart = new TimeSpan(100)},
new ScoreEvent { TeamId = 0, TimeSinceStart = new TimeSpan(200)},
new ScoreEvent { TeamId = 0, TimeSinceStart = new TimeSpan(300)},
//Score at 300 ticks is 3-0 to TeamdId = 0
new ScoreEvent { TeamId = 1, TimeSinceStart = new TimeSpan(400)},
new ScoreEvent { TeamId = 1, TimeSinceStart = new TimeSpan(500)},
new ScoreEvent { TeamId = 1, TimeSinceStart = new TimeSpan(600)},
new ScoreEvent { TeamId = 1, TimeSinceStart = new TimeSpan(700)},
//Score at 700 ticks is a 3-4 to TeamId = 1
new ScoreEvent { TeamId = 0, TimeSinceStart = new TimeSpan(800)},
new ScoreEvent { TeamId = 0, TimeSinceStart = new TimeSpan(900)},
new ScoreEvent { TeamId = 0, TimeSinceStart = new TimeSpan(1000)},
new ScoreEvent { TeamId = 0, TimeSinceStart = new TimeSpan(1100)}
//Score at 1100 ticks is 7-4 to TeamId 0
};
}
So for the example above the answers for greatest lead per team would be:
TeamId (0) = 3 greatest lead
TeamId (1) = 1 greatest lead
EDIT: Code that I've got to. I know I need to keep track of the current score somewhere.
var teamZeroLargestLead = 0;
var teamOneLargestLead = 0;
var internalTeamZeroLargestLead = 0;
var internalTeamOneLargestLead = 0;
foreach (var scoreEvent in scoreEvents.OrderBy(x => x.TimeSinceStart))
{
if (scoreEvent.TeamId == 0)
{
if (internalTeamOneLargestLead > teamOneLargestLead)
{
teamOneLargestLead = internalTeamOneLargestLead;
internalTeamOneLargestLead = 0;
}
internalTeamZeroLargestLead += 1;
}
else
{
if(internalTeamZeroLargestLead > teamZeroLargestLead)
{
teamZeroLargestLead = internalTeamZeroLargestLead;
internalTeamZeroLargestLead = 0;
}
internalTeamOneLargestLead += 1;
}
}
I've slightly updated and simplified your algorithm with foreach loop and now it returns the correct result - teamZeroLead is 3, teamOneLead is 1.
var teamZeroLead = 0;
var teamOneLead = 0;
var teamZeroScore = 0;
var teamOneScore = 0;
foreach (var scoreEvent in scoreEvents.OrderBy(x => x.TimeSinceStart))
{
if (scoreEvent.TeamId == 0)
{
teamZeroScore++;
teamZeroLead = Math.Max(teamZeroLead, teamZeroScore - teamOneScore);
}
else
{
teamOneScore++;
teamOneLead = Math.Max(teamOneLead, teamOneScore - teamZeroScore);
}
}
At every loop iteration you are calculating the current score of every team, then calculate the lead value and assign it to the result value, if it's greater then previously calculated.
The same logic can be written using Aggregate method and value tuple, you can choose what is more readable and convenient for you
var result = scoreEvents.Aggregate((teamZeroLead: 0, teamOneLead: 0, teamZeroScore: 0, teamOneScore: 0),
(scores, scoreEvent) =>
{
if (scoreEvent.TeamId == 0)
{
scores.teamZeroScore++;
scores.teamZeroLead = Math.Max(scores.teamZeroLead, scores.teamZeroScore - scores.teamOneScore);
}
else
{
scores.teamOneScore++;
scores.teamOneLead = Math.Max(scores.teamOneLead, scores.teamOneScore - scores.teamZeroScore);
}
return scores;
});
After execution you can get the result values using result.teamZeroLead and result.teamOneLead
var leftTeamId = ScoreEvents.First().TeamId
var res = ScoreEvents
.OrderBy(x => x.TimeSinceStart)
.Aggregate(
(max: 0, min: 0, curr: 0),
(acc, currSE) => {
var curr = currSE.TeamId == leftTeamId
? acc.curr +1
: acc.curr - 1;
if(curr > acc.max)
{
return (curr, acc.min, curr);
}
else if (curr < acc.min)
{
return (acc.max, curr, curr);
}
return (acc.max, acc.min, curr);
});
And for "left" team with id leftTeamId you use res.max for "right" team you use Math.Abs(res.min):
TeamId (0) = res.max greatest lead
TeamId (1) = Math.Abs(res.min) greatest lead
I did not get rightTeamId, cause in theory only one team could have scored (but assumed that at least one did =).
Since there are only two teams see if this approach satisfies your needs.
int team1Score = 0;
int team2Score = 0;
int maximumLead = 0;
int maximumLeadTeamId = -1;
for (int i = 0; i < ScoreEvents.Count; i++)
{
if (ScoreEvents[i].TeamId == 0)
{
team1Score++;
}
else
{
team2Score++;
}
int currentLead = Math.Abs(team1Score - team2Score);
if (currentLead > maximumLead)
{
maximumLead = currentLead;
maximumLeadTeamId = ScoreEvents[i].TeamId;
}
}
maximumLeadTeamId is the Id of the team with maximum lead throughout the game and maximumLead is the maximum goals difference between the two teams.
You can use this to get a dictionary of team id to score at the specified timespan:
public static Dictionary<int, int> GetMaxScoreLead(IEnumerable<ScoreEvent> scoreEvents, TimeSpan time)
{
var scoreDictionary = new Dictionary<int, int>();
var grouping = scoreEvents.Where(e => e.TimeSinceStart <= time).GroupBy(e => e.TeamId);
foreach (var group in grouping)
{
scoreDictionary.Add(group.Key, group.Count());
}
return scoreDictionary;
}
This code does not depend on the number of teams. You can trivially determine the winning team from this dictionary structure. For example:
var winningTeam = getScores.OrderByDescending(x => x.Value).FirstOrDefault();

Adding subsequent months on x-axis using Live Charts

private int SumOfNodes()
{
ManufacturingDataModel MDM = new ManufacturingDataModel();
Test t = new Test(MDM);
List<Hardware> SumOfHardware = t.GetHardware().FindAll(x => x.Nodes != 0);
int SumOfNodes = 0;
foreach (Hardware i in SumOfHardware )
{
SumOfNodes += i.Nodes;
}
return SumOfNodes;
}
private int SumOfRepeaters()
{
ManufacturingDataModel MDM = new ManufacturingDataModel();
Test t = new Test(MDM);
List<Hardware> SumOfHardware = t.GetHardware().FindAll(x => x.Repeaters != 0);
int SumOfRepeaters = 0;
foreach (Hardware i in SumOfHardware)
{
SumOfRepeaters += i.Repeaters;
}
return SumOfRepeaters;
}
private int SumOfHubs()
{
ManufacturingDataModel MDM = new ManufacturingDataModel();
Test t = new Test(MDM);
List<Hardware> SumOfHardware = t.GetHardware().FindAll(x => x.Hubs != 0);
int SumOfHubs= 0;
foreach (Hardware i in SumOfHardware)
{
SumOfHubs += i.Hubs;
}
return SumOfHubs;
}
private string Month()
{
DateTime now = DateTime.Now;
string month = DateTime.Now.ToString("MMMM");
return month;
}
private void DisplayData()
{
SeriesCollection = new SeriesCollection
{
new ColumnSeries
{
Title = "Nodes",
Values = new ChartValues<int> { SumOfNodes() }
},
};
SeriesCollection.Add
(
new ColumnSeries
{
Title = "Repeaters",
Values = new ChartValues<int> { SumOfRepeaters() }
}
);
SeriesCollection.Add
(
new ColumnSeries
{
Title = "Hubs",
Values = new ChartValues<int> { SumOfHubs() }
}
);
Labels = new[] { Month() };
Formatter = value => value.ToString("N");
DataContext = this;
}
enter image description here
At this points I've managed to create an app that adds/removes and updates my items on my database. I'm also planning to add some stats (Started off with graph visualisation) but I'm facing an issue.
I want to seperate columns based on months. So for example as seen by the image attached no matter how many items i add , remove or update the total amount for each item is added to Decemeber. But when January comes any newly added modification to the quantity of my items I would like to see adjacent to the Decemeber one.
P.S.: There is alot of code repitition which will accounted for later on.
I've managed to put something together regarding my answer above which may be usefull for someone in the future
What I've done was to
1) Get my data from my db.
2) Find the Month of interest using LINQ queries **
instead of selecting the items (i.e repeaters, nodes)
**
3) By doing so it also accounts for the items I'm interested in during the month of interest.
4) Do an incremental foreach loop as shown in my code above.
5) Change the methodd i want to display (i.e DisplayData to DisplayDecemberData) and use in the same way as above.
6) Create further methods for the subsequent months and just add the additional information in the ChartValues object.
See Below
private int DecemberNodes()
{
ManufacturingDataModel MDM = new ManufacturingDataModel();
Test t = new Test(MDM);
List<Hardware> decembernodes = t.GetHardware().FindAll(x => x.Date.Month==12);
int DecemberSumOfNodes = 0;
foreach (Hardware i in decembernodes)
{
DecemberSumOfNodes += i.Nodes;
}
return DecemberSumOfNodes;
}
private int JanuaryNodes()
{
ManufacturingDataModel MDM = new ManufacturingDataModel();
Test t = new Test(MDM);
List<Hardware> januarynodes = t.GetHardware().FindAll(x => x.Date.Month==01);
int JanuarySumOfNodes = 0;
foreach (Hardware i in januarynodes)
{
JanuarySumOfNodes += i.Nodes;
}
private void DisplayDecemberData()
{
SeriesCollection = new SeriesCollection
{
new ColumnSeries
{
Title = "Nodes",
Values = new ChartValues<int> { DecemberNodes() }
},
};
private void DisplayJanuaryData()
{
SeriesCollection = new SeriesCollection
{
new ColumnSeries
{
Title = "Nodes",
**Values = new ChartValues<int> { DecemberNodes(), JanuaryNodes() }**
},
};
There is some code repetition and I'm pretty sure there is a more code concise way of actually doing it but for now this seems to do the trick. When I simplify the code i will post it.

Get the positions of unique elements in a string[]

I have an xml file that I am accessing to create a report of time spent on a project. I'm returning the unique dates to a label created dynamically on a winform and would like to compile the time spent on a project for each unique date. I have been able to return all of the projects under each date or only one project. Currently I'm stuck on only returning one project. Can anyone please help me?? This is what the data should look like if it's correct.
04/11/15
26820 2.25
27111 8.00
04/12/15
26820 8.00
04/13/15
01det 4.33
26820 1.33
27225 4.25
etc.
This is how I'm retrieving the data
string[] weekDateString = elementDateWeekstring();
string[] uniqueDates = null;
string[] weeklyJobNumber = elementJobNumWeek();
string[] weeklyTicks = elementTicksWeek();
This is how I'm getting the unique dates.
IEnumerable<string> distinctWeekDateIE = weekDateString.Distinct();
foreach (string d in distinctWeekDateIE)
{
uniqueDates = distinctWeekDateIE.ToArray();
}
And this is how I'm creating the labels.
try
{
int dateCount;
dateCount = uniqueDates.Length;
Label[] lblDate = new Label[dateCount];
int htDate = 1;
int padDate = 10;
for (int i = 0; i < dateCount; i++ )
{
lblDate[i] = new Label();
lblDate[i].Name = uniqueDates[i].Trim('\r');
lblDate[i].Text = uniqueDates[i];
lblDate[i].TabIndex = i;
lblDate[i].Bounds = new Rectangle(18, 275 + padDate + htDate, 75, 22);
targetForm.Controls.Add(lblDate[i]);
htDate += 22;
foreach (string x in uniqueDates)
{
int[] posJobNumber;
posJobNumber = weekDateString.Select((b, a) => b == uniqueDates[i].ToString() ? a : -1).Where(a => a != -1).ToArray();
for (int pjn = 0; pjn < posJobNumber.Length; pjn++)
{
if (x.Equals(lblDate[i].Text))
{
Label lblJobNum = new Label();
int htJobNum = 1;
int padJobNum = 10;
lblJobNum.Name = weeklyJobNumber[i];
lblJobNum.Text = weeklyJobNumber[i];
lblJobNum.Bounds = new Rectangle(100, 295 + padJobNum + htJobNum, 75, 22);
targetForm.Controls.Add(lblJobNum);
htJobNum += 22;
htDate += 22;
padJobNum += 22;
}
}
}
}
}
I've been stuck on this for about 3 months. Is there anyone that can describe to me why I'm not able to properly retrieve the job numbers that are associated with a particular date. I don't believe that these are specifically being returned as dates. Just a string that looks like a date.
I really appreciate any help I can get. I'm just completely baffled. Thank you for any responses in advance. I truly appreciate the assistance.
EDIT: #Sayka - Here is the xml sample.
<?xml version="1.0" encoding="utf-8"?>
<Form1>
<Name Key="4/21/2014 6:51:17 AM">
<Date>4/21/2014</Date>
<JobNum>26820</JobNum>
<RevNum>00000</RevNum>
<Task>Modeling Secondary</Task>
<Start>06:51 AM</Start>
<End>04:27 PM</End>
<TotalTime>345945089017</TotalTime>
</Name>
<Name Key="4/22/2014 5:44:22 AM">
<Date>4/22/2014</Date>
<JobNum>26820</JobNum>
<RevNum>00000</RevNum>
<Task>Modeling Secondary</Task>
<Start>05:44 AM</Start>
<End>06:56 AM</End>
<TotalTime>43514201221</TotalTime>
</Name>
<Name Key="4/22/2014 6:57:02 AM">
<Date>4/22/2014</Date>
<JobNum>02e-n-g</JobNum>
<RevNum>00000</RevNum>
<Task>NET Eng</Task>
<Start>06:57 AM</Start>
<End>07:16 AM</End>
<TotalTime>11706118875</TotalTime>
</Name>
....
</Form1>
This is how I'm getting the information out of the xml file and returning a string[].
public static string[] elementDateWeekstring()
{
//string datetxtWeek = "";
XmlDocument xmldoc = new XmlDocument();
fileExistsWeek(xmldoc);
XmlNodeList nodeDate = xmldoc.GetElementsByTagName("Date");
int countTicks = 0;
string[] dateTxtWeek = new string[nodeDate.Count];
for (int i = 0; i < nodeDate.Count; i++)
{
dateTxtWeek[i] = nodeDate[i].InnerText;
countTicks++;
}
return dateTxtWeek;
}
Job number and Ticks are returned in a similar fashion. I've been able to reuse these snippets throught out the code. This is a one dimensional xml file?? It will always return a position for a jobnumber that equates to a date or Ticks. I will never have more or less of any one element.
You can use Linq-to-XML to parse the XML file, and then use Linq-to-objects to group (and order) the data by job date and order each group by job name.
The code to parse the XML file is like so:
var doc = XDocument.Load(filename);
var jobs = doc.Descendants("Name");
// Extract the date, job number, and total time from each "Name" element.:
var data = jobs.Select(job => new
{
Date = (DateTime)job.Element("Date"),
Number = (string)job.Element("JobNum"),
Duration = TimeSpan.FromTicks((long)job.Element("TotalTime"))
});
The code to group and order the jobs by date and order the groups by job name is:
var result =
data.GroupBy(job => job.Date).OrderBy(g => g.Key)
.Select(g => new
{
Date = g.Key,
Jobs = g.OrderBy(item => item.Number)
});
Then you can access the data by iterating over each group in result and then iterate over each job in the group, like so:
foreach (var jobsOnDate in result)
{
Console.WriteLine("{0:d}", jobsOnDate.Date);
foreach (var job in jobsOnDate.Jobs)
Console.WriteLine(" {0} {1:hh\\:mm}", job.Number, job.Duration);
}
Putting this all together in a sample compilable console application (substitute the filename for the XML file as appropriate):
using System;
using System.Linq;
using System.Xml.Linq;
namespace ConsoleApplication2
{
class Program
{
private static void Main()
{
string filename = #"d:\test\test.xml"; // Substitute your own filename here.
// Open XML file and get a collection of each "Name" element.
var doc = XDocument.Load(filename);
var jobs = doc.Descendants("Name");
// Extract the date, job number, and total time from each "Name" element.:
var data = jobs.Select(job => new
{
Date = (DateTime)job.Element("Date"),
Number = (string)job.Element("JobNum"),
Duration = TimeSpan.FromTicks((long)job.Element("TotalTime"))
});
// Group the jobs by date, and order the groups by job name:
var result =
data.GroupBy(job => job.Date).OrderBy(g => g.Key)
.Select(g => new
{
Date = g.Key,
Jobs = g.OrderBy(item => item.Number)
});
// Print out the results:
foreach (var jobsOnDate in result)
{
Console.WriteLine("{0:d}", jobsOnDate.Date);
foreach (var job in jobsOnDate.Jobs)
Console.WriteLine(" {0} {1:hh\\:mm}", job.Number, job.Duration);
}
}
}
}
The output is like this
Create a new project
Set form size bigger.
Apply these codes.
Set the location for your XML file.
Namespaces
using System.Xml;
using System.IO;
Form Code
public partial class Form1 : Form
{
const string XML_FILE_NAME = "D:\\emps.txt";
public Form1()
{
InitializeComponent();
}
private void Form1_Load(object sender, EventArgs e)
{
prepareDataGrid();
List<JOBS> jobsList = prepareXML(XML_FILE_NAME);
for (int i = 0; i < jobsList.Count; i++)
{
addDateRow(jobsList[i].jobDate.ToString("M'/'d'/'yyyy"));
for (int j = 0; j < jobsList[i].jobDetailsList.Count; j++)
dgv.Rows.Add(new string[] {
jobsList[i].jobDetailsList[j].JobNumber,
jobsList[i].jobDetailsList[j].JobHours
});
}
}
DataGridView dgv;
void prepareDataGrid()
{
dgv = new DataGridView();
dgv.BackgroundColor = Color.White;
dgv.GridColor = Color.White;
dgv.DefaultCellStyle.SelectionBackColor = Color.White;
dgv.DefaultCellStyle.SelectionForeColor = Color.Black;
dgv.DefaultCellStyle.ForeColor = Color.Black;
dgv.DefaultCellStyle.BackColor = Color.White;
dgv.DefaultCellStyle.Alignment = DataGridViewContentAlignment.MiddleRight;
dgv.Width = 600;
dgv.Dock = DockStyle.Left;
this.BackColor = Color.White;
dgv.Columns.Add("Col1", "Col1");
dgv.Columns.Add("Col2", "Col2");
dgv.Columns[0].Width = 110;
dgv.Columns[1].Width = 40;
dgv.DefaultCellStyle.Font = new System.Drawing.Font("Segoe UI", 10);
dgv.RowHeadersVisible = dgv.ColumnHeadersVisible = false;
dgv.AllowUserToAddRows =
dgv.AllowUserToDeleteRows =
dgv.AllowUserToOrderColumns =
dgv.AllowUserToResizeColumns =
dgv.AllowUserToResizeRows =
!(dgv.ReadOnly = true);
Controls.Add(dgv);
}
void addJobRow(string jobNum, string jobHours)
{
dgv.Rows.Add(new string[] {jobNum, jobHours });
}
void addDateRow(string date)
{
dgv.Rows.Add(new string[] { date, ""});
dgv.Rows[dgv.Rows.Count - 1].DefaultCellStyle.SelectionForeColor =
dgv.Rows[dgv.Rows.Count - 1].DefaultCellStyle.ForeColor = Color.Firebrick;
dgv.Rows[dgv.Rows.Count - 1].DefaultCellStyle.Font = new Font("Segoe UI Light", 13.5F);
dgv.Rows[dgv.Rows.Count - 1].DefaultCellStyle.Alignment = DataGridViewContentAlignment.MiddleLeft;
dgv.Rows[dgv.Rows.Count - 1].Height = 25;
}
List<JOBS> prepareXML(string fileName)
{
string xmlContent = "";
using (FileStream fs = new FileStream(fileName, FileMode.Open, FileAccess.Read))
using (StreamReader sr = new StreamReader(fs)) xmlContent = sr.ReadToEnd();
XmlDocument doc = new XmlDocument();
doc.LoadXml(xmlContent);
List<JOBS> jobsList = new List<JOBS>();
XmlNode form1Node = doc.ChildNodes[1];
for (int i = 0; i < form1Node.ChildNodes.Count; i++)
{
XmlNode dateNode = form1Node.ChildNodes[i].ChildNodes[0].ChildNodes[0],
jobNumNode = form1Node.ChildNodes[i].ChildNodes[1].ChildNodes[0],
timeTicksNode = form1Node.ChildNodes[i].ChildNodes[6].ChildNodes[0];
bool foundDate = false;
for (int j = 0; j < jobsList.Count; j++) if (jobsList[j].compareDate(dateNode.Value))
{
jobsList[j].addJob(jobNumNode.Value, Math.Round(TimeSpan.FromTicks(
(long)Convert.ToDouble(timeTicksNode.Value)).TotalHours, 2).ToString());
foundDate = true;
break;
}
if (!foundDate)
{
JOBS job = new JOBS(dateNode.Value);
string jbnum = jobNumNode.Value;
string tbtck = timeTicksNode.Value;
long tktk = Convert.ToInt64(tbtck);
double tkdb = TimeSpan.FromTicks(tktk).TotalHours;
job.addJob(jobNumNode.Value, Math.Round(TimeSpan.FromTicks(
Convert.ToInt64(timeTicksNode.Value)).TotalHours, 2).ToString());
jobsList.Add(job);
}
}
jobsList.OrderByDescending(x => x.jobDate);
return jobsList;
}
class JOBS
{
public DateTime jobDate;
public List<JobDetails> jobDetailsList = new List<JobDetails>();
public void addJob(string jobNumber, string jobHours)
{
jobDetailsList.Add(new JobDetails() { JobHours = jobHours, JobNumber = jobNumber });
}
public JOBS(string dateString)
{
jobDate = getDateFromString(dateString);
}
public JOBS() { }
public bool compareDate(string dateString)
{
return getDateFromString(dateString) == jobDate;
}
private DateTime getDateFromString(string dateString)
{
string[] vals = dateString.Split('/');
return new DateTime(Convert.ToInt32(vals[2]), Convert.ToInt32(vals[0]), Convert.ToInt32(vals[1]));
}
}
class JobDetails
{
public string JobNumber { get; set; }
public string JobHours { get; set; }
}
}

Determining value jumps in List<T>

I have a class:
public class ShipmentInformation
{
public string OuterNo { get; set; }
public long Start { get; set; }
public long End { get; set; }
}
I have a List<ShipmentInformation> variable called Results.
I then do:
List<ShipmentInformation> FinalResults = new List<ShipmentInformation>();
var OuterNumbers = Results.GroupBy(x => x.OuterNo);
foreach(var item in OuterNumbers)
{
var orderedData = item.OrderBy(x => x.Start);
ShipmentInformation shipment = new ShipmentInformation();
shipment.OuterNo = item.Key;
shipment.Start = orderedData.First().Start;
shipment.End = orderedData.Last().End;
FinalResults.Add(shipment);
}
The issue I have now is that within each grouped item I have various ShipmentInformation but the Start number may not be sequential by x. x can be 300 or 200 based on a incoming parameter. To illustrate I could have
Start = 1, End = 300
Start = 301, End = 600
Start = 601, End = 900
Start = 1201, End = 1500
Start = 1501, End = 1800
Because I have this jump I cannot use the above loop to create an instance of ShipmentInformation and take the first and last item in orderedData to use their data to populate that instance.
I would like some way of identifying a jump by 300 or 200 and creating an instance of ShipmentInformation to add to FinalResults where the data is sequnetial.
Using the above example I would have 2 instances of ShipmentInformation with a Start of 1 and an End of 900 and another with a Start of 1201 and End of 1800
Try the following:
private static IEnumerable<ShipmentInformation> Compress(IEnumerable<ShipmentInformation> shipments)
{
var orderedData = shipments.OrderBy(s => s.OuterNo).ThenBy(s => s.Start);
using (var enumerator = orderedData.GetEnumerator())
{
ShipmentInformation compressed = null;
while (enumerator.MoveNext())
{
var current = enumerator.Current;
if (compressed == null)
{
compressed = current;
continue;
}
if (compressed.OuterNo != current.OuterNo || compressed.End < current.Start - 1)
{
yield return compressed;
compressed = current;
continue;
}
compressed.End = current.End;
}
if (compressed != null)
{
yield return compressed;
}
}
}
Useable like so:
var finalResults = Results.SelectMany(Compress).ToList();
If you want something that probably has terrible performance and is impossible to understand, but only uses out-of-the box LINQ, I think this might do it.
var orderedData = item.OrderBy(x => x.Start);
orderedData
.SelectMany(x =>
Enumerable
.Range(x.Start, 1 + x.End - x.Start)
.Select(n => new { time = n, info = x))
.Select((x, i) => new { index = i, time = x.time, info = x.info } )
.GroupBy(t => t.time - t.info)
.Select(g => new ShipmentInformation {
OuterNo = g.First().Key,
Start = g.First().Start(),
End = g.Last().End });
My brain hurts.
(Edit for clarity: this just replaces what goes inside your foreach loop. You can make it even more horrible by putting this inside a Select statement to replace the foreach loop, like in rich's answer.)
How about this?
List<ShipmentInfo> si = new List<ShipmentInfo>();
si.Add(new ShipmentInfo(orderedData.First()));
for (int index = 1; index < orderedData.Count(); ++index)
{
if (orderedData.ElementAt(index).Start ==
(si.ElementAt(si.Count() - 1).End + 1))
{
si[si.Count() - 1].End = orderedData.ElementAt(index).End;
}
else
{
si.Add(new ShipmentInfo(orderedData.ElementAt(index)));
}
}
FinalResults.AddRange(si);
Another LINQ solution would be to use the Except extension method.
EDIT: Rewritten in C#, includes composing the missing points back into Ranges:
class Program
{
static void Main(string[] args)
{
Range[] l_ranges = new Range[] {
new Range() { Start = 10, End = 19 },
new Range() { Start = 20, End = 29 },
new Range() { Start = 40, End = 49 },
new Range() { Start = 50, End = 59 }
};
var l_flattenedRanges =
from l_range in l_ranges
from l_point in Enumerable.Range(l_range.Start, 1 + l_range.End - l_range.Start)
select l_point;
var l_min = 0;
var l_max = l_flattenedRanges.Max();
var l_allPoints =
Enumerable.Range(l_min, 1 + l_max - l_min);
var l_missingPoints =
l_allPoints.Except(l_flattenedRanges);
var l_lastRange = new Range() { Start = l_missingPoints.Min(), End = l_missingPoints.Min() };
var l_missingRanges = new List<Range>();
l_missingPoints.ToList<int>().ForEach(delegate(int i)
{
if (i > l_lastRange.End + 1)
{
l_missingRanges.Add(l_lastRange);
l_lastRange = new Range() { Start = i, End = i };
}
else
{
l_lastRange.End = i;
}
});
l_missingRanges.Add(l_lastRange);
foreach (Range l_missingRange in l_missingRanges) {
Console.WriteLine("Start = " + l_missingRange.Start + " End = " + l_missingRange.End);
}
Console.ReadKey(true);
}
}
class Range
{
public int Start { get; set; }
public int End { get; set; }
}

Categories