Coder Social home page Coder Social logo

rrule-go's Introduction

rrule-go

Go library for working with recurrence rules for calendar dates.

CI Codecov CodeQL License Go Reference

The rrule module offers a complete implementation of the recurrence rules documented in the iCalendar RFC. It is a partial port of the rrule module from the excellent python-dateutil library.

Demo

rrule.RRule

package main

import (
  "fmt"
  "time"

  "github.com/teambition/rrule-go"
)

func printTimeSlice(ts []time.Time) {
	for _, t := range ts {
		fmt.Println(t)
	}
}

func main() {
	// Daily, for 10 occurrences.
	r, _ := rrule.NewRRule(rrule.ROption{
		Freq:    rrule.DAILY,
		Count:   10,
		Dtstart: time.Date(1997, 9, 2, 9, 0, 0, 0, time.UTC),
	})

	fmt.Println(r.String())
	// DTSTART:19970902T090000Z
	// RRULE:FREQ=DAILY;COUNT=10

	printTimeSlice(r.All())
	// 1997-09-02 09:00:00 +0000 UTC
	// 1997-09-03 09:00:00 +0000 UTC
	// ...
	// 1997-09-07 09:00:00 +0000 UTC

	printTimeSlice(r.Between(
		time.Date(1997, 9, 6, 0, 0, 0, 0, time.UTC),
		time.Date(1997, 9, 8, 0, 0, 0, 0, time.UTC), true))
	// [1997-09-06 09:00:00 +0000 UTC
	//  1997-09-07 09:00:00 +0000 UTC]

	// Every four years, the first Tuesday after a Monday in November, 3 occurrences (U.S. Presidential Election day).
	r, _ = rrule.NewRRule(rrule.ROption{
		Freq:       rrule.YEARLY,
		Interval:   4,
		Count:      3,
		Bymonth:    []int{11},
		Byweekday:  []rrule.Weekday{rrule.TU},
		Bymonthday: []int{2, 3, 4, 5, 6, 7, 8},
		Dtstart:    time.Date(1996, 11, 5, 9, 0, 0, 0, time.UTC),
	})

	fmt.Println(r.String())
	// DTSTART:19961105T090000Z
	// RRULE:FREQ=YEARLY;INTERVAL=4;COUNT=3;BYMONTH=11;BYMONTHDAY=2,3,4,5,6,7,8;BYDAY=TU

	printTimeSlice(r.All())
	// 1996-11-05 09:00:00 +0000 UTC
	// 2000-11-07 09:00:00 +0000 UTC
	// 2004-11-02 09:00:00 +0000 UTC
}

rrule.Set

func ExampleSet() {
	// Daily, for 7 days, jumping Saturday and Sunday occurrences.
	set := rrule.Set{}
	r, _ := rrule.NewRRule(rrule.ROption{
		Freq:    rrule.DAILY,
		Count:   7,
		Dtstart: time.Date(1997, 9, 2, 9, 0, 0, 0, time.UTC)})
	set.RRule(r)

	fmt.Println(set.String())
	// DTSTART:19970902T090000Z
	// RRULE:FREQ=DAILY;COUNT=7

	printTimeSlice(set.All())
	// 1997-09-02 09:00:00 +0000 UTC
	// 1997-09-03 09:00:00 +0000 UTC
	// 1997-09-04 09:00:00 +0000 UTC
	// 1997-09-05 09:00:00 +0000 UTC
	// 1997-09-06 09:00:00 +0000 UTC
	// 1997-09-07 09:00:00 +0000 UTC
	// 1997-09-08 09:00:00 +0000 UTC

	// Weekly, for 4 weeks, plus one time on day 7, and not on day 16.
	set = rrule.Set{}
	r, _ = rrule.NewRRule(rrule.ROption{
		Freq:    rrule.WEEKLY,
		Count:   4,
		Dtstart: time.Date(1997, 9, 2, 9, 0, 0, 0, time.UTC)})
	set.RRule(r)
	set.RDate(time.Date(1997, 9, 7, 9, 0, 0, 0, time.UTC))
	set.ExDate(time.Date(1997, 9, 16, 9, 0, 0, 0, time.UTC))

	fmt.Println(set.String())
	// DTSTART:19970902T090000Z
	// RRULE:FREQ=WEEKLY;COUNT=4
	// RDATE:19970907T090000Z
	// EXDATE:19970916T090000Z

	printTimeSlice(set.All())
	// 1997-09-02 09:00:00 +0000 UTC
	// 1997-09-07 09:00:00 +0000 UTC
	// 1997-09-09 09:00:00 +0000 UTC
	// 1997-09-23 09:00:00 +0000 UTC
}

rrule.StrToRRule

func ExampleStrToRRule() {
	// Compatible with old DTSTART
	r, _ := rrule.StrToRRule("FREQ=DAILY;DTSTART=20060101T150405Z;COUNT=5")
	fmt.Println(r.OrigOptions.RRuleString())
	// FREQ=DAILY;COUNT=5

	fmt.Println(r.OrigOptions.String())
	// DTSTART:20060101T150405Z
	// RRULE:FREQ=DAILY;COUNT=5

	fmt.Println(r.String())
	// DTSTART:20060101T150405Z
	// RRULE:FREQ=DAILY;COUNT=5

	printTimeSlice(r.All())
	// 2006-01-01 15:04:05 +0000 UTC
	// 2006-01-02 15:04:05 +0000 UTC
	// 2006-01-03 15:04:05 +0000 UTC
	// 2006-01-04 15:04:05 +0000 UTC
	// 2006-01-05 15:04:05 +0000 UTC
}

rrule.StrToRRuleSet

func ExampleStrToRRuleSet() {
	s, _ := rrule.StrToRRuleSet("DTSTART:20060101T150405Z\nRRULE:FREQ=DAILY;COUNT=5\nEXDATE:20060102T150405Z")
	fmt.Println(s.String())
	// DTSTART:20060101T150405Z
	// RRULE:FREQ=DAILY;COUNT=5
	// EXDATE:20060102T150405Z

	printTimeSlice(s.All())
	// 2006-01-01 15:04:05 +0000 UTC
	// 2006-01-03 15:04:05 +0000 UTC
	// 2006-01-04 15:04:05 +0000 UTC
	// 2006-01-05 15:04:05 +0000 UTC
}

For more examples see python-dateutil documentation.

License

Gear is licensed under the MIT license. Copyright © 2017-2023 Teambition.

rrule-go's People

Contributors

damoye avatar dependabot[bot] avatar fujiechen avatar jameshartig avatar katrinahoffert avatar kristinn avatar nateinaction avatar rickywiens avatar sjansen avatar wangjohn avatar yordis avatar zensh avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

rrule-go's Issues

Nanoseconds in DTSTART breaks between method

If a time object with nanoseconds is supplied to RRule.DTStart(), if we then call RRule.Between() with inc = true, the first instance is not returned. However the same query when DTStart does not contain nanoseconds includes the first instance.

Sample program:

package main

import (
	"fmt"
	"time"

	"github.com/teambition/rrule-go"
)

func main() {

	startDate := time.Date(2023, 01, 10, 16, 07, 0, 0, time.UTC)
	fmt.Println("Without nanoseconds:", startDate)
	run(startDate)

	startDate = time.Date(2023, 01, 10, 16, 07, 0, 1234, time.UTC)
	fmt.Println("With nanoseconds:", startDate)
	run(startDate)

}

func run(startDate time.Time) {
	rule := "FREQ=WEEKLY;INTERVAL=1;COUNT=10"
	r, _ := rrule.StrToRRule(rule)
	endDate := startDate.AddDate(0, 0, 30)
	r.DTStart(startDate)
	between := r.Between(startDate, endDate, true)
	for _, i := range between {
		fmt.Println(i)
	}
}

Output:

Without nanoseconds: 2023-01-10 16:07:00 +0000 UTC
2023-01-10 16:07:00 +0000 UTC
2023-01-17 16:07:00 +0000 UTC
2023-01-24 16:07:00 +0000 UTC
2023-01-31 16:07:00 +0000 UTC
2023-02-07 16:07:00 +0000 UTC
With nanoseconds: 2023-01-10 16:07:00.000001234 +0000 UTC
2023-01-17 16:07:00 +0000 UTC
2023-01-24 16:07:00 +0000 UTC
2023-01-31 16:07:00 +0000 UTC
2023-02-07 16:07:00 +0000 UTC

The first entry (2023-01-10 16:07:00 +0000 UTC) is missing when nanoseconds is supplied.

Question: Timezone conversion

So, this is just a question for this awesome go project :)

I'm having two systems talking to each other.
One has a string saved such as (I really love this short format):
FREQ=MONTHLY;DTSTART=20230222T092100;BYDAY=2WE

My other system pulls this string and converts it to specific dates.
I will only use CET/CEST such as Europe/Stockholm.

When my receiving systems is interpreting this using rrule.StrToRRule, its always shows UTC. Fine, its probably some RFC thing.
But Is there a clever way to have StrToRRule output the time in CET/CEST in this oneliner?

I couldn't find any examples on how to handle this i the docs (or in python dateutil).
Right now I'm solving this by using time.LoadLocation in Go. Perhaps there is a better way?

Thanks!

Get last event time

Is it possible to extract the Until property of a set computed from a string?

Behavior change in rrule.Set.Between from v1.5.0 to v1.6.0

Go Version: 1.15.2
Test Case:

s := []string{"DTSTART;20100123T100000Z", "RRULE:FREQ=YEARLY"}
set, err := rrule.StrSliceToRRuleSet(s)
if err != nil {
	log.Fatal(err)
}
after := time.Date(2010, time.January, 23, 10, 0, 0, 0, time.UTC)
before := time.Date(2012, time.January, 23, 10, 0, 0, 0, time.UTC)
fmt.Printf("%+v\n", set.Between(after, before, true))

Output using [email protected]: [2010-01-23 10:00:00 +0000 UTC 2011-01-23 10:00:00 +0000 UTC 2012-01-23 10:00:00 +0000 UTC]
Output using [email protected]: [2010-01-23 10:00:00 +0000 UTC 2011-01-13 10:00:00 +0000 UTC 2011-01-23 10:00:00 +0000 UTC 2012-01-13 10:00:00 +0000 UTC 2012-01-23 10:00:00 +0000 UTC]

With the latest release, we are now observing some events within 10 days of each other when using FREQ=YEARLY.

What happened to 29 October 2017?

package main

import (
	"fmt"
	"time"

	rrule "github.com/teambition/rrule-go"
)

func main() {
	r, _ := rrule.NewRRule(rrule.ROption{
		Freq:     rrule.HOURLY,
		Interval: 8,
		Dtstart:  time.Date(2017, 10, 1, 5, 0, 0, 0, time.Local),
	})
	for _, t := range r.Between(time.Date(2017, 10, 27, 0, 0, 0, 0, time.Local), time.Date(2017, 11, 1, 0, 0, 0, 0, time.Local), true) {
		fmt.Println(t.Local())
	}
}

Output, with no results for 29 October 2017:

2017-10-27 05:00:00 +0200 CEST
2017-10-27 13:00:00 +0200 CEST
2017-10-27 21:00:00 +0200 CEST
2017-10-28 05:00:00 +0200 CEST
2017-10-28 13:00:00 +0200 CEST
2017-10-28 21:00:00 +0200 CEST
2017-10-30 05:00:00 +0100 CET
2017-10-30 13:00:00 +0100 CET
2017-10-30 21:00:00 +0100 CET
2017-10-31 05:00:00 +0100 CET
2017-10-31 13:00:00 +0100 CET
2017-10-31 21:00:00 +0100 CET

Timezones and daylight savings

Hello,

Let's take the France for the example.
I create a recurrent event that start the 19 March to the 2 April, every Monday and Wednesday, at 14h.
But, in France, the 25 March, a daylight saving is observed.

And my rrule.All() is wrong.

How can I handle this case please ?
Thanks a lot for this library !

Events which last longer than 1 day

Does this library provide any functionality for long events which span multiple days? For example, if my event starts on Monday, lasts 2 days and repeats every 3 days, then calling .Between({tuesday}, {thursday}, true) should return the event which starts on Monday and the event which starts on Thursday.

I'm not hugely familiar with the iCal RFC, so not even sure how this is handled, but this seems like a super useful thing to be able to do. Does this library support it? If not, perhaps I could help adding it?

set.After() works incorrect if timestamp has milliseconds

Hello, thank you for your lib, I found it very useful.
I use it on one of my projects and found a small incorrect behaviour (as I can see it).

if I have a rule something like that

    rule := rrule.ROption{
        Freq:     rrule.Frequency(recurrence.Frequency_MINUTELY),
        Count:    1,
        Bysecond: []int{10, 20, 30, 40, 59},
    }
   // RRULE:FREQ=MINUTELY;COUNT=1;BYSECOND=10,20,30,40,59

and from-time has milliseconds

from := time.Now() // 2020-07-08 17:43:20.42899 +0300 MSK m=+0.001564527

and the seconds are equal to any of the Bysecond array (20 as in the example)
so set.After() will return nil value

t := set.After(from, true)
// t == nil

I expect an event at 17:43:30.

Works correctly if I truncate milliseconds from the timestamp.

2.29 Not present

    var dd = "2023-01-30 12:45:23"

loc, _ := time.LoadLocation("Local")

dt, _ := time.ParseInLocation("2006-01-02 15:04:05", dd, loc)

r, _ = rrule.NewRRule(rrule.ROption{
	Freq: rrule.MONTHLY,
	//Bymonthday: []int{},
	//	Bymonth:    []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}, // 取哪一月的数据
	Interval: 1,
	Until:    time.Now().AddDate(0, 5, 0),
	Dtstart:  dt,
})

//set.ExDate(time.Now())
for _, t2 := range r.All() {
	fmt.Println(t2.Format("2006-01-02 15:04:05"))
}

result:
2023-01-30 12:45:23
2023-03-30 12:45:23
2023-04-30 12:45:23
2023-05-30 12:45:23
2023-06-30 12:45:23

2023-02-28 12:45:23 How to achieve?

thank you

StrToRRule Does Not Apply Timezone to UNTIL and treats it as UTC

Basically when it parses a RRULE expression like below where it’s supposed to generate one recurrence on 3/3/2020, it treats the UNTIL as UTC time instead of the specified timezone in the RRULE.

Expression in question:

DTSTART;TZID=America/Los_Angeles:20200225T060000
RRULE:FREQ=WEEKLY;INTERVAL=1;WKST=MO;BYDAY=TU;UNTIL=20200303T063000

Instead of reading UNTIL as LA time 3/3/2020 6:30AM, it reads that as LA time 3/2/2020 10:30PM.

Code-wise, what happens is as follows:

  1. You call StrToRRuleSet.
  2. It correctly trims and parses TZID and overloads locale.
  3. However, it then calls below which completely ignores any locale and forces UTC.
// StrToROption converts string to ROption
func StrToROption(rfcString string) (*ROption, error) {
	return StrToROptionInLocation(rfcString, time.UTC)
}

Overlapping ExRule and RRule causes really bad performance

I've run into an issue where overlapping RRules and ExRules cause really poor performance. I'm not sure there's much to be done here because it would take some introspection to understand that the rrules and exrules overlap with each other, but I figured I should file a report here since it is pretty bad behavior.

Here's an example benchmark that causes what I'm describing:

func rruleSet(rules []string) *rrule.Set {
  res, err := rrule.StrToRRuleSet(strings.Join(rules, "\n"))
  if err != nil {
    panic(err)
  }
  return res
}

func BenchmarkRRuleOccurrences(t *testing.B) {
  fixtures := []struct {
    Set      *rrule.Set
  }{
    {
      Set: rruleSet([]string{
        "RRULE:FREQ=DAILY;DTSTART=20190211T000000Z",
        "EXRULE:FREQ=DAILY;DTSTART=20190211T000000Z",
      }),
    },
  }

  for _, fixture := range fixtures {
    next := fixture.Set.Iterator()
    next()
  }
}

This results in very slow computation of the next item, as seen below:

goos: darwin
goarch: amd64
BenchmarkRRuleOccurrences-4            1        17931085411 ns/op
PASS
ok                                                                        17.986s

If you remove the ExRule or change it slightly (like by adding INTERVAL=2), the benchmark will run in a couple of milliseconds.

My main concern is that I can't do set.Before(X) and return a result quickly, since the underlying implementation of Before uses set.Iterator(). Even after the cursor inside of the iterator has passed the X date, we have to still wait for the iterator to find a new result.

Again, I don't think it's easy to fix this issue without a good bit of work, but I did want to call it out.

rrule.DTStart() not working as expected

I ran into an issue I was trying to set Rrule Dtstart, after generating Rrule from a string, but only date is set and not time.
The program I was running :

func main() {
	rule := "FREQ=DAILY;DTSTART=19970902T010000Z;COUNT=10"
	r, _ := rrule.StrToRRule(rule)
	for _, i := range r.All() {
		fmt.Println(i)
	}
	fmt.Println()
	testrule := "FREQ=DAILY;COUNT=10"
	testr, _ := rrule.StrToRRule(testrule)
	testr.DTStart(time.Date(1997, 9, 2, 9, 1, 0, 0, time.UTC))
	for _, i := range testr.All() {
		fmt.Println(i)
	}
}

The output for the first and second rule is

1997-09-02 01:00:00 +0000 UTC
1997-09-03 01:00:00 +0000 UTC
1997-09-04 01:00:00 +0000 UTC
1997-09-05 01:00:00 +0000 UTC
1997-09-06 01:00:00 +0000 UTC
1997-09-07 01:00:00 +0000 UTC
1997-09-08 01:00:00 +0000 UTC
1997-09-09 01:00:00 +0000 UTC
1997-09-10 01:00:00 +0000 UTC
1997-09-11 01:00:00 +0000 UTC
1997-09-02 12:20:57 +0000 UTC
1997-09-03 12:20:57 +0000 UTC
1997-09-04 12:20:57 +0000 UTC
1997-09-05 12:20:57 +0000 UTC
1997-09-06 12:20:57 +0000 UTC
1997-09-07 12:20:57 +0000 UTC
1997-09-08 12:20:57 +0000 UTC
1997-09-09 12:20:57 +0000 UTC
1997-09-10 12:20:57 +0000 UTC
1997-09-11 12:20:57 +0000 UTC

I think the issue is when you generate rrule without DTStart , time.Now() is set as DTStart,
and when you are setting DTStart only Date is set and not time

DTSTART and RFC compatibility

Hello there,

I am working with this library for a project and I was wondering about the different ways this package handles RRULE strings compared to the python dateutil package. For example:

This string "FREQ=DAILY;INTERVAL=10;COUNT=5;DTSTART=19980902T090000"
While valid in rrule-go:

	r, err := rrule.StrToRRule("FREQ=DAILY;INTERVAL=10;COUNT=5;DTSTART=19980902T090000")
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(r)
	for _, e := range r.All() {
		fmt.Println(e)
	}
FREQ=DAILY;DTSTART=19980902T090000Z;INTERVAL=10;COUNT=5
1998-09-02 09:00:00 +0000 UTC
1998-09-12 09:00:00 +0000 UTC
1998-09-22 09:00:00 +0000 UTC
1998-10-02 09:00:00 +0000 UTC
1998-10-12 09:00:00 +0000 UTC

Is actually invalid according to the python dateutil library (And also RFC5545)

>>> rrulestr("FREQ=DAILY;INTERVAL=10;COUNT=5;DTSTART=19980902T090000")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/System/Library/Frameworks/Python.framework/Versions/2.7/Extras/lib/python/dateutil/rrule.py", line 1093, in __call__
    return self._parse_rfc(s, **kwargs)
  File "/System/Library/Frameworks/Python.framework/Versions/2.7/Extras/lib/python/dateutil/rrule.py", line 1013, in _parse_rfc
    tzinfos=tzinfos)
  File "/System/Library/Frameworks/Python.framework/Versions/2.7/Extras/lib/python/dateutil/rrule.py", line 975, in _parse_rfc_rrule
    raise ValueError, "unknown parameter '%s'" % name
ValueError: unknown parameter 'DTSTART'"

The RFC spec defines DTSTART as existing at the same level as the RRULE string like:

  DTSTART:19980902T090000
  RRULE:FREQ=DAILY;INTERVAL=10;COUNT=5

Which is not compatible with rrule-go:

	rs, err := rrule.StrToRRuleSet("DTSTART:19980902T090000\n" + "RRULE:FREQ=DAILY;INTERVAL=10;COUNT=5")
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(rs)
	for _, e := range rs.All() {
		fmt.Println(e)
	}
2018/08/21 13:54:44 unsupported property: DTSTART

Process finished with exit code 1

But is compatible with the python dateutil library:

>>> r = rrulestr("""
...   DTSTART:19980902T090000
...   RRULE:FREQ=DAILY;INTERVAL=10;COUNT=5
...   """)
>>> list(r)
[datetime.datetime(1998, 9, 2, 9, 0), datetime.datetime(1998, 9, 12, 9, 0), datetime.datetime(1998, 9, 22, 9, 0), datetime.datetime(1998, 10, 2, 9, 0), datetime.datetime(1998, 10, 12, 9, 0)]
>>>

I was wondering if the functionality of this package, which seems to be more similar to rrule.js is intentional? I am willing to contribute to this package to make it more similar to the python library and the spec, but I was wondering if that was desirable to the maintainers.

Let me know if I'm way off base here, and thanks for your time.

Changing DTStart not changing iterator

Hi, Thank you for this. It's been really helpful for us.

One issue I've noticed was that changing the DTStart for an rrule set does not change the actual values generated by the iterator. For example,

package main

import (
    "log"
    "github.com/teambition/rrule-go"
)

func main() {
    log.SetFlags(0)
    ogr := []string{"DTSTART;TZID=America/Los_Angeles:20181115T000000", "RRULE:FREQ=DAILY;INTERVAL=1;WKST=SU;UNTIL=20181118T235959"}
    set, err := rrule.StrSliceToRRuleSet(ogr)
    if err != nil {
	log.Fatalln(err)
    }

    log.Println("Original Recurrence: ", ogr)
    for _, t := range set.All() {
	log.Println(t)
    }

    set.DTStart(set.GetDTStart().AddDate(0, 0, 1))

    log.Println("New Recurrence: ", set.Recurrence())

    for _, t := range set.All() {
	log.Println(t)
    }

    ns, err := rrule.StrSliceToRRuleSet(set.Recurrence())
    if err != nil {
	log.Fatalln(err)
    }

    log.Println("Reparsed Recurrence: ", ns.Recurrence())

    for _, t := range ns.All() {
	log.Println(t)
    }
}

This prints out the following,

Original Recurrence:  [DTSTART;TZID=America/Los_Angeles:20181115T000000 RRULE:FREQ=DAILY;INTERVAL=1;WKST=SU;UNTIL=20181118T235959]
2018-11-15 00:00:00 -0800 PST
2018-11-16 00:00:00 -0800 PST
2018-11-17 00:00:00 -0800 PST
2018-11-18 00:00:00 -0800 PST
New Recurrence:  [DTSTART:TZID=America/Los_Angeles:20181116T000000 RRULE:FREQ=DAILY;INTERVAL=1;WKST=SU;UNTIL=20181118T235959Z]
2018-11-15 00:00:00 -0800 PST
2018-11-16 00:00:00 -0800 PST
2018-11-17 00:00:00 -0800 PST
2018-11-18 00:00:00 -0800 PST
Reparsed Recurrence:  [DTSTART:TZID=America/Los_Angeles:20181116T000000 RRULE:FREQ=DAILY;INTERVAL=1;WKST=SU;UNTIL=20181118T235959Z]
2018-11-16 00:00:00 -0800 PST
2018-11-17 00:00:00 -0800 PST
2018-11-18 00:00:00 -0800 PST

As you can see changing the DTStart changed the recurrence but did not have any impact on the iterator. If this is the expected behaviour, it should be documented. If not I am more than happen to try and fix this.

Rule Set: All() returns events in local time even if DTStart is provided as an UTC time object

Hello everybody.

I want to report a suspected bug. I'm able to provide a pull request with a fix if this is indeed a bug.

Bug description

The DTStart's timezone is not respected when requesting a list of upcoming events.

Code for reproducing the bad behavior

package main

import (
	"fmt"
	"github.com/teambition/rrule-go"
	"time"
)

func main() {
	ruleSet := &rrule.Set{}
	rule, err := rrule.StrToRRule("FREQ=DAILY;COUNT=10;WKST=MO;BYHOUR=10;BYMINUTE=0;BYSECOND=0")
	if err != nil {
		panic(err)
	}
	ruleSet.RRule(rule)
	ruleSet.DTStart(time.Date(2019, 3, 6, 0, 0, 0, 0, time.UTC))

	fmt.Printf("DTStart: %v\n", ruleSet.GetDTStart())
	fmt.Printf("Recurrence: %v\n", ruleSet.Recurrence())

	// ruleSet.All() returns events in my local timezone instead of UTC
	for _, event := range ruleSet.All() {
		fmt.Printf("Event: %v\n", event)
	}
}

Expected results

DTStart: 2019-03-06 00:00:00 +0000 UTC
Recurrence: [DTSTART:TZID=UTC:20190306T000000 RRULE:FREQ=DAILY;COUNT=10;BYHOUR=10;BYMINUTE=0;BYSECOND=0]
Event: 2019-03-06 10:00:00 +0000 UTC
Event: 2019-03-07 10:00:00 +0000 UTC
Event: 2019-03-08 10:00:00 +0000 UTC
Event: 2019-03-09 10:00:00 +0000 UTC
Event: 2019-03-10 10:00:00 +0000 UTC
Event: 2019-03-11 10:00:00 +0000 UTC
Event: 2019-03-12 10:00:00 +0000 UTC
Event: 2019-03-13 10:00:00 +0000 UTC
Event: 2019-03-14 10:00:00 +0000 UTC
Event: 2019-03-15 10:00:00 +0000 UTC

Actual results

DTStart: 2019-03-06 00:00:00 +0000 UTC
Recurrence: [DTSTART:TZID=UTC:20190306T000000 RRULE:FREQ=DAILY;COUNT=10;BYHOUR=10;BYMINUTE=0;BYSECOND=0]
Event: 2019-03-06 10:00:00 +0100 CET
Event: 2019-03-07 10:00:00 +0100 CET
Event: 2019-03-08 10:00:00 +0100 CET
Event: 2019-03-09 10:00:00 +0100 CET
Event: 2019-03-10 10:00:00 +0100 CET
Event: 2019-03-11 10:00:00 +0100 CET
Event: 2019-03-12 10:00:00 +0100 CET
Event: 2019-03-13 10:00:00 +0100 CET
Event: 2019-03-14 10:00:00 +0100 CET
Event: 2019-03-15 10:00:00 +0100 CET

rruleset.After(time.Now()) with count 1 returns zero time value

Hi,

I tested the following code today:

package main

import (
	"log"
	"time"

	"github.com/teambition/rrule-go"
)

func main() {
	rrule, err := rrule.StrToRRuleSet("RRULE:FREQ=SECONDLY;INTERVAL=10;COUNT=1")
	if err != nil {
		log.Fatal(err)
	}

	log.Print(rrule.After(time.Now(), true))
}

It returns a zero time value.
From my perspective I would expect it to return the current time, since if I input a schedule with a count of one I would expect it to have exactly one recurrence (if the dtstart is not in the past, which is not specified here). Of course if you specify inc=false I agree it should probably return the zero value.

Actually this is what I get from other rrule libraries like shown here http://jakubroztocil.github.io/rrule/.

My proposed fix would be the following:

diff --git a/util.go b/util.go
index e01d3e9..b1cc735 100644
--- a/util.go
+++ b/util.go
@@ -172,6 +172,9 @@ func after(next Next, dt time.Time, inc bool) time.Time {
                if !ok {
                        return time.Time{}
                }
+
+               v = v.Truncate(time.Second)
+               dt = dt.Truncate(time.Second)
                if inc && !v.Before(dt) || !inc && v.After(dt) {
                        return v
                }

I truncate to seconds since seconds are the highest precision that the rrule standard defines and it does not have any value to also include the milliseconds imo.

If you agree I'd be glad to open a PR. Of course this could be kind of a breaking change and it's probably an edge case.

rrrule#All() generates wrong recurrences around DST

Describe the bug
teambition/rrule-go version: v1.8.2

rrule#All() is generating duplicate events for the same date time during DST changes.

To Reproduce
Code to reproduce the behavior:

 rule, _ := rrule.NewRRule(rrule.ROption{
	Freq: rrule.HOURLY,
	Dtstart: time.Date(2023, time.March, 26, 1, 30, 0, 0, time.Local),
	Until: time.Date(2023, time.March, 26, 5, 0, 0, 0, time.Local),
})

recurrences := rule.All()
// Produces the following result.
[]time.Time{
	time.Date(2023, time.March, 26, 1, 30, 0, 0, time.Local),

       // Duplicate events here
	time.Date(2023, time.March, 26, 3, 30, 0, 0, time.Local),
	time.Date(2023, time.March, 26, 3, 30, 0, 0, time.Local),

	time.Date(2023, time.March, 26, 4, 30, 0, 0, time.Local),
}

Expected behavior
Don't duplicate events.

Screenshots
If applicable, add screenshots to help explain your problem.

Additional context
Add any other context about the problem here.

Invalid timestamps for `HOURLY` interval RRULEs when DST change occurs

Describe the bug
teambition/rrule-go version: v1.8.2

We ran across an issue where RRULEs that use HOURLY intervals return incorrect timestamps when the timestamp corresponds to a DST switch.

Consider the following scenario:

  • DST in Australia/Sydney starts at 2022-10-02 02:00 and the clocks go forward 1 hour. The timezone changes from AEST (+1000) to AEDT (+1100).
  • An hourly schedule starting at 2022-10-02 01:00 with a count of 3, should return the following values:
    • 2022-10-02 01:00:00 +1000 AEST / 2022-10-01 15:00:00 +0000 UTC
    • 2022-10-02 03:00:00 +1100 AEDT / 2022-10-01 16:00:00 +0000 UTC
    • 2022-10-02 04:00:00 +1100 AEDT / 2022-10-01 17:00:00 +0000 UTC
  • What we get is:
    • 2022-10-02 01:00:00 +1000 AEST / 2022-10-01 15:00:00 +0000 UTC
    • 2022-10-02 03:00:00 +1100 AEDT / 2022-10-01 16:00:00 +0000 UTC
    • 2022-10-02 03:00:00 +1100 AEDT / 2022-10-01 16:00:00 +0000 UTC

The same bug can be seen when DST ends on April 2nd 2023.

To Reproduce
We have raised a PR to show the bug.

Expected behavior
Should return correct hour values.

Is there anyway to see the `Before` relative to the `DTStart`

I'm trying to build something that uses RRule for calculating something going forward. However to do so accurately it needs to do a retroactive calculation from the past.

In the scope of the code I have, DTStart is the previous event it had to calculate for. But I would love to be able to accurately see the event before that?

Is there a way to do this?


I technically can take the DTStart.Next() and then take the time difference between the two and subtract that from DTStart. But for a rule like FREQ=MONTHLY;INTERVAL=1;BYMONTHDAY=15,-1 this is not reliable, because the number of days between recurrences can be different each time.

PR's desired?

I forked rrule-go and added Set.String() and the ability to marshal/unmarshal json.

Would those capabilities be something you'd be interested in?

StrToRule doesn't follow RFC5545

According to https://tools.ietf.org/html/rfc5545
A rrule string such as:
DTSTART:20120201T093000Z\nRRULE:FREQ=WEEKLY;INTERVAL=5;UNTIL=20130130T230000Z;BYDAY=MO,FR
and is the kind of string returned from the fairly popular library https://github.com/jakubroztocil/rrule with demo here https://jakubroztocil.github.io/rrule/

However, this string won't work for two reasons,

  1. The standardized "RRULE:" Prefix from the RFC doc
  2. The new line \n format to separate the rules.

It seems like the library doesn't fully support the standard, although I may be wrong.

After generating a rule from a string, rule.DTStart does not work as expected [has workaround]

If I have code like:

startTime := /* some time which is not now, including some day which is not today */
rruleString := "FREQ=WEEKLY"
myRule, _ := rrule.StrToRRule(rruleString)
myRule.DTStart(startTime)

Then the initial calculations of day of the week for the rrule to match are wrong, as the calculations done here and in the following lines (which assume using time.Now() is valid) https://github.com/teambition/rrule-go/blob/master/rrule.go#L145 are never re-done.

Workaround
The workaround is to use rrule.StrToROption / rrule.StrToROptionInLocation and then on the resultant options struct, set Dtstart to your start time.

`UNTIL` is always interpreted as UTC

teambition/rrule-go version: v1.8.2

According to RFC5545, a DATE-TIME value can specify either a local time or UTC:

      FORM #1: DATE WITH LOCAL TIME

      The date with local time form is simply a DATE-TIME value that
      does not contain the UTC designator nor does it reference a time
      zone.  For example, the following represents January 18, 1998, at
      11 PM:

       19980118T230000

      DATE-TIME values of this type are said to be "floating" and are
      not bound to any time zone in particular.  They are used to
      represent the same hour, minute, and second value regardless of
      which time zone is currently being observed.

and

FORM #2: DATE WITH UTC TIME

      The date with UTC time, or absolute time, is identified by a LATIN
      CAPITAL LETTER Z suffix character, the UTC designator, appended to
      the time value.  For example, the following represents January 19,
      1998, at 0700 UTC:

       19980119T070000Z

      The "TZID" property parameter MUST NOT be applied to DATE-TIME
      properties whose time values are specified in UTC.

An example is given for DTSTART:

FORM #3: DATE WITH LOCAL TIME AND TIME ZONE REFERENCE

      The date and local time with reference to time zone information is
      identified by the use the "TZID" property parameter to reference
      the appropriate time zone definition.  "TZID" is discussed in
      detail in [Section 3.2.19](https://datatracker.ietf.org/doc/html/rfc5545#section-3.2.19).  For example, the following represents
      2:00 A.M. in New York on January 19, 1998:

       TZID=America/New_York:19980119T020000

NOTE: The time specified in DTSTART is interpreted correctly by the library.

RFC5545 specifies UNTIL as:

      The UNTIL rule part defines a DATE or DATE-TIME value that bounds
      the recurrence rule in an inclusive manner.  If the value
      specified by UNTIL is synchronized with the specified recurrence,
      this DATE or DATE-TIME becomes the last instance of the
      recurrence.  The value of the UNTIL rule part MUST have the same
      value type as the "DTSTART" property.  Furthermore, if the
      "DTSTART" property is specified as a date with local time, then
      the UNTIL rule part MUST also be specified as a date with local
      time.  If the "DTSTART" property is specified as a date with UTC
      time or a date with local time and time zone reference, then the
      UNTIL rule part MUST be specified as a date with UTC time.  

Note this part:

Furthermore, if the "DTSTART" property is specified as a date with local time, then
the UNTIL rule part MUST also be specified as a date with local time.

Unfortunately, this is not handled correctly. All date-time values in this field are interpreted as UTC.

To Reproduce

Code to reproduce the behavior:

func TestLocalTime(t *testing.T) {
	localTimeRRULE := "DTSTART;TZID=Australia/Sydney:19980101T090000\nRRULE:FREQ=WEEKLY;UNTIL=20201230T220000"
	r, err := StrToRRule(localTimeRRULE)
	if err != nil {
		t.Errorf("Error parsing rrule: %v", err)
	}
	localTimeRRULE2 := r.String()
	if localTimeRRULE != localTimeRRULE2 {
		t.Errorf("Expected:\n%v\ngot\n%v\n", localTimeRRULE, localTimeRRULE2)
	}
}

The test above shows that the UNTIL value is interpreted as UTC:

    str_test.go:27: Expected:
        DTSTART;TZID=Australia/Sydney:19980101T090000
        RRULE:FREQ=WEEKLY;UNTIL=20201230T220000
        got
        DTSTART;TZID=Australia/Sydney:19980101T090000
        RRULE:FREQ=WEEKLY;UNTIL=20201230T220000Z

We can confirm this in another test:

func TestLocalTime2(t *testing.T) {
	sydney, _ := time.LoadLocation("Australia/Sydney")
	localTimeRRULE := "DTSTART;TZID=Australia/Sydney:19980101T090000\nRRULE:FREQ=WEEKLY;UNTIL=20201230T220000"
	r, err := StrToRRule(localTimeRRULE)
	if err != nil {
		t.Errorf("Error parsing rrule: %v", err)
	}
	until := r.GetUntil()
	if until.Location() != sydney {
		t.Errorf("Expected:\n%v\ngot\n%v\n", sydney.String(), until.Location().String())
	}
}

which outputs:

    str_test.go:40: Expected:
        Australia/Sydney
        got
        UTC

Expected behavior

  • If the value specified in UNTIL does not end in a Z, it should be interpreted as local to the timezone specified in DTSTART as per the spec.
    Bonus points:
  • If the date-time format given in UNTIL is not the same as that given in DTSTART, it should error.
  • If the date-time format given in DTSTART is in UTC but TZID is also given, it should error.

Validating rrule

I'm not finding any documentation on validating the generated rrule. Does this exist currently?

Wrong time for events far into the future

I am parsing this event:

BEGIN:VCALENDAR
PRODID:-//xyz Corp//NONSGML PDA Calendar Version 1.0//EN
VERSION:2.0
BEGIN:VEVENT
DTSTAMP:19960704T120000Z
UID:[email protected]
ORGANIZER:mailto:[email protected]
DTSTART:21500918T143000Z
RRULE:FREQ=WEEKLY;BYDAY=MO
DTEND:21500918T153000Z
STATUS:CONFIRMED
CATEGORIES:CONFERENCE
SUMMARY:A test event summary
DESCRIPTION:A test event description
END:VEVENT
END:VCALENDAR

And then calling rruleObj.After(refTime, false) where refTime is the start of the event, so 2150-09-18 14:30. The result I am getting is 2150-09-21 15:19:41. But I'd expect it to be 2150-09-21 14:30:00.

The actual implementation is a testcase for a bot of me: https://github.com/CubicrootXYZ/RemindMe/blob/%2370-add-recurring-events/internal/icalimporter/icalimporter_test.go#L37.

Maybe there is sth. going wrong with leap years? Or my event is malformed? I'd appreciate any help on this topic.

ExRule removed - but still shown in README

I was trying to figure out why I couldn't use rrule.Set.ExRule locally, as it's shown in the README, then found it's not listed in the code anywhere. I went back in the history of the file, and it did used to be there, but was removed.

Is there a specific reason why it was removed?
Should the examples in the README get a refresh?

rrule.Set.String() proposal

I don't have a public repo up to submit a formal PR, but in my use, I needed rrule.Set.String() badly and unless I'm mistaken it is pretty simple (I'm probably wrong):

// String converts a rule set back to a parseable string
func (s *Set) String() string {
	r := ""
	for _, o := range s.rrule {
		r = r + "\nRRULE:" + o.String()
	}
	for _, o := range s.rdate {
		r = r + "\nRDATE:" + o.String()
	}
	for _, o := range s.exrule {
		r = r + "\nEXRULE:" + o.String()
	}
	for _, o := range s.exdate {
		r = r + "\nEXDATE:" + o.String()
	}
	if len(r) > 0 {
		if r[0] == '\n' {
			r = r[1:]
		}
	}
	return r
}

We use this to efficiently transport rule sets in JSON messages.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.