So basically I got the following assignment:
Returns the number of open count time in the given list of appointments.
Given appointments [9:30-10:00, 12:00-13:00, 15:15-16:30]
the result should be 4
[8:00-9:30, 10:00-12:00, 13:00-15:15, 16:30-17:00].
name="appointments">The list of current appointments
The number of open count time in the given list of appointments
What is the best way to check for open slots, without using a lot of IF statements?
This is what I got so far:
public int count (Appointment[] appointments)
{
int openslots = 0;
foreach (var t in appointments)
{
if (t.End > t.Start )
{
openslots++;
}
}
return openslots;
}
NOTE: "Appointmens[]" contains a list with all the appointments.
UPDATE 2: This is what I got so far:
Sometimes it helps to re-frame the problem in a way that makes it easier to tackle, and make sure you haven't missed anything. It can also help to draw a representation of the problem, depending on how visual you are - I think pictures make time range problems easier to grasp.
Given a time range [Tstart = 08:00, Tend = 17:00] and a list of non-overlapping sub-ranges (appointments), find the number of sub-ranges that are not covered by any range provided.
From the provided times we can draw a rough visualization of the plan like this:
8 9 10 11 12 13 14 15 16 17
+---+---+---+---+---+---+---+---+---+
| ■■ ■■■■ ■■■■■ |
Visually the breaks in the used time are clear, giving 4 breaks. If all of the possible appointment lists mapped out like the above then it'd be simple to calculate the number of open blocks as appointments.Length + 1. Here are some other options that don't fit that calculation:
8 9 10 11 12 13 14 15 16 17 Appointments to Gaps
+---+---+---+---+---+---+---+---+---+
■■■■■■■■ ■■■■ ■■■■■ | 3 : 3
| ■■■ ■■■■ ■■■■■■■■ 3 : 3
■■■■■■■■ ■■■■■■■■ 2 : 1
■■■■■■■■|■■■■■■■■■|■■■■■■■■|■■■■■■■■■ 4 : 0
We could go on and test edge cases for days, including how we deal with overlapping appointments, but a better option would be to scan the list and see what we get.
From your code it looks like your Appointment class defines a time range between Start and End. If we step through the list, tracking the end of the previous range, we can enumerate through the open times for the day like this:
static TimeSpan DayStart = DateTime.Parse("08:00");
static TimeSpan DayEnd = DateTime.Parse("17:00");
int CountOpenTimeSlots(Appointment[] appointments)
{
int count = 0;
// Tracking variable for the end of the last closed slot.
var previousEnd = DayStart;
foreach (var appointment in Appointments)
{
// check the length of time between previous end and current start
var openTime = appointment.Start - previousEnd;
if (openTime.TotalMinutes > 0)
count++;
// update tracking
previousEnd = appointment.End;
}
// Finally, check for open slot at the end of the day
if ((DayEnd - previousEnd).TotalMinutes > 0)
count++;
return count;
}
This assumes that the appointment list is already sorted, and that none of the appointments end before the start of the day or start after the end of the day, and will fail if you have any total overlaps (one appointment completely inside another).
The above can be fairly easily converted to actually generate a list of open timeslots instead of just counting them. And we can make it a little more general, allowing for specification of the work range, filter out some bad inputs, etc.
class TimeRange { public TimeSpan Start, End; }
IEnumerable<TimeRange> EnumerateOpenSlots(TimeSpan DayStart, TimeSpan DayEnd, Appointment[] appointments)
{
// clean up source list
var filtered = appointments
.Where(o => o.End >= DayStart && o.Start < DayEnd)
.OrderBy(o => o.Start);
var previousEnd = DayStart;
foreach (var range in filtered)
{
// skip total overlaps
if (range.End < previousEnd)
continue;
var openTime = range.Start - previousEnd;
if (openTime.TotalMinutes > 0)
yield return new TimeRange { Start = previousEnd, End = range.Start };
previousEnd = range.End;
}
if ((DayEnd - previousEnd).TotalMinutes > 0)
yield return new TimeRange { Start = previousEnd, End = DayEnd };
}
Now we can get the count using LINQ extensions:
int CountOpenTimeSlots(TimeRange[] appointments)
=> EnumerateOpenSlots(DayStart, DayEnd, appointsments).Count();
Depends on what you mean by "best". If, for instance, you mean concise, perhaps:
appointments.Count(t => t.End > t.Start)
Sometimes I find it's useful to have a class or struct that does the hard lifting, that you write once, but use over and over again to make these kinds of calculations easy.
Here's my Period struct that does that:
private struct Period : IEquatable<Period>
{
public DateTime StartTime { get; private set; }
public DateTime EndTime { get; private set; }
public Period(DateTime startTime, DateTime endTime)
{
this.StartTime = startTime;
this.EndTime = endTime;
}
public override bool Equals(object obj)
{
if (obj is Period)
return Equals((Period)obj);
return false;
}
public bool Equals(Period obj)
{
if (!EqualityComparer<DateTime>.Default.Equals(this.StartTime, obj.StartTime))
return false;
if (!EqualityComparer<DateTime>.Default.Equals(this.EndTime, obj.EndTime))
return false;
return true;
}
public override int GetHashCode()
{
int hash = 0;
hash ^= EqualityComparer<DateTime>.Default.GetHashCode(this.StartTime);
hash ^= EqualityComparer<DateTime>.Default.GetHashCode(this.EndTime);
return hash;
}
public override string ToString()
{
return $"{{ StartTime = {this.StartTime}, EndTime = {this.EndTime} }}";
}
public IEnumerable<Period> Fragment(Period that)
{
if (this.StartTime < that.StartTime)
{
if (this.EndTime <= that.StartTime)
{
yield return this;
yield return that;
}
else if (this.EndTime < that.EndTime)
{
yield return new Period(this.StartTime, that.StartTime);
yield return new Period(that.StartTime, this.EndTime);
yield return new Period(this.EndTime, that.EndTime);
}
else if (this.EndTime == that.EndTime)
{
yield return new Period(this.StartTime, that.StartTime);
yield return that;
}
else if (this.EndTime > that.EndTime)
{
yield return new Period(this.StartTime, that.StartTime);
yield return that;
yield return new Period(that.EndTime, this.EndTime);
}
}
else if (this.StartTime == that.StartTime)
{
if (this.EndTime < that.EndTime)
{
yield return this;
yield return new Period(this.EndTime, that.EndTime);
}
else if (this.EndTime == that.EndTime)
{
yield return this;
}
else if (this.EndTime > that.EndTime)
{
yield return that;
yield return new Period(that.EndTime, this.EndTime);
}
}
else if (this.StartTime < that.EndTime)
{
if (this.EndTime < that.EndTime)
{
yield return new Period(that.StartTime, this.StartTime);
yield return this;
yield return new Period(this.EndTime, that.EndTime);
}
else if (this.EndTime == that.EndTime)
{
yield return new Period(that.StartTime, this.StartTime);
yield return this;
}
else if (this.EndTime > that.EndTime)
{
yield return new Period(that.StartTime, this.StartTime);
yield return new Period(this.StartTime, that.EndTime);
yield return new Period(that.EndTime, this.EndTime);
}
}
else // (this.StartTime >= that.EndTime)
{
yield return that;
yield return this;
}
}
}
It's job is to represent a period in time and to be able to fragment two periods together.
For example, if I write this code:
DateTime today = DateTime.Now.Date;
var p1 = new Period(today.AddHours(9.0), today.AddHours(10.0));
var p2 = new Period(today.AddHours(9.5), today.AddHours(10.5));
var fragments = p1.Fragment(p2);
Then I get out three periods 09:00-09:30, 09:30-10:00, 10:00-10:30.
Now it's easy to represent your appointments and your full day as periods:
Period[] appointments = new[]
{
new Period(today.AddHours(9.5), today.AddHours(10.0)),
new Period(today.AddHours(12.0), today.AddHours(13.0)),
new Period(today.AddHours(15.25), today.AddHours(16.5)),
};
Period[] full_day = new[]
{
new Period(today.AddHours(9.0), today.AddHours(17.0)),
};
Computing the free slots is an easy bit of LINQ:
Period[] free_slots =
appointments
.Aggregate(full_day, (a, x) => a.SelectMany(y => y.Fragment(x)).ToArray())
.Except(appointments)
.ToArray();
That gives me: 09:00-09:30, 10:00-12:00, 13:00-15:15, 16:30-17:00.
And finally it's super easy to get the count by calling free_slots.Length.
It's a handy struct to have for these kinds of problems.
Related
I'm currently in the process of writing a class that can represent an infinitely large number (in theory). The constructor of this class creates the object from a string value, which is why the number could be of an extremely large, yet unknown, size.
The reason I started writing this class was because I wanted to be able to make a program that would be able to perform mathematical calculations with numbers of arbitrarily large size. Thus, I started writing a class that could handle values well over the standard ranges of integers, floats, doubles, (hopefully) decimals, etc.
Here are the declarations and the main constructor for the class:
/// <summary>
/// Creates a new instance of the LargeDecimal class, which represents either a whole or decimal number.
/// </summary>
/// <param name="number">The string representation of the number.</param>
public LargeDecimal(string value)
{
string number = value.Replace(" ", "");
if (number.Contains("-") && (number.IndexOf('-') == 0)) {
number = number.Replace("-", "");
IsNegative = true;
}
// Determining whether the number is whole or contains a decimal.
if (number.IndexOf('.') == -1) {
// Does not contain a decimal.
for (int i = 0; i < number.Length; i++)
wholeDigits.Add(int.Parse(number[i].ToString()));
IsWhole = true;
}
else {
// Still check if number is whole. Add all decimal digits.
string[] numArray = number.Split('.');
int sumOfDecimalDigits = 0;
for (int i = 0; i < numArray[1].ToString().Length; i++)
sumOfDecimalDigits += int.Parse(numArray[1].ToString()[i].ToString());
if (sumOfDecimalDigits <= 0) {
// Is a whole number.
for (int i = 0; i < numArray[0].ToString().Length; i++)
wholeDigits.Add(int.Parse(numArray[0].ToString()[i].ToString()));
IsWhole = true;
}
else {
// Is not a whole number.
for (int i = 0; i < numArray[0].ToString().Length; i++)
wholeDigits.Add(int.Parse(numArray[0].ToString()[i].ToString()));
for (int i = 0; i < numArray[1].ToString().Length; i++)
decimalDigits.Add(int.Parse(numArray[1].ToString()[i].ToString()));
IsWhole = false;
}
}
}
The class is basically a representation of a number through two lists of type int, where one list represents the digits that make up the whole partition of the number, and the other list represents the digits that make up the decimal partition of the number (if applicable).
I have written an Add method which accepts two LargeDecimal objects, adds their values together, and returns a new LargeDecimal object with the sum as its value. Though incomplete, it does work with LargeDecimal objects that are whole numbers only, and are both positive or both negative (picture!).
I have realized that adding methods to compare two values (greater than / less than / equal to) would be extremely useful in calculations. However, I am not sure how to check whether the value of a LargeDecimal object is greater or less than the value of another LargeDecimal.
There are cases where I can just compare the amount of items in the wholeDigits list, but that is only when the amounts of items are different for both values.
I am unsure about how to compare two numbers such as: 15498765423654973246 and 15499111137583924246.
And I think it will get more difficult if I will try and compare two fractional numbers: 8573819351.86931 and 8573809999.85999
I do not wish to use integer calculations in conjunction with place values (e.g. in the number 831, the value of the number 8 would be 8 * 100, the value of 3 would be 3 * 10, and the value of 1 would be 1 * 1), because I would like this class to be able to represent values of any given size and length and range (while an int cannot handle values up to 2147483647).
Any help regarding this would be highly appreciated! Thank you all!
I would start by implementing IComparable:
public class LargeDecimal : IComparable<LargeDecimal>
And the implementation would look like:
public int CompareTo(LargeDecimal other)
{
if (other == null) return 1;
if (ReferenceEquals(this, other)) return 0;
if (IsNegative != other.IsNegative)
{
if (other.IsNegative) return 1;
return -1;
}
int multiplier = (IsNegative) ? -1 : 1;
if (wholeDigits.Count > other.wholeDigits.Count) return 1 * multiplier;
if (wholeDigits.Count < other.wholeDigits.Count) return -1 * multiplier;
for (int i = 0; i < wholeDigits.Count; i++)
{
if (wholeDigits[i] > other.wholeDigits[i]) return 1 * multiplier;
if (wholeDigits[i] < other.wholeDigits[i]) return -1 * multiplier;
}
for (int i = 0; i < Math.Min(decimalDigits.Count, other.decimalDigits.Count); i++)
{
if (decimalDigits[i] > other.decimalDigits[i]) return 1 * multiplier;
if (decimalDigits[i] < other.decimalDigits[i]) return -1 * multiplier;
}
if (decimalDigits.Count > other.decimalDigits.Count) return 1 * multiplier;
if (decimalDigits.Count < other.decimalDigits.Count) return -1 * multiplier;
return 0;
}
Update
This project was sitting on my brain at dinner tonight, so I went at it some more for fun. Not sure if this is helpful, but figured I'd share what I came up with.
First, I added fields to make the class actually work:
public bool IsNegative { get; private set; }
public bool IsWhole { get; private set; }
private List<int> wholeDigits;
private List<int> decimalDigits;
Second, I overrode the ToString method so the numbers display nicely:
public override string ToString()
{
return string.Format("{0}{1}{2}{3}",
(IsNegative) ? "-" : "",
string.Join("", wholeDigits),
(IsWhole) ? "" : ".",
(IsWhole) ? "" : string.Join("", decimalDigits));
}
Then I implemented the Equals methods so they work as expected for a number type:
public static bool Equals(LargeDecimal first, LargeDecimal second)
{
return ReferenceEquals(first, null)
? ReferenceEquals(second, null)
: first.Equals(second);
}
public override bool Equals(object obj)
{
return Equals(obj as LargeDecimal);
}
protected bool Equals(LargeDecimal other)
{
return CompareTo(other) == 0;
}
public override int GetHashCode()
{
unchecked
{
var hashCode = (wholeDigits != null)
? wholeDigits.GetHashCode()
: 0;
hashCode = (hashCode * 397) ^
(decimalDigits != null ? decimalDigits.GetHashCode() : 0);
hashCode = (hashCode * 397) ^ IsNegative.GetHashCode();
hashCode = (hashCode * 397) ^ IsWhole.GetHashCode();
return hashCode;
}
}
Next, I added some utility methods to help out with some upcoming tasks:
private void ResetToZero()
{
wholeDigits = new List<int> { 0 };
decimalDigits = new List<int> { 0 };
IsWhole = true;
IsNegative = false;
}
private void NormalizeLists()
{
RemoveLeadingZeroes(wholeDigits);
RemoveTrailingZeroes(decimalDigits);
IsWhole = (decimalDigits.Count == 0
|| (decimalDigits.Count == 1 && decimalDigits[0] == 0));
}
private void AddLeadingZeroes(List<int> list, int numberOfZeroes)
{
if (list == null) return;
for (int i = 0; i < numberOfZeroes; i++)
{
list.Insert(0, 0);
}
}
private void AddTrailingZeroes(List<int> list, int numberOfZeroes)
{
if (list == null) return;
for (int i = 0; i < numberOfZeroes; i++)
{
list.Add(0);
}
}
private void RemoveLeadingZeroes(List<int> list, bool leaveOneIfEmpty = true)
{
if (list == null) return;
var temp = list;
for (int i = 0; i < temp.Count; i++)
{
if (temp[i] == 0)
{
list.RemoveAt(i);
}
else
{
break;
}
}
if (leaveOneIfEmpty && !list.Any()) list.Add(0);
}
private void RemoveTrailingZeroes(List<int> list, bool leaveOneIfEmpty = true)
{
if (list == null) return;
var temp = list;
for (int i = temp.Count -1; i >= 0; i--)
{
if (temp[i] == 0)
{
list.RemoveAt(i);
}
else
{
break;
}
}
if (leaveOneIfEmpty && !list.Any()) list.Add(0);
}
Next, I added some constructors. A default that sets the number to '0', one that parses a string, and another that copies the values from another LargeDecimal:
public LargeDecimal() : this("0") { }
public LargeDecimal(string value)
{
if (value == null) throw new ArgumentNullException("value");
string number = value.Replace(" ", ""); // remove spaces
number = number.TrimStart('0'); // remove leading zeroes
IsNegative = (number.IndexOf('-') == 0); // check for negative
number = number.Replace("-", ""); // remove dashes
// add a zero if there were no numbers before a decimal point
if (number.IndexOf('.') == 0) number = "0" + number;
// Initialize lists
wholeDigits = new List<int>();
decimalDigits = new List<int>();
// Get whole and decimal parts of the number
var numberParts = number.Split(new[] {'.'},
StringSplitOptions.RemoveEmptyEntries);
IsWhole = numberParts.Length == 1;
// Add whole digits to the list
wholeDigits.AddRange(numberParts[0].Select(n => int.Parse(n.ToString())));
// Add decimal digits to the list (if there are any)
if (numberParts.Length > 1 &&
numberParts[1].Sum(n => int.Parse(n.ToString())) > 0)
{
numberParts[1] = numberParts[1].TrimEnd('0');
decimalDigits.AddRange(numberParts[1].Select(n => int.Parse(n.ToString())));
}
NormalizeLists();
}
public LargeDecimal(LargeDecimal initializeFrom)
{
wholeDigits = initializeFrom.wholeDigits
.GetRange(0, initializeFrom.wholeDigits.Count);
decimalDigits = initializeFrom.decimalDigits
.GetRange(0, initializeFrom.decimalDigits.Count);
IsWhole = initializeFrom.IsWhole;
IsNegative = initializeFrom.IsNegative;
NormalizeLists();
}
Then I implemented the Add and Subtract methods
public void Add(LargeDecimal other)
{
if (other == null) return;
if (IsNegative != other.IsNegative)
{
// Get the absolue values of the two operands
var absThis = new LargeDecimal(this) {IsNegative = false};
var absOther = new LargeDecimal(other) {IsNegative = false};
// If the signs are different and the values are the same, reset to 0.
if (absThis == absOther)
{
ResetToZero();
return;
}
// Since the signs are different, we will retain the sign of the larger number
IsNegative = absThis < absOther ? other.IsNegative : IsNegative;
// Assign the difference of the two absolute values
absThis.Subtract(absOther);
wholeDigits = absThis.wholeDigits.GetRange(0, absThis.wholeDigits.Count);
decimalDigits = absThis.decimalDigits.GetRange(0, absThis.decimalDigits.Count);
NormalizeLists();
return;
}
// start with the larger decimal digits list
var newDecimalDigits = new List<int>();
newDecimalDigits = decimalDigits.Count > other.decimalDigits.Count
? decimalDigits.GetRange(0, decimalDigits.Count)
: other.decimalDigits.GetRange(0, other.decimalDigits.Count);
// and add the smaller one to it
int carry = 0; // Represents the value of the 'tens' digit to carry over
for (int i = Math.Min(decimalDigits.Count, other.decimalDigits.Count) - 1; i >= 0; i--)
{
var result = decimalDigits[i] + other.decimalDigits[i] + carry;
carry = Convert.ToInt32(Math.Floor((decimal) result / 10));
result = result % 10;
newDecimalDigits[i] = result;
}
var newWholeDigits = new List<int>();
newWholeDigits = wholeDigits.Count > other.wholeDigits.Count
? wholeDigits.GetRange(0, wholeDigits.Count)
: other.wholeDigits.GetRange(0, other.wholeDigits.Count);
for (int i = Math.Min(wholeDigits.Count, other.wholeDigits.Count) - 1; i >= 0; i--)
{
var result = wholeDigits[i] + other.wholeDigits[i] + carry;
carry = Convert.ToInt32(Math.Floor((decimal)result / 10));
result = result % 10;
newWholeDigits[i] = result;
}
if (carry > 0) newWholeDigits.Insert(0, carry);
wholeDigits = newWholeDigits.GetRange(0, newWholeDigits.Count);
decimalDigits = newDecimalDigits.GetRange(0, newDecimalDigits.Count);
NormalizeLists();
}
public void Subtract(LargeDecimal other)
{
if (other == null) return;
// If the other value is the same as this one, then the difference is zero
if (Equals(other))
{
ResetToZero();
return;
}
// Absolute values will be used to determine how we subtract
var absThis = new LargeDecimal(this) {IsNegative = false};
var absOther = new LargeDecimal(other) {IsNegative = false};
// If the signs are different, then the difference will be the sum
if (IsNegative != other.IsNegative)
{
absThis.Add(absOther);
wholeDigits = absThis.wholeDigits.GetRange(0, absThis.wholeDigits.Count);
decimalDigits = absThis.decimalDigits.GetRange(0, absThis.decimalDigits.Count);
NormalizeLists();
return;
}
// Subtract smallNumber from bigNumber to get the difference
LargeDecimal bigNumber;
LargeDecimal smallNumber;
if (absThis < absOther)
{
bigNumber = new LargeDecimal(absOther);
smallNumber = new LargeDecimal(absThis);
}
else
{
bigNumber = new LargeDecimal(absThis);
smallNumber = new LargeDecimal(absOther);
}
// Pad the whole number and decimal number lists where necessary so that both
// LargeDecimal objects have the same count of whole and decimal numbers.
AddTrailingZeroes(
bigNumber.decimalDigits.Count < smallNumber.decimalDigits.Count
? bigNumber.decimalDigits
: smallNumber.decimalDigits,
Math.Abs(bigNumber.decimalDigits.Count - smallNumber.decimalDigits.Count));
AddLeadingZeroes(smallNumber.wholeDigits,
Math.Abs(bigNumber.wholeDigits.Count - smallNumber.wholeDigits.Count));
var newWholeDigits = new List<int>();
var newDecimalDigits = new List<int>();
bool borrowed = false; // True if we borrowed 1 from next number
for (int i = bigNumber.decimalDigits.Count - 1; i >= 0; i--)
{
if (borrowed)
{
bigNumber.decimalDigits[i] -= 1; // We borrowed one from this number last time
borrowed = false;
}
if (bigNumber.decimalDigits[i] < smallNumber.decimalDigits[i])
{
bigNumber.decimalDigits[i] += 10; // Borrow from next number and add to this one
borrowed = true;
}
// Since we're working from the back of the list, always add to the front
newDecimalDigits.Insert(0, bigNumber.decimalDigits[i] - smallNumber.decimalDigits[i]);
}
for (int i = bigNumber.wholeDigits.Count - 1; i >= 0; i--)
{
if (borrowed)
{
bigNumber.wholeDigits[i] -= 1;
borrowed = false;
}
if (bigNumber.wholeDigits[i] < smallNumber.wholeDigits[i])
{
bigNumber.wholeDigits[i] += 10;
borrowed = true;
}
newWholeDigits.Insert(0, bigNumber.wholeDigits[i] - smallNumber.wholeDigits[i]);
}
if (absThis < absOther) IsNegative = !IsNegative;
wholeDigits = newWholeDigits.GetRange(0, newWholeDigits.Count);
decimalDigits = newDecimalDigits.GetRange(0, newDecimalDigits.Count);
NormalizeLists();
}
And finally overrode the numeric operators:
public static LargeDecimal operator +(LargeDecimal first, LargeDecimal second)
{
if (first == null) return second;
if (second == null) return first;
var result = new LargeDecimal(first);
result.Add(second);
return result;
}
public static LargeDecimal operator -(LargeDecimal first, LargeDecimal second)
{
if (first == null) return second;
if (second == null) return first;
var result = new LargeDecimal(first);
result.Subtract(second);
return result;
}
public static bool operator >(LargeDecimal first, LargeDecimal second)
{
if (first == null) return false;
return first.CompareTo(second) > 0;
}
public static bool operator <(LargeDecimal first, LargeDecimal second)
{
if (second == null) return false;
return second.CompareTo(first) > 0;
}
public static bool operator >=(LargeDecimal first, LargeDecimal second)
{
if (first == null) return false;
return first.CompareTo(second) >= 0;
}
public static bool operator <=(LargeDecimal first, LargeDecimal second)
{
if (second == null) return false;
return second.CompareTo(first) >= 0;
}
public static bool operator ==(LargeDecimal first, LargeDecimal second)
{
return Equals(first, second);
}
public static bool operator !=(LargeDecimal first, LargeDecimal second)
{
return !Equals(first, second);
}
Thanks for the fun challenge!!
Assuming that this implementation looks something like this:
List<int> WholeList;
List<int> FactionalList;
bool IsNegative;
and they both grow away from the decimal point, then a comparison algorithm would go like this
First compare signs. Negative is always less than positive.
Compare lengths of WholeList, longer has larger magnitude (larger number is dependent on sign)
If WholeList.Count the same. Compare each digit starting with most significant (aka WholeList[Count-1] first), first that are different between numbers will determine larger magnitude.
If you make it into the FractionalList, and then run out of digits in one list. The number with the longer FractionalList will be the larger magnitude.
I have these 3 strings (using # as a delimiter):
Name#startTime#endTime#room
Meeting#19:00:00#20:30:00#Conference
Hist 2368#19:00:00#20:30:00#Large Conference Room
Hist 2368#09:00:00#10:30:00#Large Conference Room
I want to know how would I generate this
Conference 9:00:00 19:00:00
Large Conference Room 10:30:00 20:30:00
Large Conference Room 20:30:00 22:00:00
So what this is generating are the times of a room that are free. In the top 3 strings we can see that Conference is occupied from 19:00:00 to 20:30:00 so the free time is 9:00:00 to 19:00:00 (A day starts at 9:00:00 and ends at 22:00:00).
So to make this task relatively easy you need to define a class that understands how to split a period of time given a potentially overlapping period of time.
Here's that class:
private sealed class Period : IEquatable<Period>
{
public DateTime StartTime { get; private set; }
public DateTime EndTime { get; private set; }
public Period(DateTime startTime, DateTime endTime)
{
this.StartTime = startTime;
this.EndTime = endTime;
}
public override bool Equals(object obj)
{
if (obj is Period)
return Equals((Period)obj);
return false;
}
public bool Equals(Period obj)
{
if (obj == null)
return false;
if (!EqualityComparer<DateTime>.Default.Equals(
this.StartTime, obj.StartTime))
return false;
if (!EqualityComparer<DateTime>.Default.Equals(
this.EndTime, obj.EndTime))
return false;
return true;
}
public override int GetHashCode()
{
int hash = 0;
hash ^= EqualityComparer<DateTime>.Default
.GetHashCode(this.StartTime);
hash ^= EqualityComparer<DateTime>.Default
.GetHashCode(this.EndTime);
return hash;
}
public override string ToString()
{
return String.Format("{{ StartTime = {0}, EndTime = {1} }}",
this.StartTime, this.EndTime);
}
public IEnumerable<Period> Split(Period period)
{
if (period.StartTime <= this.StartTime)
{
if (period.EndTime <= this.StartTime)
yield return this;
else if (period.EndTime >= this.EndTime)
yield break;
else
yield return new Period(period.EndTime, this.EndTime);
}
else if (period.StartTime < this.EndTime)
{
yield return new Period(this.StartTime, period.StartTime);
if (period.EndTime < this.EndTime)
{
yield return new Period(period.EndTime, this.EndTime);
}
}
else
yield return this;
}
}
The important code here is the IEnumerable<Period> Split(Period period) method. It goes through each possible case when comparing two time periods and returns zero, one or two periods that can be left after the split.
So, given your input data is like this:
var lines = new []
{
"Meeting#19:00:00#20:30:00#Conference",
"Hist 2368#19:00:00#20:30:00#Large Conference Room",
"Hist 2368#09:00:00#10:30:00#Large Conference Room",
};
var full_day =
new Period(
DateTime.Parse("09:00"),
DateTime.Parse("22:00"));
I can then run this code to determine the free times:
var free_times =
from line in lines
let parts = line.Split('#')
let Start = DateTime.Parse(parts[1])
let End = DateTime.Parse(parts[2])
orderby Start, End
group new Period(Start, End) by parts[3] into groups
select new
{
Room = groups.Key,
FreePeriods =
groups.Aggregate(new [] { full_day },
(ys, x) => ys.SelectMany(y => y.Split(x)).ToArray()),
};
The result I get is:
NB: Your example results in the question do not match your data. I have assumed that your data is correct and ignored your example results.
I have a number of objects each with 3 numerical properties: "high", "low" and "tiebreaker". They are to be sorted as such: if an object's low is higher than another object's high, it appears before it in the list. Likewise if an object's high is lower than another's low, it appears later in the list. But in the case that two objects have conflicting ranges (eg one's high is between the other object's low and high), the tiebreaker property is considered wherein the object with the higher tiebreaker value gets placed earlier on the list.
I am specifically working with c#, but I think the ideas here are language agnostic enough such that code of any sort (no puns) would be welcome.
Also, I have worked on this myself. I have a nested for-loop that is just not working out for me so far. I'd give up some code but I'm on my phone and that makes it a chore. Besides, this is probably a fun one for you and you don't need my ugly code in your way anyhow.
Are you assuming that Min <= Tie <= Max? You do not say so in your question, and if you do not, the sort order is not well defined because it is not transitive. For instance, writing your ranges as [Min, Tie, Max], consider:
A: [5,-10, 6]
B: [0, 1, 10]
C: [2, 3, 4]
A < B (because they overlap and -10 < 1)
B < C (because they overlap and 1 < 3)
but A > C (because they don't overlap and 5 > 4)
If they are you can define a custom IComparer<Range> for your Range class, and pass it to any c# sort method.
Update and here's one such implementation.
public struct RangeWithTie<T> where T : IEquatable<T>, IComparable<T>
{
readonly T min;
readonly T max;
readonly T tie;
readonly bool isNonEmpty;
public static Range<T> Empty = new Range<T>();
public static IComparer<RangeWithTie<T>> CreateSortingComparer()
{
return new RangeWithTieComparer();
}
public RangeWithTie(T start, T tie, T end)
{
// Enfore start <= tie <= end
var comparer = Comparer<T>.Default;
if (comparer.Compare(start, end) > 0) // if start > end
{
throw new ArgumentOutOfRangeException("start and end are reversed");
}
else if (comparer.Compare(start, tie) > 0)
{
throw new ArgumentOutOfRangeException("tie is less than start");
}
else if (comparer.Compare(tie, end) > 0)
{
throw new ArgumentOutOfRangeException("tie is bigger than end");
}
else
{
this.min = start;
this.max = end;
this.tie = tie;
}
this.isNonEmpty = true;
}
public T Min { get { return min; } }
public T Max { get { return max; } }
public T Tie { get { return tie; } }
public bool IsEmpty { get { return !isNonEmpty; } }
public class RangeWithTieComparer : IComparer<RangeWithTie<T>>
{
#region IComparer<RangeWithTie<T>> Members
public int Compare(RangeWithTie<T> x, RangeWithTie<T> y)
{
// return x - y.
if (x.IsEmpty)
{
if (y.IsEmpty)
return 0;
else
return -1;
}
else if (y.IsEmpty)
{
return 1;
}
var comparer = Comparer<T>.Default;
if (comparer.Compare(y.Min, x.Max) > 0)
return -1;
else if (comparer.Compare(x.Min, y.Max) > 0)
return 1;
return comparer.Compare(x.Tie, y.Tie);
}
#endregion
}
public override string ToString()
{
if (IsEmpty)
return "Empty";
StringBuilder s = new StringBuilder();
s.Append('[');
if (Min != null)
{
s.Append(Min.ToString());
}
s.Append(", ");
if (Tie != null)
{
s.Append(Tie.ToString());
}
s.Append(", ");
if (Max != null)
{
s.Append(Max.ToString());
}
s.Append(']');
return s.ToString();
}
}
This could be used like so:
var sortedRanges = ranges.OrderBy(x => x, RangeWithTie<double>.CreateSortingComparer()).ToArray();
I didn't make the struct implement IComparer<RangeWithTie<T>> directly because ranges with identical comparisons aren't necessarily equal. For instance, [-1,0,1] and [-2,0,1] have identical comparisons but are not equal.
A quick solution, and a console application to test it. This method will return the larger of two objects. Just replace dynamic with the appropriate object type you need.
class Program
{
private static object Sort(dynamic first, dynamic second)
{
if (OverlapExists(first, second))
{
// Note: If tiebreakers are equal, the first will be returned:
return first.tiebreaker >= second.tiebreaker ? first : second;
}
else
{
// Note: Only need to test one value (just high); Since we know
// there is no overlap, the whole object (both high and low) must
// be either over or under that which it is compared to:
return first.high > second.high ? first : second;
}
}
private static bool OverlapExists(dynamic first, dynamic second)
{
return (first.low < second.high) && (second.low < first.high);
}
static void Main(string[] args)
{
dynamic first = new {name="first", high = 10,
tiebreaker = 5, low = 1 };
dynamic second = new {name="second", high = 15,
tiebreaker = 12, low = 11 };
dynamic third = new {name="third", high = 20,
tiebreaker = 9, low = 6 };
var firstResult = Sort(first, second);
var secondResult = Sort(first, third);
var thirdResult = Sort(second, third);
Console.WriteLine("1) " + first.ToString()
+ "\nVS: " + second.ToString());
Console.WriteLine("Winner: " + firstResult.name);
Console.WriteLine("\n2) " + first.ToString()
+ "\nVS: " + third.ToString());
Console.WriteLine("Winner: " + secondResult.name);
Console.WriteLine("\n3) " + second.ToString()
+ "\nVS: " + third.ToString());
Console.WriteLine("Winner: " + thirdResult.name);
Console.ReadKey();
}
}
Let’s say you have a List<T> (T being your objects with High-, Low- and Tie- Property), then you can use
list.Sort(…);
with a Comparison<T> as a Parameter. That’s a delegate that takes 2 of you objects and should return < 0, when the first instance of your object should be a head of the other instance or 0 if they are of equal order (or > 0 if the second second object should be ahead of first).
Or you could pass an custom comparer (implementing IComparer<T>) which does basically the same as the Comparison<T> but inform of an interface.
No matter what your logic is, you may implement IComparable to enable an Array or List's sorting capability. So, as the follow code shows,
public class MyStuff : IComparable<MyStuff>
{
public int High { get; set; }
public int Low { get; set; }
public int TieBreaker { get; set; }
public int CompareTo(MyStuff other)
{
// if an object's low is higher than another object's high,
// it appears before it in the list
if ((this.Low > other.High) ||
// if its high is between the other object's low and
// high then compare their tiebreaker
(this.High > other.Low && this.High < other.High &&
this.TieBreaker > other.TieBreaker))
return 1;
else if (this.Low == other.High)
return 0;
else
return -1;
}
}
The basic idea is CompareTo returns either 1 (move this before other), 0 (retain both positions) or -1 (move this after other), depending on your ordering logic.
See IComparable<T>
class DataObject : IComparable<DataObject>
{
public double High, Low, Tiebreaker;
public int CompareTo(DataObject obj)
{
// this doesn't seem to make sense as a range sort, but seems to match your question...
// low > another high
if (this.Low != obj.High)
return this.Low.CompareTo(obj.High);
// otherwise sort tiebreaker ascending
else this.TieBreaker.CompareTo(obj.TieBreaker);
}
}
used as
var items = new[] { new DataObject(1,2,3), new DataObject(4,5,6) };
Array.Sort<DataObject>(items);
// items is now sorted
I am looking for a method of splitting a date range into a series of date ranges by chunk size of days. I am planning on using this to buffer calls to a service which if the date range is too large, the service faults.
This is what I have come up with so far. It seems to work, but I am not sure if it will exit properly. This seems like something that has probably been done several times before, but I can't find it.
public IEnumerable<Tuple<DateTime, DateTime>> SplitDateRange(DateTime start, DateTime end, int dayChunkSize)
{
var newStart = start;
var newEnd = start.AddDays(dayChunkSize);
while (true)
{
yield return new Tuple<DateTime, DateTime>(newStart, newEnd);
if (newEnd == end)
yield break;
newStart = newStart.AddDays(dayChunkSize);
newEnd = (newEnd.AddDays(dayChunkSize) > end ? end : newEnd.AddDays(dayChunkSize));
}
}
I'm looking for improvement suggestions, or "Dude, use this existing function for this!"
I think your code fails when the difference between start and end is smaller than dayChunkSize.
See this:
var singleRange = SplitDateRange(DateTime.Now, DateTime.Now.AddDays(7), dayChunkSize: 15).ToList();
Debug.Assert(singleRange.Count == 1);
Proposed solution:
public static IEnumerable<Tuple<DateTime, DateTime>> SplitDateRange(DateTime start, DateTime end, int dayChunkSize)
{
DateTime chunkEnd;
while ((chunkEnd = start.AddDays(dayChunkSize)) < end)
{
yield return Tuple.Create(start, chunkEnd);
start = chunkEnd;
}
yield return Tuple.Create(start, end);
}
There are a couple of problems with your solution:
the test newEnd == end may never be true, so the while could loop forever (I now see that this condition should always be triggered, but it wasn't obvious on first reading of the code; the while(true) feels a bit dangerous still)
AddDays is called three times for each iteration (minor performance issue)
Here is an alternative:
public IEnumerable<Tuple<DateTime, DateTime>> SplitDateRange(DateTime start, DateTime end, int dayChunkSize)
{
DateTime startOfThisPeriod = start;
while (startOfThisPeriod < end)
{
DateTime endOfThisPeriod = startOfThisPeriod.AddDays(dayChunkSize);
endOfThisPeriod = endOfThisPeriod < end ? endOfThisPeriod : end;
yield return Tuple.Create(startOfThisPeriod, endOfThisPeriod);
startOfThisPeriod = endOfThisPeriod;
}
}
Note that this truncates the last period to end on end as given in the code in the question. If that's not needed, the second line of the while could be omitted, simplifying the method. Also, startOfThisPeriod isn't strictly necessary, but I felt that was clearer than reusing start.
With respect to accepted answer you could use the short form of tuples:
private static IEnumerable<(DateTime, DateTime)> GetDateRange1(DateTime startDate, DateTime endDate, int daysChunkSize)
{
DateTime markerDate;
while ((markerDate = startDate.AddDays(daysChunkSize)) < endDate)
{
yield return (startDate, markerDate);
startDate = markerDate;
}
yield return (startDate, endDate);
}
But I prefer to use named tuples:
private static IEnumerable<(DateTime StartDate, DateTime EndDate)> GetDateRange(DateTime startDate, DateTime endDate, int daysChunkSize)
{
DateTime markerDate;
while ((markerDate = startDate.AddDays(daysChunkSize)) < endDate)
{
yield return (StartDate: startDate, EndDate: markerDate);
startDate = markerDate;
}
yield return (StartDate: startDate, EndDate: endDate);
}
Your code looks fine for me. I don't really like the idea of while(true)
But other solution would be to use enumerable.Range:
public static IEnumerable<Tuple<DateTime, DateTime>> SplitDateRange(DateTime start, DateTime end, int dayChunkSize)
{
return Enumerable
.Range(0, (Convert.ToInt32((end - start).TotalDays) / dayChunkSize +1))
.Select(x => Tuple.Create(start.AddDays(dayChunkSize * (x)), start.AddDays(dayChunkSize * (x + 1)) > end
? end : start.AddDays(dayChunkSize * (x + 1))));
}
or also, this will also work:
public static IEnumerable<Tuple<DateTime, DateTime>> SplitDateRange(DateTime start, DateTime end, int dayChunkSize)
{
var dateCount = (end - start).TotalDays / 5;
for (int i = 0; i < dateCount; i++)
{
yield return Tuple.Create(start.AddDays(dayChunkSize * i)
, start.AddDays(dayChunkSize * (i + 1)) > end
? end : start.AddDays(dayChunkSize * (i + 1)));
}
}
I do not have any objects for any of the implementations. They are practically identical.
If you know how many chunks/intervals/periods/parts you want to split your time range into, I've found the following to be helpful
You can use the DateTime.Ticks property to define your intervals, and then create a series of DateTime objects based on your defined interval:
IEnumerable<DateTime> DivideTimeRangeIntoIntervals(DateTime startTS, DateTime endTS, int numberOfIntervals)
{
long startTSInTicks = startTS.Ticks;
long endTsInTicks = endTS.Ticks;
long tickSpan = endTS.Ticks - startTS.Ticks;
long tickInterval = tickSpan / numberOfIntervals;
List<DateTime> listOfDates = new List<DateTime>();
for (long i = startTSInTicks; i <= endTsInTicks; i += tickInterval)
{
listOfDates.Add(new DateTime(i));
}
return listOfDates;
}
You can convert that listOfDates into however you want to represent a timerange (a tuple, a dedicated date range object, etc). You can also modify this function to directly return it in the form you need it.
There are a lot of corner cases that are unhandled in the answers so far. And it's not entirely clear how you would want to handle them. Do you want overlapping start/end of ranges? Is there a minimum range size? Below is some code that'll handle some of the corner cases, you'll have to think about overlapping especially and possibly push the start/end of ranges by a few seconds or maybe more depending on the data you're returning.
public static IEnumerable<(DateTime start, DateTime end)> PartitionDateRange(DateTime start,
DateTime end,
int chunkSizeInDays)
{
if (start > end)
yield break;
if (end - start < TimeSpan.FromDays(chunkSizeInDays))
{
yield return (start, end);
yield break;
}
DateTime e = start.AddDays(chunkSizeInDays);
for (;e < end; e = e.AddDays(chunkSizeInDays))
{
yield return (e.AddDays(-chunkSizeInDays), e);
}
if (e < end && end - e > TimeSpan.FromMinutes(1))
yield return (e, end);
}
Example call:
static void Main(string[] _)
{
Console.WriteLine("expected");
DateTime start = DateTime.Now - TimeSpan.FromDays(10);
DateTime end = DateTime.Now;
foreach (var range in PartitionDateRange(start, end, 2))
{
Console.WriteLine($"{range.start} to {range.end}");
}
Console.WriteLine("start > end");
start = end + TimeSpan.FromDays(1);
foreach (var range in PartitionDateRange(start, end, 2))
{
Console.WriteLine($"{range.start} to {range.end}");
}
Console.WriteLine("less than partition size");
start = end - TimeSpan.FromDays(1);
foreach (var range in PartitionDateRange(start, end, 2))
{
Console.WriteLine($"{range.start} to {range.end}");
}
}
The accepted solution looks good in most cases. If you need to take away overlap on the beginning and the end of each chunk, then this might work better.
public static IEnumerable<(DateTime FromDate, DateTime ToDate)> SplitDateRange(DateTime start, DateTime end, int dayChunkSize)
{
DateTime chunkEnd;
while ((chunkEnd = start.AddDays(dayChunkSize-1)) < end)
{
yield return (start, chunkEnd);
start = chunkEnd.AddDays(1);
}
yield return (start, end);
}
hare is an example spliced by month
IEnumerable<(DateTime, DateTime)> SplitDateRange(DateTime start, DateTime end, int monthChunkSize)
{
DateTime dateEnd=DateTime.Parse(end.ToString());
for (int i = 0;start.AddMonths(i) < dateEnd; i+=monthChunkSize)
{
end = start.AddMonths(i+monthChunkSize);
start.AddMonths(i);
yield return (start.AddMonths(i), end<dateEnd?end:dateEnd);
}
}
So I was writing a mergesort in C# as an exercise and although it worked, looking back at the code, there was room for improvement.
Basically, the second part of the algorithm requires a routine to merge two sorted lists.
Here is my way too long implementation that could use some refactoring:
private static List<int> MergeSortedLists(List<int> sLeft, List<int> sRight)
{
if (sLeft.Count == 0 || sRight.Count == 0)
{
sLeft.AddRange(sRight);
return sLeft;
}
else if (sLeft.Count == 1 && sRight.Count == 1)
{
if (sLeft[0] <= sRight[0])
sLeft.Add(sRight[0]);
else
sLeft.Insert(0, sRight[0]);
return sLeft;
}
else if (sLeft.Count == 1 && sRight.Count > 1)
{
for (int i=0; i<sRight.Count; i++)
{
if (sLeft[0] <= sRight[i])
{
sRight.Insert(i, sLeft[0]);
return sRight;
}
}
sRight.Add(sLeft[0]);
return sRight;
}
else if (sLeft.Count > 1 && sRight.Count == 1)
{
for (int i=0; i<sLeft.Count; i++)
{
if (sRight[0] <= sLeft[i])
{
sLeft.Insert(i, sRight[0]);
return sLeft;
}
}
sLeft.Add(sRight[0]);
return sLeft;
}
else
{
List<int> list = new List<int>();
if (sLeft[0] <= sRight[0])
{
list.Add(sLeft[0]);
sLeft.RemoveAt(0);
}
else
{
list.Add(sRight[0]);
sRight.RemoveAt(0);
}
list.AddRange(MergeSortedLists(sLeft, sRight));
return list;
}
}
Surely this routine can be improved/shortened by removing recursion, etc. There are even other ways to merge 2 sorted lists. So any refactoring is welcome.
Although I do have an answer, I'm curious as to how would other programmers would go about improving this routine.
Thank you!
Merging two sorted lists can be done in O(n).
List<int> lList, rList, resultList;
int r,l = 0;
while(l < lList.Count && r < rList.Count)
{
if(lList[l] < rList[r]
resultList.Add(lList[l++]);
else
resultList.Add(rList[r++]);
}
//And add the missing parts.
while(l < lList.Count)
resultList.Add(lList[l++]);
while(r < rList.Count)
resultList.Add(rList[r++]);
My take on this would be:
private static List<int> MergeSortedLists(List<int> sLeft, List<int> sRight)
{
List<int> result = new List<int>();
int indexLeft = 0;
int indexRight = 0;
while (indexLeft < sLeft.Count || indexRight < sRight.Count)
{
if (indexRight == sRight.Count ||
(indexLeft < sLeft.Count && sLeft[indexLeft] < sRight[indexRight]))
{
result.Add(sLeft[indexLeft]);
indexLeft++;
}
else
{
result.Add(sRight[indexRight]);
indexRight++;
}
}
return result;
}
Exactly what I'd do if I had to do it by hand. =)
Are you really sure your code works at all? Without testing it, i see the following:
...
else if (sLeft.Count > 1 && sRight.Count == 0) //<-- sRight is empty
{
for (int i=0; i<sLeft.Count; i++)
{
if (sRight[0] <= sLeft[i]) //<-- IndexError?
{
sLeft.Insert(i, sRight[0]);
return sLeft;
}
}
sLeft.Add(sRight[0]);
return sLeft;
}
...
As a starting point, I would remove your special cases for when either of the lists has Count == 1 - they can be handled by your more general (currently recursing) case.
The if (sLeft.Count > 1 && sRight.Count == 0) will never be true because you've checked for sRight.Count == 0 at the start - so this code will never be reached and is redundant.
Finally, instead of recursing (which is very costly in this case due to the number of new Lists you create - one per element!), I'd do something like this in your else (actually, this could replace your entire method):
List<int> list = new List<int>();
while (sLeft.Count > 0 && sRight.Count > 0)
{
if (sLeft[0] <= sRight[0])
{
list.Add(sLeft[0]);
sLeft.RemoveAt(0);
}
else
{
list.Add(sRight[0]);
sRight.RemoveAt(0);
}
}
// one of these two is already empty; the other is in sorted order...
list.AddRange(sLeft);
list.AddRange(sRight);
return list;
(Ideally I'd refactor this to use integer indexes against each list, instead of using .RemoveAt, because it's more performant to loop through the list than destroy it, and because it might be useful to leave the original lists intact. This is still more efficient code than the original, though!)
You were asking for differrent approaches as well. I might do as below depending on the usage. The below code is lazy so it will not sort the entire list at once but only when elements are requested.
class MergeEnumerable<T> : IEnumerable<T>
{
public IEnumerator<T> GetEnumerator()
{
var left = _left.GetEnumerator();
var right = _right.GetEnumerator();
var leftHasSome = left.MoveNext();
var rightHasSome = right.MoveNext();
while (leftHasSome || rightHasSome)
{
if (leftHasSome && rightHasSome)
{
if(_comparer.Compare(left.Current,right.Current) < 0)
{
yield return returner(left);
} else {
yield return returner(right);
}
}
else if (rightHasSome)
{
returner(right);
}
else
{
returner(left);
}
}
}
private T returner(IEnumerator<T> enumerator)
{
var current = enumerator.Current;
enumerator.MoveNext();
return current;
}
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
{
return ((IEnumerable<T>)this).GetEnumerator();
}
private IEnumerable<T> _left;
private IEnumerable<T> _right;
private IComparer<T> _comparer;
MergeEnumerable(IEnumerable<T> left, IEnumerable<T> right, IComparer<T> comparer)
{
_left = left;
_right = right;
_comparer = comparer;
}
}
EDIT: It's basically the same implementatin as Sergey Osypchuk his will from start to finish when looking only at the sorting be fastest but the latency will be higher as well due to the fact of sorting the entire list upfront. So as I said depending on the usage I might go with this approach and an alternative would be something similar to Sergey Osypchuk
Often you can use a stack instead of use recursion
Merge list (by theory, input lists are sorted in advance) sorting could be implemented in following way:
List<int> MergeSorting(List<int> a, List<int> b)
{
int apos = 0;
int bpos = 0;
List<int> result = new List<int>();
while (apos < a.Count && bpos < b.Count)
{
int avalue = int.MaxValue;
int bvalue = int.MaxValue;
if (apos < a.Count)
avalue = a[apos];
if (bpos < b.Count)
bvalue = b[bpos];
if (avalue < bvalue)
{
result.Add(avalue);
apos++;
}
else
{
result.Add(bvalue);
bpos++;
}
}
return result;
}
In case you start with not sorted list you need to split it by sorted subsequence and than marge them using function above
I never use recursion for merge sort. You can make iterative passes over the input, taking advantage of the fact that the sorted block size doubles with every merge pass. Keep track of the block size and the count of items you've processed from each input list; when they're equal, the list is exhausted. When both lists are exhausted you can move on to the next pair of blocks. When the block size is greater than or equal to your input size, you're done.
Edit: Some of the information I had left previously was incorrect, due to my misunderstanding - a List in C# is similar to an array and not a linked list. My apologies.