1/*
2  Copyright (C) 2004-2007 Marcus Mueller
3  Copyright (C) 2007      Helge Hess
4
5  This file is part of SOPE.
6
7  SOPE is free software; you can redistribute it and/or modify it under
8  the terms of the GNU Lesser General Public License as published by the
9  Free Software Foundation; either version 2, or (at your option) any
10  later version.
11
12  SOPE is distributed in the hope that it will be useful, but WITHOUT ANY
13  WARRANTY; without even the implied warranty of MERCHANTABILITY or
14  FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public
15  License for more details.
16
17  You should have received a copy of the GNU Lesser General Public
18  License along with SOPE; see the file COPYING.  If not, write to the
19  Free Software Foundation, 59 Temple Place - Suite 330, Boston, MA
20  02111-1307, USA.
21*/
22
23#include "NGCalendarDateRange.h"
24#include <NGExtensions/NSCalendarDate+misc.h>
25#include <NGExtensions/NSNull+misc.h>
26#include "common.h"
27
28@implementation NGCalendarDateRange
29
30+ (id)calendarDateRangeWithStartDate:(NSCalendarDate *)start
31  endDate:(NSCalendarDate *)end
32{
33  return [[[self alloc] initWithStartDate:start endDate:end] autorelease];
34}
35
36- (id)initWithStartDate:(NSCalendarDate *)start endDate:(NSCalendarDate *)end {
37  NSAssert(start != nil, @"startDate MUST NOT be nil!");
38  NSAssert(end   != nil, @"endDate MUST NOT be nil!");
39
40  if ((self = [super init])) {
41    if ([start compare:end] == NSOrderedAscending) {
42      self->startDate = [start copy];
43      self->endDate   = [end   copy];
44    }
45    else {
46      self->startDate = [end   copy];
47      self->endDate   = [start copy];
48    }
49  }
50  return self;
51}
52
53- (void)dealloc {
54  [self->startDate release];
55  [self->endDate  release];
56  [super dealloc];
57}
58
59/* NSCopying */
60
61- (id)copyWithZone:(NSZone *)zone {
62  /* object is immutable */
63  return [self retain];
64}
65
66/* accessors */
67
68- (NSCalendarDate *)startDate {
69  return self->startDate;
70}
71
72- (NSCalendarDate *)endDate {
73  return self->endDate;
74}
75
76- (NGCalendarDateRange *)intersectionDateRange:(NGCalendarDateRange *)other {
77  NSCalendarDate *b, *c, *d;
78
79  if ([self compare:other] == NSOrderedAscending) {
80    b = self->endDate;
81    c = [other startDate];
82    d = [other endDate];
83  }
84  else {
85    b = [other endDate];
86    c = self->startDate;
87    d = self->endDate;
88  }
89  // [a;b[ ?< [c;d[
90  if ([b compare:c] == NSOrderedAscending)
91    return nil; // no intersection
92  // b ?< d
93  if ([b compare:d] == NSOrderedAscending) {
94    // c !< b  && b !< d -> [c;b[
95    if([c compare:b] == NSOrderedSame)
96      return nil; // no real range, thus return nil!
97    else
98      return [NGCalendarDateRange calendarDateRangeWithStartDate:c endDate:b];
99  }
100  // if([c compare:d] == NSOrderedSame)
101  //   return nil; // no real range, thus return nil!
102  // b !> d -> [c;d[
103  return [NGCalendarDateRange calendarDateRangeWithStartDate:c endDate:d];
104}
105
106- (BOOL)doesIntersectWithDateRange:(NGCalendarDateRange *)_other {
107  // TODO: improve
108  if (_other == nil) return NO;
109  return [self intersectionDateRange:_other] != nil ? YES : NO;
110}
111
112- (NGCalendarDateRange *)unionDateRange:(NGCalendarDateRange *)other {
113  NSCalendarDate *a, *b, *d;
114
115  if ([self compare:other] == NSOrderedAscending) {
116    a = self->startDate;
117    b = self->endDate;
118    d = [other endDate];
119  }
120  else {
121    a = [other startDate];
122    b = [other endDate];
123    d = self->endDate;
124  }
125  if ([b compare:d] == NSOrderedAscending)
126    return [NGCalendarDateRange calendarDateRangeWithStartDate:a endDate:d];
127
128  return [NGCalendarDateRange calendarDateRangeWithStartDate:a endDate:b];
129}
130
131- (BOOL)containsDate:(NSCalendarDate *)_date {
132  NSComparisonResult result;
133
134  result = [self->startDate compare:_date];
135  if (!((result == NSOrderedSame) || (result == NSOrderedAscending)))
136    return NO;
137  result = [self->endDate compare:_date];
138  if (result == NSOrderedAscending)
139    return NO;
140  return YES;
141}
142
143- (BOOL)containsDateRange:(NGCalendarDateRange *)_range {
144  NSComparisonResult result;
145
146  result = [self->startDate compare:[_range startDate]];
147  if (!((result == NSOrderedSame) || (result == NSOrderedAscending)))
148    return NO;
149  result = [self->endDate compare:[_range endDate]];
150  if (result == NSOrderedAscending)
151    return NO;
152  return YES;
153}
154
155- (NSTimeInterval)duration {
156  return [self->endDate timeIntervalSinceDate:self->startDate];
157}
158
159/* comparison */
160
161- (BOOL)isEqual:(id)other {
162  if (other == nil)
163    return NO;
164  if (other == self)
165    return YES;
166
167  if ([other isKindOfClass: object_getClass(self)] == NO)
168    return NO;
169
170  return ([self->startDate isEqual:[other startDate]] &&
171	  [self->endDate isEqual:[other endDate]]) ? YES : NO;
172}
173
174- (NSUInteger)hash {
175  return [self->startDate hash] ^ [self->endDate hash];
176}
177
178- (NSComparisonResult)compare:(NGCalendarDateRange *)other {
179  return [self->startDate compare:[other startDate]];
180}
181
182/* KVC */
183
184- (id)valueForUndefinedKey:(NSString *)_key {
185  /* eg this is used in OGo on 'dateId' to probe for event objects */
186  return nil;
187}
188
189/* description */
190
191- (NSString *)description {
192  NSMutableString *description;
193
194  description = [NSMutableString stringWithCapacity:64];
195
196  [description appendFormat:@"<%@[0x%p]: startDate:%@ endDate: ",
197	         NSStringFromClass(object_getClass(self)), self, self->startDate];
198
199  if ([self->startDate isEqual:self->endDate])
200    [description appendString:@"== startDate"];
201  else
202    [description appendFormat:@"%@", self->endDate];
203  [description appendString:@">"];
204  return description;
205}
206
207@end /* NGCalendarDateRange */
208
209
210@implementation NSArray(NGCalendarDateRanges)
211
212- (NSArray *)arrayByCreatingDateRangesFromObjectsWithStartDateKey:(NSString *)s
213  andEndDateKey:(NSString *)e
214{
215  NSMutableArray *ma;
216  NSUInteger     i, count;
217
218  count = [self count];
219  ma    = [NSMutableArray arrayWithCapacity:count];
220  for (i = 0; i < count; i++) {
221    NGCalendarDateRange *daterange;
222    NSCalendarDate *start, *end;
223    id object;
224
225    object = [self objectAtIndex:i];
226    start  = [object valueForKey:s];
227    end    = [object valueForKey:e];
228
229    /* skip invalid data */
230    if (![start isNotNull]) continue;
231    if (![end   isNotNull]) continue;
232
233    daterange =
234      [[NGCalendarDateRange alloc] initWithStartDate:start endDate:end];
235    if (daterange) [ma addObject:daterange];
236    [daterange release];
237  }
238  return ma;
239}
240
241- (BOOL)dateRangeArrayContainsDate:(NSCalendarDate *)_date {
242  NSUInteger i, count;
243
244  if (_date == nil)
245    return NO;
246  if ((count = [self count]) == 0)
247    return NO;
248
249  for (i = 0; i < count; i++) {
250    if ([[self objectAtIndex:i] containsDate:_date])
251      return YES;
252  }
253  return NO;
254}
255- (NSUInteger)indexOfFirstIntersectingDateRange:(NGCalendarDateRange *)_range {
256  NSUInteger i, count;
257
258  if (_range == nil)
259    return NO;
260
261  if ((count = [self count]) == 0)
262    return NSNotFound;
263
264  for (i = 0; i < count; i++) {
265    if ([[self objectAtIndex:i] doesIntersectWithDateRange:_range])
266      return i;
267  }
268  return NSNotFound;
269}
270
271- (NSArray *)arrayByCompactingContainedDateRanges {
272  // TODO: this is a candidate for unit testing ...
273  // TODO: pretty "slow" algorithm, improve
274  NSMutableArray *ma;
275  NSUInteger     i, count;
276
277  count = [self count];
278  if (count < 2)
279    return [[self copy] autorelease];
280
281  ma = [NSMutableArray arrayWithCapacity:count];
282  [ma addObject:[self objectAtIndex:0]]; /* add first range */
283
284  for (i = 1; i < count; i++) {
285    NGCalendarDateRange *rangeToAdd;
286    NGCalendarDateRange *availRange;
287    NGCalendarDateRange *newRange;
288    NSUInteger idx;
289
290    rangeToAdd = [self objectAtIndex:i];
291    idx = [ma indexOfFirstIntersectingDateRange:rangeToAdd];
292
293    if (idx == NSNotFound) {
294      /* range not yet covered in array */
295      [ma addObject:rangeToAdd];
296      continue;
297    }
298
299    /* union old range and replace the entry */
300
301    availRange = [ma objectAtIndex:idx];
302    newRange   = [availRange unionDateRange:rangeToAdd];
303
304    [ma replaceObjectAtIndex:idx withObject:newRange];
305  }
306  /* Note: we might want to join ranges up to some "closeness" (eg 1s)? */
307  return [ma sortedArrayUsingSelector:@selector(compare:)];
308}
309
310@end /* NSArray(NGCalendarDateRanges) */
311