Skip to content

Commit

Permalink
chore: rework and add tests for all rrule parts
Browse files Browse the repository at this point in the history
  • Loading branch information
vareversat committed Dec 29, 2024
1 parent b99b3cb commit 0a7b75c
Show file tree
Hide file tree
Showing 27 changed files with 902 additions and 266 deletions.
6 changes: 3 additions & 3 deletions properties/property.go
Original file line number Diff line number Diff line change
Expand Up @@ -585,10 +585,10 @@ func (rsP *requestStatusPropertyType) ToICalendarPropFormat(output io.Writer) {
func (rrP *recurrenceRulePropertyType) ToICalendarPropFormat(output io.Writer) {
var unfoldedOutput bytes.Buffer
var partsOutput bytes.Buffer
if rrP.Value.GetValue() != nil {
partsOutput.Write([]byte(fmt.Sprint(rrP.Value.GetValue())))
if rrP.Value.GetStringValue() != "" {
partsOutput.Write([]byte(fmt.Sprint(rrP.Value.GetStringValue())))
}
unfoldedOutput.Write([]byte(fmt.Sprintf("%s:%s", rrP.PropName, rrP.Value.GetValue())))
unfoldedOutput.Write([]byte(fmt.Sprintf("%s:%s", rrP.PropName, rrP.Value.GetStringValue())))
foldOutput(&unfoldedOutput)
unfoldedOutput.WriteTo(output)
}
Expand Down
108 changes: 69 additions & 39 deletions types/recurrence_rule/by_day_part.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,62 +6,92 @@ import (
"io"
)

const (
minWeekOrdinal = -53
maxWeekOrdinal = 53
)

type ByDayPart interface {
RecurrenceRulePart
}

type byDayPart struct {
PartName RecurrenceRulePartName
WeekDays []RRWeekDayNum
partName RecurrenceRulePartName
weekDays weekDayWithOrdinals
}

func NewByDayPart(days []RRWeekDayNum) ByDayPart {
return &byDayPart{
PartName: BYDAY,
WeekDays: days,
}
type weekDayWithOrdinal struct {
ordinal int32
weekday WeekDay
}

func (f byDayPart) ToICalendarPartFormat(output io.Writer) {
output.Write([]byte(fmt.Sprintf("%s=%s", f.GetPartName(), f.GetPartValue())))
}
type weekDayWithOrdinals []weekDayWithOrdinal

type WeekDay string

const (
Monday WeekDay = "MO"
Tuesday WeekDay = "TU"
Wednesday WeekDay = "WE"
Thursday WeekDay = "TH"
Friday WeekDay = "FR"
Saturday WeekDay = "SA"
Sunday WeekDay = "SU"
)

func (f byDayPart) GetPartName() RecurrenceRulePartName {
return f.PartName
// NewByDayPart give the info on which day of the month the recurrence occurs. See [RFC-5545] ref for more info
// Example: BYDAY=1TU => "11th tuesday occurrence in the year"
//
// [RFC-5545]: https://datatracker.ietf.org/doc/html/rfc5545#section-3.3.10
func NewByDayPart(days []weekDayWithOrdinal) ByDayPart {
return &byDayPart{
partName: ByDay,
weekDays: days,
}
}

func (f byDayPart) GetPartValue() string {
var secondsOutput bytes.Buffer
for i := 0; i < len(f.WeekDays); i++ {
secondsOutput.Write([]byte(f.WeekDays[i].GetValue()))
if len(f.WeekDays)-1 > i {
secondsOutput.Write([]byte(fmt.Sprint(",")))
}
// NewWeekDayWithOrdinal return an ordinal followed by a week name. See [RFC-5545] ref for more info
// Example: 11TU => "11th tuesday occurrence in the year"
//
// [RFC-5545]: https://datatracker.ietf.org/doc/html/rfc5545#section-3.3.10
func NewWeekDayWithOrdinal(ordinal int32, weekDay WeekDay) (*weekDayWithOrdinal, error) {
if ordinal < minWeekOrdinal || ordinal > maxWeekOrdinal || ordinal == 0 {
return nil, fmt.Errorf(
"%d is not a valid ordinal. It must satisfy this : ord ∈ [%d;0[U]0;%d]",
ordinal,
minWeekOrdinal,
maxWeekOrdinal,
)
}
return secondsOutput.String()
return &weekDayWithOrdinal{
ordinal: ordinal,
weekday: weekDay,
}, nil
}

type RRWeekday string
func (p *byDayPart) ToICalendarPartFormat(output io.Writer) {
output.Write([]byte(fmt.Sprintf("%s=%s", p.GetPartName(), p.GetPartValue())))
}

const (
SU RRWeekday = "SU"
MO RRWeekday = "MO"
TU RRWeekday = "MO"
WE RRWeekday = "WE"
TH RRWeekday = "TH"
FR RRWeekday = "FR"
SA RRWeekday = "SA"
)
func (p *byDayPart) GetPartName() RecurrenceRulePartName {
return p.partName
}

// RRWeekDayNum
// Ordinal in range [0,23]
// Sign + or -
type RRWeekDayNum struct {
Sign string
Ordinal int
Weekday RRWeekday
func (p *byDayPart) GetPartValue() string {
var output bytes.Buffer
for i := 0; i < len(p.weekDays); i++ {
output.Write([]byte(p.weekDays[i].ToString()))
if len(p.weekDays)-1 > i {
output.Write([]byte(","))
}
}
return output.String()
}

func (rrdN *RRWeekDayNum) GetValue() string {
return fmt.Sprintf("%s%d%s", rrdN.Sign, rrdN.Ordinal, rrdN.Weekday)
func (w *weekDayWithOrdinal) ToString() string {
if w != nil {
return fmt.Sprintf("%d%s", w.ordinal, w.weekday)
} else {
return ""
}
}
70 changes: 70 additions & 0 deletions types/recurrence_rule/by_day_part_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package recurrence_rule

import (
"reflect"
"testing"
)

func TestNewByDayPart(t *testing.T) {
week1, _ := NewWeekDayWithOrdinal(1, Tuesday)
week2, _ := NewWeekDayWithOrdinal(20, Tuesday)
type args struct {
days []weekDayWithOrdinal
}
tests := []struct {
name string
args args
want string
}{
{
"Create BYDAY Recurrence rule component #1",
args{days: []weekDayWithOrdinal{*week1}},
"1TU",
},
{
"Create BYDAY Recurrence rule component #2",
args{days: []weekDayWithOrdinal{*week2}},
"20TU",
},
{
"Create BYDAY Recurrence rule component #3",
args{days: []weekDayWithOrdinal{*week1, *week2}},
"1TU,20TU",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := NewByDayPart(tt.args.days); !reflect.DeepEqual(got.GetPartValue(), tt.want) {
t.Errorf("got.GetPartValue() = %s, want %s", got.GetPartValue(), tt.want)
}
})
}
}

func TestNewWeekDayWithOrdinal(t *testing.T) {
type args struct {
ordinal int32
weekDay WeekDay
}
tests := []struct {
name string
args args
want string
wantErr bool
}{
{"Create WeekDay (no error)", args{ordinal: 1, weekDay: Tuesday}, "1TU", false},
{"Create WeekDay (with error)", args{ordinal: 100, weekDay: Tuesday}, "", true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := NewWeekDayWithOrdinal(tt.args.ordinal, tt.args.weekDay)
if (err != nil) != tt.wantErr {
t.Errorf("NewWeekDayWithOrdinal() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got.ToString(), tt.want) {
t.Errorf("got.ToString() = %s, want %s", got.ToString(), tt.want)
}
})
}
}
52 changes: 34 additions & 18 deletions types/recurrence_rule/by_hour_part.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,38 +8,54 @@ import (
"github.com/vareversat/gics/types"
)

const (
minHour = 0
maxHour = 23
)

type ByHourPart interface {
RecurrenceRulePart
}

type byHourPart struct {
PartName RecurrenceRulePartName
Seconds []types.IntegerType
partName RecurrenceRulePartName
hours []types.IntegerType
}

// NewByHourPart seconds in range [0,23]
func NewByHourPart(seconds []int32) ByHourPart {
return &byHourPart{
PartName: BYHOUR,
Seconds: types.NewIntegerValues(seconds),
// NewByHourPart give the info on which day of the month the recurrence occurs. See [RFC-5545] ref for more info
// Example: BYHOUR=19 => "at 19:XX (7:XX pm)"
//
// [RFC-5545]: https://datatracker.ietf.org/doc/html/rfc5545#section-3.3.10
func NewByHourPart(hours []int32) (ByHourPart, error) {
for i := 0; i < len(hours); i++ {
if hours[i] < minHour || hours[i] > maxHour {
return &byHourPart{
partName: ByHour,
hours: types.NewIntegerValues([]int32{0}),
}, fmt.Errorf("%d (%d-th) is not a valid hour. It must satisfy this : hour ∈ [%d;%d]", hours[i], i, minHour, maxHour)
}
}
return &byHourPart{
partName: ByHour,
hours: types.NewIntegerValues(hours),
}, nil
}

func (f byHourPart) ToICalendarPartFormat(output io.Writer) {
output.Write([]byte(fmt.Sprintf("%s=%s", f.GetPartName(), f.GetPartValue())))
func (p *byHourPart) ToICalendarPartFormat(output io.Writer) {
output.Write([]byte(fmt.Sprintf("%s=%s", p.GetPartName(), p.GetPartValue())))
}

func (f byHourPart) GetPartName() RecurrenceRulePartName {
return f.PartName
func (p *byHourPart) GetPartName() RecurrenceRulePartName {
return p.partName
}

func (f byHourPart) GetPartValue() string {
var secondsOutput bytes.Buffer
for i := 0; i < len(f.Seconds); i++ {
secondsOutput.Write([]byte(f.Seconds[i].GetStringValue()))
if len(f.Seconds)-1 > i {
secondsOutput.Write([]byte(fmt.Sprint(",")))
func (p *byHourPart) GetPartValue() string {
var output bytes.Buffer
for i := 0; i < len(p.hours); i++ {
output.Write([]byte(p.hours[i].GetStringValue()))
if len(p.hours)-1 > i {
output.Write([]byte((",")))
}
}
return secondsOutput.String()
return output.String()
}
39 changes: 39 additions & 0 deletions types/recurrence_rule/by_hour_part_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package recurrence_rule

import (
"reflect"
"testing"
)

func TestNewByHourPart(t *testing.T) {
type args struct {
hours []int32
}
tests := []struct {
name string
args args
want string
wantErr bool
}{
{"Create BYHOUR Recurrence rule component #1", args{hours: []int32{10}}, "10", false},
{
"Create BYHOUR Recurrence rule component #2",
args{hours: []int32{10, 22}},
"10,22",
false,
},
{"Create BYHOUR Recurrence rule component #3", args{hours: []int32{10, 22, 33}}, "0", true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := NewByHourPart(tt.args.hours)
if (err != nil) != tt.wantErr {
t.Errorf("NewByHourPart() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got.GetPartValue(), tt.want) {
t.Errorf("got.ToString() = %s, want %s", got.GetPartValue(), tt.want)
}
})
}
}
48 changes: 32 additions & 16 deletions types/recurrence_rule/by_minute_part.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,37 +8,53 @@ import (
"github.com/vareversat/gics/types"
)

const (
minMinute = 0
maxMinute = 59
)

type ByMinutePart interface {
RecurrenceRulePart
}

type byMinutePart struct {
PartName RecurrenceRulePartName
Seconds []types.IntegerType
partName RecurrenceRulePartName
minutes []types.IntegerType
}

// NewByMinutePart seconds in range [0,59]
func NewByMinutePart(seconds []int32) ByMinutePart {
return &byMinutePart{
PartName: BYMINUTE,
Seconds: types.NewIntegerValues(seconds),
// NewByMinutePart give the info on which hour of the day the recurrence occurs. See [RFC-5545] ref for more info
// Example: BYMINUTE=19 => "at XX:19 (XX:19 am/pm)"
//
// [RFC-5545]: https://datatracker.ietf.org/doc/html/rfc5545#section-3.3.10
func NewByMinutePart(minutes []int32) (ByMinutePart, error) {
for i := 0; i < len(minutes); i++ {
if minutes[i] < minMinute || minutes[i] > maxMinute {
return &byMinutePart{
partName: ByMinute,
minutes: types.NewIntegerValues([]int32{0}),
}, fmt.Errorf("%d (%d-th) is not a valid minute. It must satisfy this : minute ∈ [%d;%d]", minutes[i], i, minMinute, maxMinute)
}
}
return &byMinutePart{
partName: ByMinute,
minutes: types.NewIntegerValues(minutes),
}, nil
}

func (f byMinutePart) ToICalendarPartFormat(output io.Writer) {
output.Write([]byte(fmt.Sprintf("%s=%s", f.GetPartName(), f.GetPartValue())))
func (p *byMinutePart) ToICalendarPartFormat(output io.Writer) {
output.Write([]byte(fmt.Sprintf("%s=%s", p.GetPartName(), p.GetPartValue())))
}

func (f byMinutePart) GetPartName() RecurrenceRulePartName {
return f.PartName
func (p *byMinutePart) GetPartName() RecurrenceRulePartName {
return p.partName
}

func (f byMinutePart) GetPartValue() string {
func (p *byMinutePart) GetPartValue() string {
var secondsOutput bytes.Buffer
for i := 0; i < len(f.Seconds); i++ {
secondsOutput.Write([]byte(f.Seconds[i].GetStringValue()))
if len(f.Seconds)-1 > i {
secondsOutput.Write([]byte(fmt.Sprint(",")))
for i := 0; i < len(p.minutes); i++ {
secondsOutput.Write([]byte(p.minutes[i].GetStringValue()))
if len(p.minutes)-1 > i {
secondsOutput.Write([]byte(","))
}
}
return secondsOutput.String()
Expand Down
Loading

0 comments on commit 0a7b75c

Please sign in to comment.