Related
I am working on dates in Laravel. I have to set dates for patient future injections.
To keep it simple, let's suppose today is 13-03-2019 (Wednesday).
I created first date as:
$firstDate = Carbon::create(2019,03 ,18, 12); // The day is Monday
// set date
Carbon::setTestNow($firstDate);
Now I want the next two appointments should be on Wednesday and Friday. So I again set the dates as follow:
// set second date
$secondDate = new Carbon('Wednesday');
Carbon::setTestNow($secondDate);
// set thirdDate
$thirdDate = new Carbon('Friday');
Carbon::setTestNow($thirdDate);
According to above example the output should be:
2019-03-18
2019-03-20
2019-03-22
But the problem is that it outputs the first set date correct but print the 2nd and 3rd date wrong as it considers 'Wednesday' of next week as today's date.
So the Output print as:
2019-03-18
2019-03-13
2019-03-14
I have spent a lot of time on it, I would appreciate if anyone of you people could help me in this.
I would appreciate if anyone guides me where I am going wrong.
Thanks.
As the setTestNow() function was not working for 2nd and third date/days, so I first get all three required days then convert them to 'dayOfWeek' which returns day number (Sunday 0, Monday 1 and so on...). I subtracted the first day from second and third day and then finally add these days to the date that i get from the datepicker.
// set the start date
if( $visitstart_date != null && $visitstart_date != '') {
Carbon::setTestNow($visitstart_date);
} else {
Carbon::setTestNow();
}
if($perweek_visit1_day != '')
{
//Get first selected day number
$firstDay = Carbon::parse($perweek_visit1_day)->dayOfWeek;
$perweek_visit1_dayDate = Carbon::now();
}
if($perweek_visit2_day != '')
{
//Get second day numer
$secondDay = Carbon::parse($perweek_visit2_day)->dayOfWeek - $firstDay;
$perweek_visit2_dayDate = Carbon::now()->addDays($secondDay);
}
if($perweek_visit3_day != '')
{
//Get third day number
$thirdDay = Carbon::parse($perweek_visit3_day)->dayOfWeek - $firstDay;
$perweek_visit3_dayDate = Carbon::now()->addDays($thirdDay);
}
Using Vue js and v-validate how can I determine if the date of birth is greater than 21 and less then 55 years old? Any help is greatly appreciated.
import * as moment from "moment";
let birthday = moment(moment.now()).diff(moment(this.user.day + this.user.month + this.user.year, "DD.MM.YYYY"),"years");
if(birthday > 21 && birthday < 55) {
// do next steps
}
Moment(npm install moment) is used to parse, manipulate & display dates and times in JavaScript. moment.now() will give the present date and assuming you have three fields for day, month and year in different variables, format it and use the diff function to find the age.
The example from Madhuri works for me but i change the input as hard coded string.
enter code here let birthday = moment(moment.now()).diff(moment('01.01.1990', "DD.MM.YYYY"), "years");
if(birthday >= 20 && birthday <=70 ){
return birthday;
}
}
I solved it with with value from form input which works for me.
checkBirthday(){
const birthDayDate = document.getElementById("birthdate").value;
const age = moment().diff(birthDayDate, "years");
// let birthday = moment(moment.now()).diff(moment('01.01.1990', "DD.MM.YYYY"), "years");
if(age >= 18 && age <=74 ){
return age;
}
},
The topic at hand is a messy domain-specific problem working with dates in Oracle's ERP software called JD Edwards. Its detail is documented in this question.
Before writing wrapper classes for handling the dates and times from JD Edwards, I want to know if JodaTime or Java 8 introduced any special support for this unique time format, or if I'll have to do significant string manipulation regardless of the libraries I use.
This is an obscure problem, so please only respond if you have specific knowledge of this problem, and/or JodaTime/Java 8/JSR 310.
ADDITION:
Per Basil Bourque's request, adding example of timestamps that accompany said dates. Here are two example of date/time fields from different tables:
JCSBMDATE:115100, JCSBMTIME:120102.0
RLUPMJ:114317, RLUPMT:141805.0
Also, the date variable is being cast as a BigDecimal and the time is a Double. So, I'll probably keep the string parsers around, but also write factory methods that take the BigDecimal/Double values natively as well.
It seems that the time field is actually the number of Milliseconds (not seconds) from the start of the day, and the ".0" can be ignored. So, one will have to perform a conversion and calculation like so:
localDate.atTime(LocalTime.ofNanoOfDay(Long.parseLong(jdeTime) * 1000000))
JD Edwards date defined
Actually the detail of a JD Edwards date is not so gory, according to this simple description on a page at Oracle.com:
About the Julian Date Format
Date fields in JD Edwards World files are stored in the Julian format. …
The Julian (*JUL) date format is CYYDDD, where:
C is added to 19 to create the century, i.e. 0 + 19 = 19, 1 + 19 = 20. YY is the year within the century, DDD is the day in the year.
Terms:
I would call the C part a “century-offset”, how many centuries to add to 19. Use 0 for 19xx years, and 1 for 20xx years.
The java.time framework calls the DDD a “DayOfYear”, and “ordinal date” is another term. The use of “Julian” for a day-number-within-a-year is common but not correct, conflicting with a Julian Day.
The java.time framework does not include direct support for parsing or generating strings of this format, not that I can find.
JulianFields
There is the java.time.temporal.JulianFields but those are for an redefined version of Julian dates where we count the number of days from an epoch (1970-01-01 (ISO) rather than the historic November 24, 4714 BC (proleptic Gregorian)), while ignoring years altogether. So this has nothing to do with the JD Edwards definition, contrary to some incorrect advice on that page linked in the Question.
Ordinal Date
This JD Edwards date is a version of an ordinal date. The ordinal date is sometimes referred to casually (and incorrectly) as a "julian" date only because it shares the idea of counting a sequence of days. But an ordinal date counts days from the beginning of the year to end of year for a number always between 1 and 365/366 (leap year), not counting since some epoch and growing into a number into the thousands.
Back to the Question, handling the JD Edwards date in java.time…
No, I do not find any direct or indirect support the JD Edwards date built into java.time.
The java.date.format package seems unaware of the century of a date, only the year and the era. So no way that I can find to define the C part of a JD Edwards date.
The last part of a JD Edwards date, the ordinal number of days in the year, is well-handled with within both the date-time classes and the formatting classes.
Wrap LocalDate
Since a JD Edwards date apparently has the same logic as the ISO chronology used by java.time, the only real issue at hand is parsing and generating String objects according to this particular format. All other behavior can be leveraged from a LocalDate.
Since I cannot find a way to define a java.time.format.DateTimeFormatter for this purpose, I suggest writing a utility class to handle these chores.
Ideally we would extend the LocalDate class, overriding its parse and toString methods. And perhaps a getCenturyOffset method. But the LocalDate class is marked final and cannot be extended. So I would create something like this class shown below, wrapping a LocalDate.
CAVEAT: Use at your own risk. Fresh code, barely run, hardly tested. Meant as an example, not for use in production. Use according to terms of the ISC License.
package com.example.whatever;
import java.time.LocalDate;
import java.time.ZoneId;
/**
* Wraps a 'LocalDate' to provide parsing/generating of strings in format known
* as JD Edwards date.
*
* Format is CYYDDD where C is the number of centuries from 1900, YY is the year
* within that century, and DDD is the ordinal day within the year (1-365 or
* 1-366 in Leap Year).
*
* Immutable object. Thread-safe (hopefully! No guarantees).
*
* I would rather have done this by extending the 'java.time.LocalDate' class, but that class is marked 'final'.
*
* Examples: '000001' is January 1 of 1900. '116032' is February 1, 2016.
*
* © 2016 Basil Bourque. This source code may be used according to terms of the ISC License at https://opensource.org/licenses/ISC
*
* #author Basil Bourque
*/
public class JDEdwardsLocalDate {
private LocalDate localDate = null;
private int centuryOffset;
private int yearOfCentury;
private String formatted = null;
// Static Factory method, in lieu of public constructor.
static public JDEdwardsLocalDate from ( LocalDate localDateArg ) {
return new JDEdwardsLocalDate ( localDateArg );
}
// Static Factory method, in lieu of public constructor.
static public JDEdwardsLocalDate parse ( CharSequence charSequenceArg ) {
if ( null == charSequenceArg ) {
throw new IllegalArgumentException ( "Passed CharSequence that is null. Message # 0072f897-b05f-4a0e-88d9-57cfd63a712c." );
}
if ( charSequenceArg.length () != 6 ) {
throw new IllegalArgumentException ( "Passed CharSequence that is not six characters in length. Message # eee1e134-8ec9-4c92-aff3-9296eac1a84a." );
}
String string = charSequenceArg.toString ();
// Should have all digits. Test by converting to an int.
try {
int testAsInteger = Integer.parseInt ( string );
} catch ( NumberFormatException e ) {
throw new IllegalArgumentException ( "Passed CharSequence contains non-digits. Fails to convert to an integer value. Message # 0461f0ee-b6d6-451c-8304-6ceface05332." );
}
// Validity test passed.
// Parse.
int centuryOffset = Integer.parseInt ( string.substring ( 0 , 1 ) ); // Plus/Minus from '19' (as in '1900').
int yearOfCentury = Integer.parseInt ( string.substring ( 1 , 3 ) );
int ordinalDayOfYear = Integer.parseInt ( string.substring ( 3 ) );
int centuryStart = ( ( centuryOffset + 19 ) * 100 ); // 0 -> 1900. 1 -> 2000. 2 -> 2100.
int year = ( centuryStart + yearOfCentury );
LocalDate localDate = LocalDate.ofYearDay ( year , ordinalDayOfYear );
return new JDEdwardsLocalDate ( localDate );
}
// Constructor.
private JDEdwardsLocalDate ( LocalDate localDateArg ) {
this.localDate = localDateArg;
// Calculate century offset, how many centuries plus/minus from 1900.
int year = this.localDate.getYear ();
int century = ( year / 100 );
this.yearOfCentury = ( year - ( century * 100 ) ); // example: if 2016, return 16.
this.centuryOffset = ( century - 19 );
// Format as string.
String paddedYearOfCentury = String.format ( "%02d" , this.yearOfCentury );
String paddedDayOfYear = String.format ( "%03d" , this.localDate.getDayOfYear () );
this.formatted = ( this.centuryOffset + paddedYearOfCentury + paddedDayOfYear );
}
#Override
public String toString () {
return this.formatted;
}
public LocalDate toLocalDate () {
// Returns a java.time.LocalDate which shares the same ISO chronology as a JD Edwards Date.
return this.localDate;
}
public int getDayOfYear () {
// Returns ordinal day number within the year, 1-365 inclusive or 1-366 for Leap Year.
return this.localDate.getDayOfYear();
}
public int getYear () {
// Returns a year number such as 2016.
return this.localDate.getYear();
}
public int getYearOfCentury () {
// Returns a number within 0 and 99 inclusive.
return this.yearOfCentury;
}
public int getCenturyOffset () {
// Returns 0 for 19xx dates, 1 for 20xx dates, 2 for 21xx dates, and so on.
return this.centuryOffset;
}
public static void main ( String[] args ) {
// '000001' is January 1, 1900.
JDEdwardsLocalDate jde1 = JDEdwardsLocalDate.parse ( "000001" );
System.out.println ( "'000001' = JDEdwardsLocalDate: " + jde1 + " = LocalDate: " + jde1.toLocalDate () + " Should be: January 1, 1900. " );
// '116032' is February 1, 2016.
JDEdwardsLocalDate jde2 = JDEdwardsLocalDate.parse ( "116032" );
System.out.println ( "'116032' = JDEdwardsLocalDate: " + jde2 + " = LocalDate: " + jde2.toLocalDate () + " Should be: February 1, 2016." );
// Today
LocalDate today = LocalDate.now ( ZoneId.systemDefault () );
JDEdwardsLocalDate jdeToday = JDEdwardsLocalDate.from ( today );
System.out.println ( "LocalDate.now(): " + today + " = JDEdwardsLocalDate: " + jdeToday + " to LocalDate: " + jdeToday.toLocalDate () );
}
}
When run.
'000001' = JDEdwardsLocalDate: 000001 = LocalDate: 1900-01-01 Should be: January 1, 1900.
'116032' = JDEdwardsLocalDate: 116032 = LocalDate: 2016-02-01 Should be: February 1, 2016.
LocalDate.now(): 2016-05-09 = JDEdwardsLocalDate: 116130 to LocalDate: 2016-05-09
JD Edwards time-of-day
As for JD Edwards time-of-day formats, I searched and could not find any documentation. If you know of some, please edit your Question to add links. The only mentions of JDE times seemed to be a count of seconds from midnight.
If that is the case (a count since midnight), the java.time.LocalTime class has you covered. A LocalTime can be instantiated and read as either:
Whole seconds since start of day ( withSecond, ofSecondOfDay )
Fractional seconds since start of day, with a resolution of nanoseconds ( withNano, ofNanoOfDay )
Nanosecond resolution means up to nine digits of a decimal fraction. No problem handling the six digits you mentioned. Just do the math, multiply/divide by 1_000L. Just be aware that means possible data loss as you could be truncating those last three digits of fraction (7th, 8th, 9th digits of decimal fraction) if the LocalTime value came from outside of JD Edwards data. [FYI, the old java.util.Date/.Calendar classes, as well as Joda-Time, are limited to milliseconds resolution, for three digits of decimal fraction.]
Not recommended: You could do some kind of combo class, composed of a LocalDate and a LocalTime. Or use a LocalDateTime. The key issue is time zone. If a JD Edwards date-time is always in a certain time zone such as UTC, then it might make sense to combine and use an OffsetDateTime . But if it has no specific time zone context, if the values are just a fuzzy idea of a date-time rather than specific points on the timeline, then use LocalDateTime as it has no time zone. If a JDE is always in UTC, use OffsetDateTime set to ZoneOffset.UTC. If you want to specify a time zone (an offset plus rules for handling anomalies such as DST), use ZonedDateTime.
Recommended: Use a LocalTime separately. I do not think you want to be using my JDEdwardsLocalDate class in your business logic, especially because it is not a full implementation fitting into the java.time framework. My intention is to use that class to immediately convert to LocalDate when you encounter a JDE date. Same goes for a JDE time-of-day, convert to LocalTime immediately. If their context is always UTC, create an OffsetDateTime with UTC, and then pass that around your business logic. Only go back to a JDE date & time when necessary (persisting to database column of that JDE type, or reporting to user expecting that JDE presentation).
OffsetDateTime odt = OffsetDateTime.of( myLocalDate , myLocalTime , ZoneOffset.UTC );
If the JDE date & time has some other context implied, then assign the intended time zone.
ZoneId zoneId = ZoneId.of( "America/Montreal" );
ZonedDateTime zdt = ZonedDateTime.of( myLocalDate , myLocalTime , zoneId );
Time zone is crucial here. You must understand the concepts in general. Be clear that LocalDate and LocalTime and LocalDateTime are not a moment on the timeline. They have no specific meaning until you adjust them into a time zone (or at least an offset-from-UTC).
My diagram of date-time types included on this Answer may help you if not familiar with the java.time types.
And you must understand the meaning of JDE date & time and their use in your apps/databases. As I could not find anything about JDE time, I could not learn anything about the JD Edwards intentions towards time zones. So I cannot suggest anything more specific.
No: Neither Joda Time nor Java 8 have support for JD Edwards time representations.
Does anyone know of a good library (or code snippet) for converting a TimeSpan object to a "friendly" string such as:
Two years, three months and four days
One week and two days
(It's for a document expiry system, where the expiry could be anything from a few days to several decades)
Just to clarify, say I had a TimeSpan with 7 days, that should print "1 week", 14 days "2 weeks", 366 days "1 year and 1 day", etc etc.
I just stumbled upon this question because I wanted to do a similar thing. After some googling I still didn't find what I wanted: display a timespan in a sort of "rounded" fashion. I mean: when some event took several days, it doesn't always make sense to display the milliseconds. However, when it took minutes, it probably does. And in that case, I don't want 0 days and 0 hours to be displayed. So, I want to parametrize the number of relevant timespan parts to be displayed. This resulted in this bit of code:
public static class TimeSpanExtensions
{
private enum TimeSpanElement
{
Millisecond,
Second,
Minute,
Hour,
Day
}
public static string ToFriendlyDisplay(this TimeSpan timeSpan, int maxNrOfElements)
{
maxNrOfElements = Math.Max(Math.Min(maxNrOfElements, 5), 1);
var parts = new[]
{
Tuple.Create(TimeSpanElement.Day, timeSpan.Days),
Tuple.Create(TimeSpanElement.Hour, timeSpan.Hours),
Tuple.Create(TimeSpanElement.Minute, timeSpan.Minutes),
Tuple.Create(TimeSpanElement.Second, timeSpan.Seconds),
Tuple.Create(TimeSpanElement.Millisecond, timeSpan.Milliseconds)
}
.SkipWhile(i => i.Item2 <= 0)
.Take(maxNrOfElements);
return string.Join(", ", parts.Select(p => string.Format("{0} {1}{2}", p.Item2, p.Item1, p.Item2 > 1 ? "s" : string.Empty)));
}
}
Example (LinqPad):
new TimeSpan(1,2,3,4,5).ToFriendlyDisplay(3).Dump();
new TimeSpan(0,5,3,4,5).ToFriendlyDisplay(3).Dump();
Displays:
1 Day, 2 Hours, 3 Minutes
5 Hours, 3 Minutes, 4 Seconds
Suits me, see if it suits you.
Not a fully featured implementation, but it should get you close enough.
DateTime dtNow = DateTime.Now;
DateTime dtYesterday = DateTime.Now.AddDays(-435.0);
TimeSpan ts = dtNow.Subtract(dtYesterday);
int years = ts.Days / 365; //no leap year accounting
int months = (ts.Days % 365) / 30; //naive guess at month size
int weeks = ((ts.Days % 365) % 30) / 7;
int days = (((ts.Days % 365) % 30) % 7);
StringBuilder sb = new StringBuilder();
if(years > 0)
{
sb.Append(years.ToString() + " years, ");
}
if(months > 0)
{
sb.Append(months.ToString() + " months, ");
}
if(weeks > 0)
{
sb.Append(weeks.ToString() + " weeks, ");
}
if(days > 0)
{
sb.Append(days.ToString() + " days.");
}
string FormattedTimeSpan = sb.ToString();
In the end, do you really need to let someone know a document is going to expire exactly 1 year, 5 months, 2 weeks, and 3 days from now? Can't you get by with telling them the document will expire over 1 year from now, or over 5 months from now? Just take the largest unit and say over n of that unit.
There is now also the Humanizer project that looks very interesting that can do this and way more.
Here is my solution to this. It is based on other answers in this thread, with added support for year and month as that was requested in the original question (and was what I needed).
As for the discussion whether or not this makes sense I would say that there are cases where it does so. In my case we wanted to show the duration of agreements that in some cases are just a few days, and in other cases several years.
Tests;
[Test]
public void ToFriendlyDuration_produces_expected_result()
{
new DateTime(2019, 5, 28).ToFriendlyDuration(null).Should().Be("Until further notice");
new DateTime(2019, 5, 28).ToFriendlyDuration(new DateTime(2020, 5, 28)).Should().Be("1 year");
new DateTime(2019, 5, 28).ToFriendlyDuration(new DateTime(2021, 5, 28)).Should().Be("2 years");
new DateTime(2019, 5, 28).ToFriendlyDuration(new DateTime(2021, 8, 28)).Should().Be("2 years, 3 months");
new DateTime(2019, 5, 28).ToFriendlyDuration(new DateTime(2019, 8, 28)).Should().Be("3 months");
new DateTime(2019, 5, 28).ToFriendlyDuration(new DateTime(2019, 8, 31)).Should().Be("3 months, 3 days");
new DateTime(2019, 5, 1).ToFriendlyDuration(new DateTime(2019, 5, 31)).Should().Be("30 days");
new DateTime(2010, 5, 28).ToFriendlyDuration(new DateTime(2020, 8, 28)).Should().Be("10 years, 3 months");
new DateTime(2010, 5, 28).ToFriendlyDuration(new DateTime(2020, 5, 29)).Should().Be("10 years, 1 day");
}
Implementation;
private class TermAndValue
{
public TermAndValue(string singular, string plural, int value)
{
Singular = singular;
Plural = plural;
Value = value;
}
public string Singular { get; }
public string Plural { get; }
public int Value { get; }
public string Term => Value > 1 ? Plural : Singular;
}
public static string ToFriendlyDuration(this DateTime value, DateTime? endDate, int maxNrOfElements = 2)
{
if (!endDate.HasValue)
return "Until further notice";
var extendedTimeSpan = new TimeSpanWithYearAndMonth(value, endDate.Value);
maxNrOfElements = Math.Max(Math.Min(maxNrOfElements, 5), 1);
var termsAndValues = new[]
{
new TermAndValue("year", "years", extendedTimeSpan.Years),
new TermAndValue("month", "months", extendedTimeSpan.Months),
new TermAndValue("day", "days", extendedTimeSpan.Days),
new TermAndValue("hour", "hours", extendedTimeSpan.Hours),
new TermAndValue("minute", "minutes", extendedTimeSpan.Minutes)
};
var parts = termsAndValues.Where(i => i.Value != 0).Take(maxNrOfElements);
return string.Join(", ", parts.Select(p => $"{p.Value} {p.Term}"));
}
internal class TimeSpanWithYearAndMonth
{
internal TimeSpanWithYearAndMonth(DateTime startDate, DateTime endDate)
{
var span = endDate - startDate;
Months = 12 * (endDate.Year - startDate.Year) + (endDate.Month - startDate.Month);
Years = Months / 12;
Months -= Years * 12;
if (Months == 0 && Years == 0)
{
Days = span.Days;
}
else
{
var startDateExceptYearsAndMonths = startDate.AddYears(Years);
startDateExceptYearsAndMonths = startDateExceptYearsAndMonths.AddMonths(Months);
Days = (endDate - startDateExceptYearsAndMonths).Days;
}
Hours = span.Hours;
Minutes = span.Minutes;
}
public int Minutes { get; }
public int Hours { get; }
public int Days { get; }
public int Years { get; }
public int Months { get; }
}
I know this is old, but I wanted to answer with a great nuget package.
Install-Package Humanizer
https://www.nuget.org/packages/Humanizer
https://github.com/MehdiK/Humanizer
Example from their readme.md
TimeSpan.FromMilliseconds(1299630020).Humanize(4) => "2 weeks, 1 day, 1 hour, 30 seconds"
#ian-becker Needs the credit
The TimeSpan object has Days, Hours, Minutes, and Seconds properties on it, so it wouldn't be too hard to make a snippet that formats those values to a friendly string.
Unfortunately Days is the largest value. Anything longer than that and you'll have to start worrying about days in a month for every year...etc. You're better off stopping at days in my opinion (the added effort doesn't seem worth the gain).
UPDATE
...I figured I'd bring this up from my own comment:
Understandable, but is "This document expires in 10 years, 3 months, 21 days, 2 hours, and 30 minutes" really any more helpful or less silly? If it were up to me, since neither representation seems very useful...I'd leave off the timespan for expiry until the date got reasonably close (30 or 60 days maybe if you're worried about getting the document updated). Seems a much better UX choice to me.
It probably won't do everything you are looking for, but in v4 Microsoft will be implementing IFormattable on TimeSpan.
To format any period longer than 1 day (i.e. month/year/decade etc.) a Timespan object is not enough.
Suppose your timespan is 35 days, then from Apr 1 you would get one month and five days, whereas from Dec 1 you would get one month and four days.
Yes! I needed the same so many times then now I create my onw package and published it in Nuget. You are welcome to use it.
The package name is EstecheAssemblies
It's easy to implement:
using EstecheAssemblies;
var date = new DateTime("2019-08-08 01:03:21");
var text = date.ToFriendlyTimeSpan();
See how-do-i-calculate-relative-time, asked (by user number 1) a year ago when SO was young and not public.
It was (probably) the basis for the current age display for SO questions and answers.
Closed. This question is opinion-based. It is not currently accepting answers.
Want to improve this question? Update the question so it can be answered with facts and citations by editing this post.
Closed 1 year ago.
The community reviewed whether to reopen this question 1 year ago and left it closed:
Original close reason(s) were not resolved
Improve this question
I'm building a group calendar application that needs to support recurring events, but all the solutions I've come up with to handle these events seem like a hack. I can limit how far ahead one can look, and then generate all the events at once. Or I can store the events as repeating and dynamically display them when one looks ahead on the calendar, but I'll have to convert them to a normal event if someone wants to change the details on a particular instance of the event.
I'm sure there's a better way to do this, but I haven't found it yet. What's the best way to model recurring events, where you can change details of or delete particular event instances?
(I'm using Ruby, but please don't let that constrain your answer. If there's a Ruby-specific library or something, though, that's good to know.)
I would use a 'link' concept for all future recurring events. They are dynamically displayed in the calendar and link back to a single reference object. When events have taken place the link is broken and the event becomes a standalone instance. If you attempt to edit a recurring event then prompt to change all future items (i.e. change single linked reference) or change just that instance (in which case convert this to a standalone instance and then make change). The latter cased is slightly problematic as you need to keep track in your recurring list of all future events that were converted to single instance. But, this is entirely do-able.
So, in essence, have 2 classes of events - single instances and recurring events.
I have developed multiple calendar-based applications, and also authored a set of reusable JavaScript calendar components that support recurrence. I wrote up an overview of how to design for recurrence that might be helpful to someone. While there are a few bits that are specific to the library I wrote, the vast majority of the advice offered is general to any calendar implementation.
Some of the key points:
Store recurrence using the iCal RRULE format -- that's one wheel you really don't want to reinvent
Do NOT store individual recurring event instances as rows in your database! Always store a recurrence pattern.
There are many ways to design your event/exception schema, but a basic starting point example is provided
All date/time values should be stored in UTC and converted to local for display
The end date stored for a recurring event should always be the end date of the recurrence range (or your platform's "max date" if recurring "forever") and the event duration should be stored separately. This is to ensure a sane way of querying for events later. Read the linked article for more details about this.
Some discussion around generating event instances and recurrence editing strategies is included
It's a really complicated topic with many, many valid approaches to implementing it. I will say that I've actually implemented recurrence several times successfully, and I would be wary of taking advice on this subject from anyone who hasn't actually done it.
There can be many problems with recurring events, let me highlight a few that I know of.
Solution 1 - no instances
Store original appointment + recurrence data, do not store all the instances.
Problems:
You'll have to calculate all the instances in a date window when you need them, costly
Unable to handle exceptions (ie. you delete one of the instances, or move it, or rather, you can't do this with this solution)
Solution 2 - store instances
Store everything from 1, but also all the instances, linked back to the original appointment.
Problems:
Takes a lot of space (but space is cheap, so minor)
Exceptions must be handled gracefully, especially if you go back and edit the original appointment after making an exception. For instance, if you move the third instance one day forward, what if you go back and edit the time of the original appointment, re-insert another on the original day and leave the moved one? Unlink the moved one? Try to change the moved one appropriately?
Of course, if you're not going to do exceptions, then either solution should be fine, and you basically choose from a time/space trade off scenario.
You may want to look at iCalendar software implementations or the standard itself (RFC 2445 RFC 5545).
Ones to come to mind quickly are the Mozilla projects http://www.mozilla.org/projects/calendar/ A quick search reveals http://icalendar.rubyforge.org/ as well.
Other options can be considered depending on how you're going to store the events. Are you building your own database schema? Using something iCalendar-based, etc.?
I'm working with the following:
http://github.com/elevation/event_calendar - model and helper for a calendar
http://github.com/seejohnrun/ice_cube - awesome recurring gem
http://github.com/justinfrench/formtastic - easy forms
and a gem in progress that extends formtastic with an input type :recurring (form.schedule :as => :recurring), which renders an iCal-like interface and a before_filter to serialize the view into an IceCube object again, ghetto-ly.
My idea is to make it incredibility easy to add recurring attributes to a model and connect it easily in the view. All in a couple of lines.
So what does this give me? Indexed, Edit-able, Recurring attributes.
events stores a single day instance, and is used in the calendar view/helper
say task.schedule stores the yaml'd IceCube object, so you can do calls like : task.schedule.next_suggestion.
Recap: I use two models, one flat, for the calendar display, and one attribute'd for the functionality.
I'm using the database schema as described below to store the recurrence parameters
http://github.com/bakineggs/recurring_events_for
Then I use runt to dynamically calculate the dates.
https://github.com/mlipper/runt
Keep track of a recurrence rule (probably based on iCalendar, per #Kris K.). This will include a pattern and a range (Every third Tuesday, for 10 occurrences).
For when you want to edit/delete a specific occurrence, keep track of exception dates for the above recurrence rule (dates where the event doesn't occur as the rule specifies).
If you deleted, that's all you need, if you edited, create another event, and give it a parent ID set to the main event. You can choose whether to include all of the main event's information in this record, or if it only holds the changes and inherits everything that doesn't change.
Note that if you allow recurrence rules that don't end, you have to think about how to display your now infinite amount of information.
Hope that helps!
I'd recommend using the power of the date library and the semantics of the range module of ruby. A recurring event is really a time, a date range (a start & end) and usually a single day of the week. Using date & range you can answer any question:
#!/usr/bin/ruby
require 'date'
start_date = Date.parse('2008-01-01')
end_date = Date.parse('2008-04-01')
wday = 5 # friday
(start_date..end_date).select{|d| d.wday == wday}.map{|d| d.to_s}.inspect
Produces all days of the event, including the leap year!
# =>"[\"2008-01-04\", \"2008-01-11\", \"2008-01-18\", \"2008-01-25\", \"2008-02-01\", \"2008-02-08\", \"2008-02-15\", \"2008-02-22\", \"2008-02-29\", \"2008-03-07\", \"2008-03-14\", \"2008-03-21\", \"2008-03-28\"]"
From these answers, I've sort of sifted out a solution. I really like the idea of the link concept. Recurring events could be a linked list, with the tail knowing its recurrence rule. Changing one event would then be easy, because the links stay in place, and deleting an event is easy as well - you just unlink an event, delete it, and re-link the event before and after it. You still have to query recurring events every time someone looks at a new time period never been looked at before on the calendar, but otherwise this is pretty clean.
You could store the events as repeating, and if a particular instance was edited, create a new event with the same event ID. Then when looking up the event, search for all events with the same event ID to get all the information. I'm not sure if you rolled your own event library, or if you're using an existing one so it may not be possible.
Check the article below for three good ruby date/time libraries.
ice_cube in particular seems a solid choice for recurrence rules and other stuff that an event calendar would need.
http://www.rubyinside.com/3-new-date-and-time-libraries-for-rubyists-3238.html
In javascript:
Handling recurring schedules:
http://bunkat.github.io/later/
Handling complex events and dependencies between those schedules:
http://bunkat.github.io/schedule/
Basically, you create the rules then you ask the lib to compute the next N recurring events (specifying a date range or not). The rules can be parsed / serialised for saving them into your model.
If you have a recurring event and would like to modify only one recurrence you can use the except() function to dismiss a particular day and then add a new modified event for this entry.
The lib supports very complex patterns, timezones and even croning events.
Store the events as repeating and dynamically display them, however allow the recurring event to contain a list of specific events that could override the default information on a specific day.
When you query the recurring event it can check for a specific override for that day.
If a user makes changes, then you can ask if he wants to update for all instances (default details) or just that day (make a new specific event and add it to the list).
If a user asks to delete all recurrences of this event you also have the list of specifics to hand and can remove them easily.
The only problematic case would be if the user wants to update this event and all future events. In which case you'll have to split the recurring event into two. At this point you may want to consider linking recurring events in some way so you can delete them all.
For .NET programmers who are prepared to pay some licensing fees, you might find Aspose.Network useful... it includes an iCalendar compatible library for recurring appointments.
You store the events in iCalendar format directly, which allows for open-ended repetition, time-zone localisation and so forth.
You could store these in a CalDAV server and then when you want to display the events you can use the option of the report defined in CalDAV to ask the server to do the expansion of the recurring events across the viewed period.
Or you could store them in a database yourself and use some kind of iCalendar parsing library to do the expansion, without needing the PUT/GET/REPORT to talk to a backend CalDAV server. This is probably more work - I'm sure CalDAV servers hide complexity somewhere.
Having the events in iCalendar format will probably make things simpler in the long run as people will always want them to be exported for putting in other software anyway.
I have Simply implemented this feature! Logic is as follows, first you need two tables. RuleTable store general or recycle paternal events. ItemTable is stored cycle events. For example, when you create a cyclic event, the start time for 6 November 2015, the end time for the December 6 (or forever), cycle for one week. You insert data into a RuleTable, fields are as follows:
TableID: 1 Name: cycleA
StartTime: 6 November 2014 (I kept thenumber of milliseconds),
EndTime: 6 November 2015 (if it is repeated forever, and you can keep the value -1)
Cycletype: WeekLy.
Now you want to query November 20 to December 20 data. You can write a function RecurringEventBE (long start, long end), based on the starting and ending time, WeekLy, you can calculate the collection you want, < cycleA11.20, cycleA 11.27, cycleA 12.4 ......>.
In addition to November 6, and the rest I called him a virtual event. When the user changes a virtual event' name after (cycleA11.27 for example), you insert a data into a ItemTable. Fields are as follows:
TableID: 1
Name, cycleB
StartTime, 27 November 2014
EndTime,November 6 2015
Cycletype, WeekLy
Foreignkey, 1 (pointingto the table recycle paternal events).
In function RecurringEventBE (long start, long end), you use this data covering virtual event (cycleB11.27)
sorry about my english, I tried.
This is my RecurringEventBE:
public static List<Map<String, Object>> recurringData(Context context,
long start, long end) { // 重复事件的模板处理,生成虚拟事件(根据日期段)
long a = System.currentTimeMillis();
List<Map<String, Object>> finalDataList = new ArrayList<Map<String, Object>>();
List<Map<String, Object>> tDataList = BillsDao.selectTemplateBillRuleByBE(context); //RuleTable,just select recurringEvent
for (Map<String, Object> iMap : tDataList) {
int _id = (Integer) iMap.get("_id");
long bk_billDuedate = (Long) iMap.get("ep_billDueDate"); // 相当于事件的开始日期 Start
long bk_billEndDate = (Long) iMap.get("ep_billEndDate"); // 重复事件的截止日期 End
int bk_billRepeatType = (Integer) iMap.get("ep_recurringType"); // recurring Type
long startDate = 0; // 进一步精确判断日记起止点,保证了该段时间断获取的数据不未空,减少不必要的处理
long endDate = 0;
if (bk_billEndDate == -1) { // 永远重复事件的处理
if (end >= bk_billDuedate) {
endDate = end;
startDate = (bk_billDuedate <= start) ? start : bk_billDuedate; // 进一步判断日记起止点,这样就保证了该段时间断获取的数据不未空
}
} else {
if (start <= bk_billEndDate && end >= bk_billDuedate) { // 首先判断起止时间是否落在重复区间,表示该段时间有重复事件
endDate = (bk_billEndDate >= end) ? end : bk_billEndDate;
startDate = (bk_billDuedate <= start) ? start : bk_billDuedate; // 进一步判断日记起止点,这样就保证了该段时间断获取的数据不未空
}
}
Calendar calendar = Calendar.getInstance();
calendar.setTimeInMillis(bk_billDuedate); // 设置重复的开始日期
long virtualLong = bk_billDuedate; // 虚拟时间,后面根据规则累加计算
List<Map<String, Object>> virtualDataList = new ArrayList<Map<String, Object>>();// 虚拟事件
if (virtualLong == startDate) { // 所要求的时间,小于等于父本时间,说明这个是父事件数据,即第一条父本数据
Map<String, Object> bMap = new HashMap<String, Object>();
bMap.putAll(iMap);
bMap.put("indexflag", 1); // 1表示父本事件
virtualDataList.add(bMap);
}
long before_times = 0; // 计算从要求时间start到重复开始时间的次数,用于定位第一次发生在请求时间段落的时间点
long remainder = -1;
if (bk_billRepeatType == 1) {
before_times = (startDate - bk_billDuedate) / (7 * DAYMILLIS);
remainder = (startDate - bk_billDuedate) % (7 * DAYMILLIS);
} else if (bk_billRepeatType == 2) {
before_times = (startDate - bk_billDuedate) / (14 * DAYMILLIS);
remainder = (startDate - bk_billDuedate) % (14 * DAYMILLIS);
} else if (bk_billRepeatType == 3) {
before_times = (startDate - bk_billDuedate) / (28 * DAYMILLIS);
remainder = (startDate - bk_billDuedate) % (28 * DAYMILLIS);
} else if (bk_billRepeatType == 4) {
before_times = (startDate - bk_billDuedate) / (15 * DAYMILLIS);
remainder = (startDate - bk_billDuedate) % (15 * DAYMILLIS);
} else if (bk_billRepeatType == 5) {
do { // 该段代码根据日历处理每天重复事件,当事件比较多的时候效率比较低
Calendar calendarCloneCalendar = (Calendar) calendar
.clone();
int currentMonthDay = calendarCloneCalendar
.get(Calendar.DAY_OF_MONTH);
calendarCloneCalendar.add(Calendar.MONTH, 1);
int nextMonthDay = calendarCloneCalendar
.get(Calendar.DAY_OF_MONTH);
if (currentMonthDay > nextMonthDay) {
calendar.add(Calendar.MONTH, 1 + 1);
virtualLong = calendar.getTimeInMillis();
} else {
calendar.add(Calendar.MONTH, 1);
virtualLong = calendar.getTimeInMillis();
}
} while (virtualLong < startDate);
} else if (bk_billRepeatType == 6) {
do { // 该段代码根据日历处理每天重复事件,当事件比较多的时候效率比较低
Calendar calendarCloneCalendar = (Calendar) calendar
.clone();
int currentMonthDay = calendarCloneCalendar
.get(Calendar.DAY_OF_MONTH);
calendarCloneCalendar.add(Calendar.MONTH, 2);
int nextMonthDay = calendarCloneCalendar
.get(Calendar.DAY_OF_MONTH);
if (currentMonthDay > nextMonthDay) {
calendar.add(Calendar.MONTH, 2 + 2);
virtualLong = calendar.getTimeInMillis();
} else {
calendar.add(Calendar.MONTH, 2);
virtualLong = calendar.getTimeInMillis();
}
} while (virtualLong < startDate);
} else if (bk_billRepeatType == 7) {
do { // 该段代码根据日历处理每天重复事件,当事件比较多的时候效率比较低
Calendar calendarCloneCalendar = (Calendar) calendar
.clone();
int currentMonthDay = calendarCloneCalendar
.get(Calendar.DAY_OF_MONTH);
calendarCloneCalendar.add(Calendar.MONTH, 3);
int nextMonthDay = calendarCloneCalendar
.get(Calendar.DAY_OF_MONTH);
if (currentMonthDay > nextMonthDay) {
calendar.add(Calendar.MONTH, 3 + 3);
virtualLong = calendar.getTimeInMillis();
} else {
calendar.add(Calendar.MONTH, 3);
virtualLong = calendar.getTimeInMillis();
}
} while (virtualLong < startDate);
} else if (bk_billRepeatType == 8) {
do {
calendar.add(Calendar.YEAR, 1);
virtualLong = calendar.getTimeInMillis();
} while (virtualLong < startDate);
}
if (remainder == 0 && virtualLong != startDate) { // 当整除的时候,说明当月的第一天也是虚拟事件,判断排除为父本,然后添加。不处理,一个月第一天事件会丢失
before_times = before_times - 1;
}
if (bk_billRepeatType == 1) { // 单独处理天事件,计算出第一次出现在时间段的事件时间
virtualLong = bk_billDuedate + (before_times + 1) * 7
* (DAYMILLIS);
calendar.setTimeInMillis(virtualLong);
} else if (bk_billRepeatType == 2) {
virtualLong = bk_billDuedate + (before_times + 1) * (2 * 7)
* DAYMILLIS;
calendar.setTimeInMillis(virtualLong);
} else if (bk_billRepeatType == 3) {
virtualLong = bk_billDuedate + (before_times + 1) * (4 * 7)
* DAYMILLIS;
calendar.setTimeInMillis(virtualLong);
} else if (bk_billRepeatType == 4) {
virtualLong = bk_billDuedate + (before_times + 1) * (15)
* DAYMILLIS;
calendar.setTimeInMillis(virtualLong);
}
while (startDate <= virtualLong && virtualLong <= endDate) { // 插入虚拟事件
Map<String, Object> bMap = new HashMap<String, Object>();
bMap.putAll(iMap);
bMap.put("ep_billDueDate", virtualLong);
bMap.put("indexflag", 2); // 2表示虚拟事件
virtualDataList.add(bMap);
if (bk_billRepeatType == 1) {
calendar.add(Calendar.DAY_OF_MONTH, 7);
} else if (bk_billRepeatType == 2) {
calendar.add(Calendar.DAY_OF_MONTH, 2 * 7);
} else if (bk_billRepeatType == 3) {
calendar.add(Calendar.DAY_OF_MONTH, 4 * 7);
} else if (bk_billRepeatType == 4) {
calendar.add(Calendar.DAY_OF_MONTH, 15);
} else if (bk_billRepeatType == 5) {
Calendar calendarCloneCalendar = (Calendar) calendar
.clone();
int currentMonthDay = calendarCloneCalendar
.get(Calendar.DAY_OF_MONTH);
calendarCloneCalendar.add(Calendar.MONTH,
1);
int nextMonthDay = calendarCloneCalendar
.get(Calendar.DAY_OF_MONTH);
if (currentMonthDay > nextMonthDay) {
calendar.add(Calendar.MONTH, 1
+ 1);
} else {
calendar.add(Calendar.MONTH, 1);
}
}else if (bk_billRepeatType == 6) {
Calendar calendarCloneCalendar = (Calendar) calendar
.clone();
int currentMonthDay = calendarCloneCalendar
.get(Calendar.DAY_OF_MONTH);
calendarCloneCalendar.add(Calendar.MONTH,
2);
int nextMonthDay = calendarCloneCalendar
.get(Calendar.DAY_OF_MONTH);
if (currentMonthDay > nextMonthDay) {
calendar.add(Calendar.MONTH, 2
+ 2);
} else {
calendar.add(Calendar.MONTH, 2);
}
}else if (bk_billRepeatType == 7) {
Calendar calendarCloneCalendar = (Calendar) calendar
.clone();
int currentMonthDay = calendarCloneCalendar
.get(Calendar.DAY_OF_MONTH);
calendarCloneCalendar.add(Calendar.MONTH,
3);
int nextMonthDay = calendarCloneCalendar
.get(Calendar.DAY_OF_MONTH);
if (currentMonthDay > nextMonthDay) {
calendar.add(Calendar.MONTH, 3
+ 3);
} else {
calendar.add(Calendar.MONTH, 3);
}
} else if (bk_billRepeatType == 8) {
calendar.add(Calendar.YEAR, 1);
}
virtualLong = calendar.getTimeInMillis();
}
finalDataList.addAll(virtualDataList);
}// 遍历模板结束,产生结果为一个父本加若干虚事件的list
/*
* 开始处理重复特例事件特例事件,并且来时合并
*/
List<Map<String, Object>>oDataList = BillsDao.selectBillItemByBE(context, start, end);
Log.v("mtest", "特例结果大小" +oDataList );
List<Map<String, Object>> delectDataListf = new ArrayList<Map<String, Object>>(); // finalDataList要删除的结果
List<Map<String, Object>> delectDataListO = new ArrayList<Map<String, Object>>(); // oDataList要删除的结果
for (Map<String, Object> fMap : finalDataList) { // 遍历虚拟事件
int pbill_id = (Integer) fMap.get("_id");
long pdue_date = (Long) fMap.get("ep_billDueDate");
for (Map<String, Object> oMap : oDataList) {
int cbill_id = (Integer) oMap.get("billItemHasBillRule");
long cdue_date = (Long) oMap.get("ep_billDueDate");
int bk_billsDelete = (Integer) oMap.get("ep_billisDelete");
if (cbill_id == pbill_id) {
if (bk_billsDelete == 2) {// 改变了duedate的特殊事件
long old_due = (Long) oMap.get("ep_billItemDueDateNew");
if (old_due == pdue_date) {
delectDataListf.add(fMap);//该改变事件在时间范围内,保留oMap
}
} else if (bk_billsDelete == 1) {
if (cdue_date == pdue_date) {
delectDataListf.add(fMap);
delectDataListO.add(oMap);
}
} else {
if (cdue_date == pdue_date) {
delectDataListf.add(fMap);
}
}
}
}// 遍历特例事件结束
}// 遍历虚拟事件结束
// Log.v("mtest", "delectDataListf的大小"+delectDataListf.size());
// Log.v("mtest", "delectDataListO的大小"+delectDataListO.size());
finalDataList.removeAll(delectDataListf);
oDataList.removeAll(delectDataListO);
finalDataList.addAll(oDataList);
List<Map<String, Object>> mOrdinaryList = BillsDao.selectOrdinaryBillRuleByBE(context, start, end);
finalDataList.addAll(mOrdinaryList);
// Log.v("mtest", "finalDataList的大小"+finalDataList.size());
long b = System.currentTimeMillis();
Log.v("mtest", "算法耗时"+(b-a));
return finalDataList;
}
What if you have a recurring appointment with no end date? As cheap as space is, you don't have infinite space, so Solution 2 is a non-starter there...
May I suggest that "no end date" can be resolved to an end date at the end of the century. Even for a dayly event the amount of space remains cheap.