C
C#15mo ago
Aaron I

✅ Reoccurring times with a possibility to a one time rep in C#

Suppose we have an entity which represents irl event, this event can reoccur every week or be one-time event. How should I manage it? currently my code is pretty disgusting I'm getting a boolean value _reoccurring which determines which type it is. Then, depends on the value I either assign the following properties accordingly:
//null when is one-time, can be separated to a designated struct:
DayOfWeek Day { get; set; }
TimeSpan Start {...}
TimeSpan End {...}
//null when reoccurring:
DateTime Start {...}
DateTime End {...}
//null when is one-time, can be separated to a designated struct:
DayOfWeek Day { get; set; }
TimeSpan Start {...}
TimeSpan End {...}
//null when reoccurring:
DateTime Start {...}
DateTime End {...}
It might be stupid and I'm missing something which makes it easier a lot more, but after digging SO for 30min I decided to come here. There is separation between the two because I don't want a bound to a specific date when working on reoccurring time-events. Another idea I thought of is using the same boolean prop (_reoccurring), keep Start and End props and depends on _reoccurring value I will ignore/ include the date that Start and End bound to. However, I don't know if it's a reasonable/proper solution.
24 Replies
Pobiega
Pobiega15mo ago
You could use inheritance here to have two different types, since the data required for the two event-types is different alternatively, have the event be a "single time" event, but have a "RecurranceSchedule" property that is nullable if its not null, it contains all the data required for recurrance
Aaron I
Aaron IOP15mo ago
I see where you go here, I thought about it but I feel like it overcomplicate things later when configuring these in the Db and would become a headache to maintain.
Pobiega
Pobiega15mo ago
its not too bad, assuming EF Core just use Table per Hierarchy (default) and its fine and for the second alternative, have the schedule be an owned type
Aaron I
Aaron IOP15mo ago
For example (just to make sure I understand): I have datetime and any time the date passes I update it and add 7 days to it? Per hierarchy makes sure both ReoccurredEvent and OneTimeEvent are in the same table?
Pobiega
Pobiega15mo ago
yes it will create nullable columns for all the type-specific props in the database
Aaron I
Aaron IOP15mo ago
I see
Pobiega
Pobiega15mo ago
so this is a different topic, kinda. when dealing with recurring events, you can model that however you want but a fairly common way is to have "virtual" events you have a "template" event that gets "copied" to all the virtual ones (the ones that match the recurrance schedule) but only the first one exists in the database, until someone edits a virtual one
Aaron I
Aaron IOP15mo ago
I think the idea itself is good if I'm understanding correctly but execution may be a bit difficult, I would need to have a periodic check which dates passed and update them. Wouldn't it lead to incorrect data and intensity I wouldn't want to add
Pobiega
Pobiega15mo ago
at that point it gets turned into a real one, recurrance scheudled copied to it (with it as the start/template) and the previous one cut short its... not trivial 🙂 well, it all depends on what you need in outlook for example, you can edit a series or a specific occurance if you create a recurring event in outlook then look in the far future, you will not see your event it only creates the next ~30 or so instances of it and updates as you go if you dont need the ability to edit an individual occurance, only a series, you can skip a lot of complexity
Aaron I
Aaron IOP15mo ago
Well I do not, a good example is a lesson A lesson can reoccur as long as we're in an academic year or a weekly meeting, which happens as long as we're in development period something like this
Pobiega
Pobiega15mo ago
okay
Aaron I
Aaron IOP15mo ago
This is why I used first this struct:
DayOfWeek Day { get; set; }
TimeSpan Start {...}
TimeSpan End {...}
DayOfWeek Day { get; set; }
TimeSpan Start {...}
TimeSpan End {...}
Pobiega
Pobiega15mo ago
what if a lesson would fall on a national holiday? shouldnt that be cancelled then? that would be an exception to the schedule, and thats hard to model
Aaron I
Aaron IOP15mo ago
We'll I have another system for it, and its not exactly a lesson. But if you have an exceptional time stamp, it wouldn't request additional data about that time, so the client isn't seeing the reoccurrance or anything in that time stamp
Pobiega
Pobiega15mo ago
I made a test implementation. Its not perfect, but it shows what I was talking about
public class Event
{
public string Name { get; set; }
public Occurance Occurance { get; }
public RecurranceSchedule? RecurranceSchedule { get; }

public Event(string name, DateTimeOffset start, DateTimeOffset stop, RecurranceSchedule? recurranceSchedule = null)
{
Name = name;
Occurance = new Occurance(start, stop);
RecurranceSchedule = recurranceSchedule;
}

public IEnumerable<Event> GetInstances()
{
yield return this;
if (RecurranceSchedule is null) yield break;
foreach (var occurance in RecurranceSchedule.GetOccurances(Occurance))
{
yield return new Event(Name, occurance.Start, occurance.Stop);
}
}
}

public class RecurranceSchedule
{
public TimeSpan Interval { get; }
public DateTimeOffset? EndsAt { get; }

public RecurranceSchedule(TimeSpan interval, DateTimeOffset? endsAt)
{
Interval = interval;
EndsAt = endsAt;
}

public IEnumerable<Occurance> GetOccurances(Occurance first)
{
var occurance = first;
var count = 0;
while (count < 10 || occurance.Start <= EndsAt)
{
count++;
occurance = new Occurance(occurance.Start + Interval, occurance.Stop + Interval);
yield return occurance;
}
}
}

public record Occurance(DateTimeOffset Start, DateTimeOffset Stop);

public class RecurranceScheduleBuilder
{
private TimeSpan _interval;
private DateTimeOffset? _endDate;

public RecurranceScheduleBuilder Weekly()
{
_interval = TimeSpan.FromDays(7);
return this;
}

public RecurranceScheduleBuilder Until(DateTimeOffset endDate)
{
_endDate = endDate;
return this;
}

public RecurranceSchedule Build()
{
return new RecurranceSchedule(_interval, _endDate);
}
}
public class Event
{
public string Name { get; set; }
public Occurance Occurance { get; }
public RecurranceSchedule? RecurranceSchedule { get; }

public Event(string name, DateTimeOffset start, DateTimeOffset stop, RecurranceSchedule? recurranceSchedule = null)
{
Name = name;
Occurance = new Occurance(start, stop);
RecurranceSchedule = recurranceSchedule;
}

public IEnumerable<Event> GetInstances()
{
yield return this;
if (RecurranceSchedule is null) yield break;
foreach (var occurance in RecurranceSchedule.GetOccurances(Occurance))
{
yield return new Event(Name, occurance.Start, occurance.Stop);
}
}
}

public class RecurranceSchedule
{
public TimeSpan Interval { get; }
public DateTimeOffset? EndsAt { get; }

public RecurranceSchedule(TimeSpan interval, DateTimeOffset? endsAt)
{
Interval = interval;
EndsAt = endsAt;
}

public IEnumerable<Occurance> GetOccurances(Occurance first)
{
var occurance = first;
var count = 0;
while (count < 10 || occurance.Start <= EndsAt)
{
count++;
occurance = new Occurance(occurance.Start + Interval, occurance.Stop + Interval);
yield return occurance;
}
}
}

public record Occurance(DateTimeOffset Start, DateTimeOffset Stop);

public class RecurranceScheduleBuilder
{
private TimeSpan _interval;
private DateTimeOffset? _endDate;

public RecurranceScheduleBuilder Weekly()
{
_interval = TimeSpan.FromDays(7);
return this;
}

public RecurranceScheduleBuilder Until(DateTimeOffset endDate)
{
_endDate = endDate;
return this;
}

public RecurranceSchedule Build()
{
return new RecurranceSchedule(_interval, _endDate);
}
}
var recurringEvent = new Event("Yoga class", DateTimeOffset.Now, DateTimeOffset.Now.AddHours(1), new RecurranceScheduleBuilder().Weekly().Build());
var onetimeEvent = new Event("Consultation", DateTimeOffset.Now.AddDays(1), DateTimeOffset.Now.AddDays(1).AddHours(1));

var rootEvents = new List<Event> { recurringEvent, onetimeEvent };

var allEvents = rootEvents.SelectMany(x => x.GetInstances());
foreach (var instance in allEvents)
{
Console.WriteLine($"{instance.Name}: {instance.Occurance.Start} to {instance.Occurance.Stop}");
}
var recurringEvent = new Event("Yoga class", DateTimeOffset.Now, DateTimeOffset.Now.AddHours(1), new RecurranceScheduleBuilder().Weekly().Build());
var onetimeEvent = new Event("Consultation", DateTimeOffset.Now.AddDays(1), DateTimeOffset.Now.AddDays(1).AddHours(1));

var rootEvents = new List<Event> { recurringEvent, onetimeEvent };

var allEvents = rootEvents.SelectMany(x => x.GetInstances());
foreach (var instance in allEvents)
{
Console.WriteLine($"{instance.Name}: {instance.Occurance.Start} to {instance.Occurance.Stop}");
}
Aaron I
Aaron IOP15mo ago
I see, well thanks I didn't expect this. Just to make sure I understand the intentions behind this impl: 1. Occurrences are made when queried (from memory) 2. I assume you just didn't include it but when initiating an event(from db or a new one entirely) we'll update the first occurrence to match todays date 3. It wouldn't work if we need to get a record of old occurrences?
Pobiega
Pobiega15mo ago
yep yep and yep You could do a system where when an occurance "is happening" it gets turned into a concrete event that would solve 3 nicely, and could also be used to "fix" 2
Aaron I
Aaron IOP15mo ago
Well, I'd look what I can do with it. Helped a lot, thank you as always!
Pobiega
Pobiega15mo ago
kinda depends on if this is an on-demand app or something that runs 24/7 how you'd solve that thou
Aaron I
Aaron IOP15mo ago
Well, it is not on demand and it is not running 24/7. It's more of a record-house with extra capabilities and actions in its domain. With such events they're not reoccurring on demand but rather just live inside the schedule of the user within the time context that he defined them. He can go back and see them and go forward and see them in his schedule. They're not different, they're the same events (such as lessons, meetings, briefs, etc). What made this a bit more complicated is to introduce "one-time" feature.
Pobiega
Pobiega15mo ago
well its either a server based app that runs all the time, or its a program the user starts and stops as desired (a normal client app) you could at startup check all events with recurring schedules that have been in the (recent?) past and turn their occurances to concrete events, and run the same check every 5 minutes or so while the program is running if its important to know what "series" an event is connected to, you could turn recurringSchedule into a db entity in its own right
Aaron I
Aaron IOP15mo ago
Oh you mean like this. Well it is a server.
Pobiega
Pobiega15mo ago
I'd run a background job that promotes occurances to real events then, if you need a strong level of historic data and querying
Aaron I
Aaron IOP15mo ago
I think I have an idea just need to test it, It would be a record which be held within the event and nullable, depending on it features it would contain data, but it wouldn't update just hold the regular time stamps it occurs in, if it's one time it'll contain a full DateTime
Want results from more Discord servers?
Add your server