Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

refactor

  • Loading branch information...
commit a0ff4cd5949395e44ce9d6c83ebbaebb5d5fc620 1 parent 58d65da
Dan Barry authored May 12, 2008
72  generate_recurrences.sql
... ...
@@ -1,54 +1,48 @@
1 1
 CREATE OR REPLACE FUNCTION  generate_recurrences(
2  
-  pattern_type TEXT,
3 2
   duration INTERVAL,
4  
-  original_date DATE,
5 3
   range_start DATE,
6  
-  range_end DATE
  4
+  range_end DATE,
  5
+  repeat_month INT,
  6
+  repeat_week INT,
  7
+  repeat_day INT
7 8
 )
8 9
   RETURNS setof DATE
9 10
   LANGUAGE plpgsql IMMUTABLE
10 11
   AS $BODY$
11 12
 DECLARE
12  
-  next_date DATE;
  13
+  next_date DATE := range_start;
  14
+  current_month INT;
  15
+  current_week INT;
13 16
 BEGIN
14  
-  next_date := original_date + duration * CEIL(intervals_between(original_date, range_start, duration));
15  
-  IF pattern_type = 'positive_week_dow' OR pattern_type = 'negative_week_dow' THEN
16  
-    -- in case the first recurrence is before next_date but after range_start
17  
-    next_date := next_date - 2 * duration;
18  
-    LOOP
19  
-      -- prevent an infinite loop
20  
-      IF duration = '28 days'::interval AND extract(month from next_date + duration) = extract(month from next_date) THEN
21  
-        next_date := next_date + duration;
22  
-      END IF;
23  
-      next_date := next_date + duration;
24  
-
25  
-      -- ensure month is correct for yearly events
26  
-      IF duration = '364 days'::interval THEN
27  
-        WHILE extract(month from next_date) != extract(month from original_date) LOOP
28  
-          next_date := next_date + cast(extract(month from original_date) - extract(month from next_date) as int) % 12 * '28 days'::interval;
29  
-        END LOOP;
  17
+  IF repeat_month IS NOT NULL THEN
  18
+    next_date := next_date + (((12 + repeat_month - cast(extract(month from next_date) as int)) % 12) || ' months')::interval;
  19
+  END IF;
  20
+  IF repeat_week IS NULL AND repeat_day IS NOT NULL THEN
  21
+    IF duration = '7 days'::interval THEN
  22
+      next_date := next_date + (((7 + repeat_day - cast(extract(dow from next_date) as int)) % 7) || ' days')::interval;
  23
+    ELSE
  24
+      next_date := next_date + (repeat_day - extract(day from next_date) || ' days')::interval;
  25
+    END IF;
  26
+  END IF;
  27
+  LOOP
  28
+    IF repeat_week IS NOT NULL AND repeat_day IS NOT NULL THEN
  29
+      current_month := extract(month from next_date);
  30
+      next_date := next_date + (((7 + repeat_day - cast(extract(dow from next_date) as int)) % 7) || ' days')::interval;
  31
+      IF extract(month from next_date) != current_month THEN
  32
+        next_date := next_date - '7 days'::interval;
30 33
       END IF;
31  
-
32  
-      IF pattern_type = 'positive_week_dow' THEN
33  
-        next_date := next_date
34  
-          + (CEIL(extract(day from original_date) / 7)
35  
-            - CEIL(extract(day from next_date) / 7))
36  
-          * '7 days'::interval;
  34
+      IF repeat_week > 0 THEN
  35
+        current_week := CEIL(extract(day from next_date) / 7);
37 36
       ELSE
38  
-        next_date := next_date
39  
-          + (CEIL((1 + extract(day from next_date + '1 month'::interval - next_date) - extract(day from next_date)) / 7)
40  
-            - CEIL((1 + extract(day from original_date + '1 month'::interval - original_date) - extract(day from original_date)) / 7))
41  
-          * '7 days'::interval;
  37
+        current_week := -CEIL((1 + extract(day from next_date + '1 month'::interval - next_date) - extract(day from next_date)) / 7);
42 38
       END IF;
43  
-      EXIT WHEN next_date > range_end;
44  
-      CONTINUE WHEN next_date < range_start OR next_date < original_date; -- subtracting an extra duration could have put us before the range_start or original_date
  39
+      next_date := next_date + (repeat_week - current_week) * '7 days'::interval;
  40
+    END IF;
  41
+    EXIT WHEN next_date > range_end;
  42
+    IF next_date >= range_start THEN
45 43
       RETURN NEXT next_date;
46  
-    END LOOP;
47  
-  ELSE
48  
-    WHILE next_date <= range_end LOOP
49  
-      RETURN NEXT next_date;
50  
-      next_date := next_date + duration;
51  
-    END LOOP;
52  
-  END IF;
  44
+    END IF;
  45
+    next_date := next_date + duration;
  46
+  END LOOP;
53 47
 END;
54 48
 $BODY$;
4  interval_for.sql
@@ -13,10 +13,6 @@ BEGIN
13 13
     RETURN '1 month'::interval;
14 14
   ELSIF recurs = 'yearly' THEN
15 15
     RETURN '1 year'::interval;
16  
-  ELSIF recurs = 'monthly_by_week_dow' THEN
17  
-    RETURN '28 days'::interval;
18  
-  ELSIF recurs = 'yearly_by_week_dow' THEN
19  
-    RETURN '364 days'::interval;
20 16
   ELSE
21 17
     RAISE EXCEPTION 'Recurrence % not supported by generate_recurrences()', recurs;
22 18
   END IF;
123  recurring_events_for.sql
@@ -15,10 +15,8 @@ DECLARE
15 15
   start_time TIME;
16 16
   end_date TIMESTAMP;
17 17
   next_date DATE;
18  
-  recurs TEXT;
19  
-  pattern_type TEXT;
  18
+  recurrences_start TIMESTAMP;
20 19
   recurrences_end TIMESTAMP;
21  
-  offset INTERVAL;
22 20
   duration INTERVAL;
23 21
 BEGIN
24 22
   FOR row IN
@@ -58,120 +56,31 @@ BEGIN
58 56
         END IF;
59 57
       END IF;
60 58
 
61  
-      recurs := event.frequency;
62  
-      pattern_type := 'normal';
63  
-      offset := '00:00:00'::interval;
  59
+      duration := interval_for(event.frequency);
64 60
 
65  
-      IF recurs = 'weekly' AND row.day IS NOT NULL THEN
66  
-        offset := offset + (((row.day-cast(extract(dow from start_date) as int))%7)||' days')::interval;
67  
-
68  
-      ELSIF recurs = 'monthly' AND row.week IS NOT NULL AND row.day IS NOT NULL THEN
69  
-        offset := offset + (((row.day-cast(extract(dow from start_date) as int))%7)||' days')::interval;
70  
-
71  
-        -- adjusting the day of week could have put us in a different month
72  
-        IF extract(month from start_date) != extract(month from start_date + offset) THEN
73  
-          IF cast(extract(month from start_date) - extract(month from start_date + offset) as int) % 12 = 1 THEN
74  
-            offset := offset + '7 days'::interval;
75  
-          ELSE
76  
-            offset := offset - '7 days'::interval;
77  
-          END IF;
78  
-        END IF;
79  
-
80  
-        recurs := 'monthly_by_week_dow';
81  
-        offset := offset - '28 days'::interval;
82  
-        IF row.week > 0 THEN
83  
-          pattern_type := 'positive_week_dow';
84  
-          WHILE offset < 0 LOOP
85  
-            offset := offset + '28 days'::interval;
86  
-            offset := offset + (row.week - CEIL(extract(day from start_date + offset)/7)) * '7 days'::interval;
87  
-          END LOOP;
88  
-        ELSE
89  
-          pattern_type := 'negative_week_dow';
90  
-          WHILE offset < 0 LOOP
91  
-            offset := offset + '28 days'::interval;
92  
-            offset := offset
93  
-              + (CEIL((1 + extract(day from start_date + offset + '1 month'::interval - (start_date + offset)) - extract(day from start_date + offset)) / 7)
94  
-                + row.week)
95  
-              * '7 days'::interval;
96  
-          END LOOP;
97  
-        END IF;
98  
-
99  
-      ELSIF recurs = 'monthly' AND row.day IS NOT NULL THEN
100  
-        offset := offset + ((row.day-cast(extract(day from start_date) as int))||' days')::interval;
101  
-
102  
-      ELSIF recurs = 'yearly' AND row.week IS NOT NULL AND row.day IS NOT NULL THEN
103  
-        IF row.month IS NULL THEN
104  
-          row.month = extract(month from start_date);
105  
-        END IF;
106  
-        offset := offset + ((row.month-cast(extract(month from start_date) as int))||' months')::interval;
107  
-        offset := offset + (((row.day-cast(extract(dow from start_date+offset) as int))%7)||' days')::interval;
108  
-
109  
-        -- adjusting the day of week could have put us in a different month
110  
-        IF extract(month from start_date) != extract(month from start_date + offset) THEN
111  
-          IF cast(extract(month from start_date) - extract(month from start_date + offset) as int) % 12 = 1 THEN
112  
-            offset := offset + '7 days'::interval;
113  
-          ELSE
114  
-            offset := offset - '7 days'::interval;
115  
-          END IF;
116  
-        END IF;
117  
-
118  
-        recurs := 'yearly_by_week_dow';
119  
-        offset := offset - '364 days'::interval;
120  
-        IF row.week > 0 THEN
121  
-          pattern_type := 'positive_week_dow';
122  
-          WHILE start_date+offset < start_date LOOP
123  
-            offset := offset + '364 days'::interval;
124  
-            IF extract(month from start_date+offset) != row.month THEN
125  
-              offset := offset + '7 days'::interval;
126  
-            END IF;
127  
-            offset := offset + (row.week - CEIL(extract(day from start_date + offset)/7)) * '7 days'::interval;
128  
-          END LOOP;
129  
-        ELSE
130  
-          pattern_type := 'negative_week_dow';
131  
-          WHILE start_date+offset < start_date LOOP
132  
-            offset := offset + '364 days'::interval;
133  
-            IF extract(month from start_date+offset) != row.month THEN
134  
-              offset := offset + '7 days'::interval;
135  
-            END IF;
136  
-            offset := offset
137  
-              + (CEIL((1 + extract(day from start_date + offset + '1 month'::interval - (start_date + offset)) - extract(day from start_date + offset)) / 7)
138  
-                + row.week)
139  
-              * '7 days'::interval;
140  
-          END LOOP;
141  
-        END IF;
142  
-
143  
-      ELSIF recurs = 'yearly' AND row.month IS NOT NULL OR row.day IS NOT NULL THEN
144  
-        IF row.month IS NOT NULL THEN
145  
-          offset := offset + ((row.month-cast(extract(month from start_date) as int))||' months')::interval;
146  
-        END IF;
147  
-        IF row.day IS NOT NULL THEN
148  
-          offset := offset + ((row.day-cast(extract(day from start_date) as int))||' days')::interval;
149  
-        END IF;
  61
+      IF event.until IS NOT NULL AND event.until < range_end THEN
  62
+        recurrences_end := event.until;
  63
+      ELSIF event.count IS NOT NULL AND start_date + (event.count - 1) * duration < range_end THEN
  64
+        recurrences_end := start_date + (event.count - 1) * duration;
  65
+      ELSE
  66
+        recurrences_end := range_end;
150 67
       END IF;
151 68
 
152  
-      duration := interval_for(recurs);
153  
-      WHILE start_date + offset < start_date LOOP
154  
-        offset := offset + duration;
155  
-      END LOOP;
156  
-      start_date := start_date + offset;
157  
-      end_date := end_date + offset;
158  
-
159  
-      IF event.until IS NOT NULL AND event.until < range_end THEN
160  
-        recurrences_end = event.until;
161  
-      ELSIF event.count IS NOT NULL AND start_date+(event.count-1)*duration < range_end THEN
162  
-        recurrences_end = start_date+(event.count-1)*duration;
  69
+      IF start_date > range_start THEN
  70
+        recurrences_start := start_date;
163 71
       ELSE
164  
-        recurrences_end = range_end;
  72
+        recurrences_start := start_date + FLOOR(intervals_between(start_date::date, range_start::date, duration)) * duration;
165 73
       END IF;
166 74
 
167 75
       FOR next_date IN
168 76
         SELECT *
169 77
           FROM generate_recurrences(
170  
-            pattern_type,
171 78
             duration,
172  
-            start_date::date,
173  
-            (range_start - (end_date - start_date))::date,
174  
-            recurrences_end::date
  79
+            recurrences_start::date,
  80
+            recurrences_end::date,
  81
+            row.month,
  82
+            row.week,
  83
+            row.day
175 84
           )
176 85
       LOOP
177 86
         CONTINUE WHEN next_date = original_date;
105  spec/generate_recurrences_spec.rb
@@ -2,7 +2,7 @@
2 2
 
3 3
 describe 'generate_recurrences' do
4 4
   it "should return dates inside of the range" do
5  
-    executing("select * from generate_recurrences('normal', '1 day', '2008-04-20', '2008-04-25', '2008-04-29');").should == [
  5
+    executing("select * from generate_recurrences('1 day', '2008-04-25', '2008-04-29', NULL, NULL, NULL);").should == [
6 6
       ['2008-04-25'],
7 7
       ['2008-04-26'],
8 8
       ['2008-04-27'],
@@ -12,7 +12,7 @@
12 12
   end
13 13
 
14 14
   it "should only include dates on the frequency specified" do
15  
-    executing("select * from generate_recurrences('normal', '7 days', '2008-04-01', '2008-04-01', '2008-04-29');").should == [
  15
+    executing("select * from generate_recurrences('7 days', '2008-04-01', '2008-04-29', NULL, NULL, NULL);").should == [
16 16
       ['2008-04-01'],
17 17
       ['2008-04-08'],
18 18
       ['2008-04-15'],
@@ -21,28 +21,48 @@
21 21
     ]
22 22
   end
23 23
 
24  
-  it "should not include dates before the original_date" do
25  
-    executing("select * from generate_recurrences('normal', '1 day', '2008-04-25', '2008-04-20', '2008-04-29');").should == [
26  
-      ['2008-04-25'],
27  
-      ['2008-04-26'],
28  
-      ['2008-04-27'],
29  
-      ['2008-04-28'],
30  
-      ['2008-04-29']
31  
-    ]
  24
+  describe 'by day of week' do
  25
+    it "should return dates on the requested day of week" do
  26
+      executing("select * from generate_recurrences('7 days', '2008-05-12', '2008-05-23', NULL, NULL, 4);").should == [
  27
+        ['2008-05-15'],
  28
+        ['2008-05-22']
  29
+      ]
  30
+    end
32 31
   end
33 32
 
34  
-  it "should return dates whole intervals away from original_date even if start_date isn't" do
35  
-    executing("select * from generate_recurrences('normal', '7 days', '2008-04-01', '2008-04-04', '2008-04-29');").should == [
36  
-      ['2008-04-08'],
37  
-      ['2008-04-15'],
38  
-      ['2008-04-22'],
39  
-      ['2008-04-29']
40  
-    ]
  33
+  describe 'by month' do
  34
+    it "should return the day in the requested month" do
  35
+      executing("select * from generate_recurrences('1 year', '2008-04-15', '2010-06-15', 5, NULL, NULL);").should == [
  36
+        ['2008-05-15'],
  37
+        ['2009-05-15'],
  38
+        ['2010-05-15']
  39
+      ]
  40
+    end
  41
+  end
  42
+
  43
+  describe 'by day of month' do
  44
+    it "should return the dates on the requested day of month" do
  45
+      executing("select * from generate_recurrences('1 month', '2008-04-01', '2008-06-30', NULL, NULL, 15);").should == [
  46
+        ['2008-04-15'],
  47
+        ['2008-05-15'],
  48
+        ['2008-06-15']
  49
+      ]
  50
+    end
  51
+
  52
+    describe 'by month' do
  53
+      it "should return the correct day of month in the correct month" do
  54
+        executing("select * from generate_recurrences('1 year', '2008-04-01', '2010-06-30', 5, NULL, 15);").should == [
  55
+          ['2008-05-15'],
  56
+          ['2009-05-15'],
  57
+          ['2010-05-15']
  58
+        ]
  59
+      end
  60
+    end
41 61
   end
42 62
 
43 63
   describe 'by week and day of week' do
44 64
     it "should include the dates on the particular positive offset week's day of week" do
45  
-      executing("select * from generate_recurrences('positive_week_dow', '28 days', '2008-04-15', '2008-04-15', '2008-06-17');").should == [
  65
+      executing("select * from generate_recurrences('1 month', '2008-04-01', '2008-06-30', NULL, 3, 2);").should == [
46 66
         ['2008-04-15'],
47 67
         ['2008-05-20'],
48 68
         ['2008-06-17']
@@ -50,50 +70,29 @@
50 70
     end
51 71
 
52 72
     it "should include the dates on the particular negative offset week's day of week" do
53  
-      executing("select * from generate_recurrences('negative_week_dow', '28 days', '2008-04-17', '2008-04-17', '2008-06-19');").should == [
  73
+      executing("select * from generate_recurrences('1 month', '2008-04-01', '2008-06-30', NULL, -2, 4);").should == [
54 74
         ['2008-04-17'],
55 75
         ['2008-05-22'],
56 76
         ['2008-06-19']
57 77
       ]
58 78
     end
59 79
 
60  
-    it "should not get stuck in an infinite loop when the duration does not put us in the next month" do
61  
-      executing("select * from generate_recurrences('positive_week_dow', '28 days', '2008-04-01', '2008-04-01', '2008-06-03');").should == [
62  
-        ['2008-04-01'],
63  
-        ['2008-05-06'],
64  
-        ['2008-06-03']
65  
-      ]
66  
-    end
67  
-
68  
-    it "should not put the next recurrence in the wrong month if the duration does not put us in the right month next year" do
69  
-      executing("select * from generate_recurrences('positive_week_dow', '364 days', '2008-04-01', '2008-04-01', '2010-04-06');").should == [
70  
-        ['2008-04-01'],
71  
-        ['2009-04-07'],
72  
-        ['2010-04-06']
73  
-      ]
74  
-    end
75  
-
76  
-    it "should not skip the first recurrence if the ceiled intervals to range_start puts us in the next month" do
77  
-      executing("select * from generate_recurrences('positive_week_dow', '28 days', '2008-05-08', '2008-06-10', '2008-08-14');").should == [
78  
-        ['2008-06-12'],
79  
-        ['2008-07-10'],
80  
-        ['2008-08-14']
  80
+    it "should not skip a month if offsetting to the correct day of week puts us in the wrong month" do
  81
+      executing("select * from generate_recurrences('1 month', '2008-05-01', '2008-07-31', NULL, -1, 5);").should == [
  82
+        ['2008-05-30'],
  83
+        ['2008-06-27'],
  84
+        ['2008-07-25']
81 85
       ]
82 86
     end
83 87
 
84  
-    it "should not include recurrences before the original_date" do
85  
-      executing("select * from generate_recurrences('positive_week_dow', '28 days', '2008-05-01', '2008-04-01', '2008-06-05');").should == [
86  
-        ['2008-05-01'],
87  
-        ['2008-06-05']
88  
-      ]
89  
-    end
90  
-
91  
-    it "should return the correct week when the day of week is the same as that of the last day of the month" do
92  
-      executing("select * from generate_recurrences('negative_week_dow', '28 days', '2008-04-26', '2008-04-26', '2008-06-28');").should == [
93  
-        ['2008-04-26'],
94  
-        ['2008-05-31'],
95  
-        ['2008-06-28']
96  
-      ]
  88
+    describe 'by month' do
  89
+      it "should return the dates in the correct month" do
  90
+        executing("select * from generate_recurrences('1 year', '2008-04-01', '2010-06-30', 5, 3, 2);").should == [
  91
+          ['2008-05-20'],
  92
+          ['2009-05-19'],
  93
+          ['2010-05-18']
  94
+        ]
  95
+      end
97 96
     end
98 97
   end
99 98
 end
8  spec/interval_for_spec.rb
@@ -13,15 +13,7 @@
13 13
     executing("select interval_for('monthly');").should == [ ['1 mon'] ]
14 14
   end
15 15
 
16  
-  it "should return '28 days' for 'monthly_by_week_dow'" do
17  
-    executing("select interval_for('monthly_by_week_dow');").should == [ ['28 days'] ]
18  
-  end
19  
-
20 16
   it "should return '1 year' for 'yearly'" do
21 17
     executing("select interval_for('yearly');").should == [ ['1 year'] ]
22 18
   end
23  
-
24  
-  it "should return '364 days' for 'yearly_by_week_dow'" do
25  
-    executing("select interval_for('yearly_by_week_dow');").should == [ ['364 days'] ]
26  
-  end
27 19
 end

0 notes on commit a0ff4cd

Please sign in to comment.
Something went wrong with that request. Please try again.