]>
Commit | Line | Data |
---|---|---|
b7449926 | 1 | // run-pass |
29967ef6 | 2 | // ignore-compare-mode-chalk |
b7449926 | 3 | |
0531ce1d | 4 | #![feature(fn_traits, |
abe05a73 | 5 | step_trait, |
f9f354fc | 6 | step_trait_ext, |
2c00a5a8 | 7 | unboxed_closures, |
abe05a73 | 8 | )] |
5bcae85e SL |
9 | |
10 | //! Derived from: <https://raw.githubusercontent.com/quickfur/dcal/master/dcal.d>. | |
11 | //! | |
12 | //! Originally converted to Rust by [Daniel Keep](https://github.com/DanielKeep). | |
13 | ||
14 | use std::fmt::Write; | |
5bcae85e SL |
15 | |
16 | /// Date representation. | |
17 | #[derive(Copy, Clone, Debug, Eq, Ord, PartialEq, PartialOrd)] | |
18 | struct NaiveDate(i32, u32, u32); | |
19 | ||
20 | impl NaiveDate { | |
21 | pub fn from_ymd(y: i32, m: u32, d: u32) -> NaiveDate { | |
22 | assert!(1 <= m && m <= 12, "m = {:?}", m); | |
23 | assert!(1 <= d && d <= NaiveDate(y, m, 1).days_in_month(), "d = {:?}", d); | |
24 | NaiveDate(y, m, d) | |
25 | } | |
26 | ||
27 | pub fn year(&self) -> i32 { | |
28 | self.0 | |
29 | } | |
30 | ||
31 | pub fn month(&self) -> u32 { | |
32 | self.1 | |
33 | } | |
34 | ||
35 | pub fn day(&self) -> u32 { | |
36 | self.2 | |
37 | } | |
38 | ||
39 | pub fn succ(&self) -> NaiveDate { | |
40 | let (mut y, mut m, mut d, n) = ( | |
41 | self.year(), self.month(), self.day()+1, self.days_in_month()); | |
42 | if d > n { | |
43 | d = 1; | |
44 | m += 1; | |
45 | } | |
46 | if m > 12 { | |
47 | m = 1; | |
48 | y += 1; | |
49 | } | |
50 | NaiveDate::from_ymd(y, m, d) | |
51 | } | |
52 | ||
53 | pub fn weekday(&self) -> Weekday { | |
54 | use Weekday::*; | |
55 | ||
56 | // 0 = Sunday | |
57 | let year = self.year(); | |
58 | let dow_jan_1 = (year*365 + ((year-1) / 4) - ((year-1) / 100) + ((year-1) / 400)) % 7; | |
59 | let dow = (dow_jan_1 + (self.day_of_year() as i32 - 1)) % 7; | |
60 | [Sun, Mon, Tue, Wed, Thu, Fri, Sat][dow as usize] | |
61 | } | |
62 | ||
63 | pub fn isoweekdate(&self) -> (i32, u32, Weekday) { | |
64 | let first_dow_mon_0 = self.year_first_day_of_week().num_days_from_monday(); | |
65 | ||
66 | // Work out this date's DOtY and week number, not including year adjustment. | |
67 | let doy_0 = self.day_of_year() - 1; | |
68 | let mut week_mon_0: i32 = ((first_dow_mon_0 + doy_0) / 7) as i32; | |
69 | ||
70 | if self.first_week_in_prev_year() { | |
71 | week_mon_0 -= 1; | |
72 | } | |
73 | ||
74 | let weeks_in_year = self.last_week_number(); | |
75 | ||
76 | // Work out the final result. | |
0731742a | 77 | // If the week is `-1` or `>= weeks_in_year`, we will need to adjust the year. |
5bcae85e SL |
78 | let year = self.year(); |
79 | let wd = self.weekday(); | |
80 | ||
81 | if week_mon_0 < 0 { | |
82 | (year - 1, NaiveDate::from_ymd(year - 1, 1, 1).last_week_number(), wd) | |
83 | } else if week_mon_0 >= weeks_in_year as i32 { | |
84 | (year + 1, (week_mon_0 + 1 - weeks_in_year as i32) as u32, wd) | |
85 | } else { | |
86 | (year, (week_mon_0 + 1) as u32, wd) | |
87 | } | |
88 | } | |
89 | ||
90 | fn first_week_in_prev_year(&self) -> bool { | |
91 | let first_dow_mon_0 = self.year_first_day_of_week().num_days_from_monday(); | |
92 | ||
93 | // Any day in the year *before* the first Monday of that year | |
94 | // is considered to be in the last week of the previous year, | |
95 | // assuming the first week has *less* than four days in it. | |
96 | // Adjust the week appropriately. | |
97 | ((7 - first_dow_mon_0) % 7) < 4 | |
98 | } | |
99 | ||
100 | fn year_first_day_of_week(&self) -> Weekday { | |
101 | NaiveDate::from_ymd(self.year(), 1, 1).weekday() | |
102 | } | |
103 | ||
104 | fn weeks_in_year(&self) -> u32 { | |
105 | let days_in_last_week = self.year_first_day_of_week().num_days_from_monday() + 1; | |
106 | if days_in_last_week >= 4 { 53 } else { 52 } | |
107 | } | |
108 | ||
109 | fn last_week_number(&self) -> u32 { | |
110 | let wiy = self.weeks_in_year(); | |
111 | if self.first_week_in_prev_year() { wiy - 1 } else { wiy } | |
112 | } | |
113 | ||
114 | fn day_of_year(&self) -> u32 { | |
115 | (1..self.1).map(|m| NaiveDate::from_ymd(self.year(), m, 1).days_in_month()) | |
116 | .fold(0, |a,b| a+b) + self.day() | |
117 | } | |
118 | ||
119 | fn is_leap_year(&self) -> bool { | |
120 | let year = self.year(); | |
121 | if year % 4 != 0 { | |
122 | return false | |
123 | } else if year % 100 != 0 { | |
124 | return true | |
125 | } else if year % 400 != 0 { | |
126 | return false | |
127 | } else { | |
128 | return true | |
129 | } | |
130 | } | |
131 | ||
132 | fn days_in_month(&self) -> u32 { | |
133 | match self.month() { | |
134 | /* Jan */ 1 => 31, | |
135 | /* Feb */ 2 => if self.is_leap_year() { 29 } else { 28 }, | |
136 | /* Mar */ 3 => 31, | |
137 | /* Apr */ 4 => 30, | |
138 | /* May */ 5 => 31, | |
139 | /* Jun */ 6 => 30, | |
140 | /* Jul */ 7 => 31, | |
141 | /* Aug */ 8 => 31, | |
142 | /* Sep */ 9 => 30, | |
143 | /* Oct */ 10 => 31, | |
144 | /* Nov */ 11 => 30, | |
145 | /* Dec */ 12 => 31, | |
146 | _ => unreachable!() | |
147 | } | |
148 | } | |
149 | } | |
150 | ||
151 | impl<'a, 'b> std::ops::Add<&'b NaiveDate> for &'a NaiveDate { | |
152 | type Output = NaiveDate; | |
153 | ||
154 | fn add(self, other: &'b NaiveDate) -> NaiveDate { | |
155 | assert_eq!(*other, NaiveDate(0, 0, 1)); | |
156 | self.succ() | |
157 | } | |
158 | } | |
159 | ||
f9f354fc | 160 | unsafe impl std::iter::Step for NaiveDate { |
041b39d2 | 161 | fn steps_between(_: &Self, _: &Self) -> Option<usize> { |
5bcae85e SL |
162 | unimplemented!() |
163 | } | |
164 | ||
f9f354fc XL |
165 | fn forward_checked(start: Self, n: usize) -> Option<Self> { |
166 | Some((0..n).fold(start, |x, _| x.succ())) | |
5bcae85e SL |
167 | } |
168 | ||
f9f354fc | 169 | fn backward_checked(_: Self, _: usize) -> Option<Self> { |
dc9dc135 XL |
170 | unimplemented!() |
171 | } | |
5bcae85e SL |
172 | } |
173 | ||
174 | #[derive(Copy, Clone, Debug, Eq, Ord, PartialEq, PartialOrd)] | |
175 | pub enum Weekday { | |
176 | Mon, | |
177 | Tue, | |
178 | Wed, | |
179 | Thu, | |
180 | Fri, | |
181 | Sat, | |
182 | Sun, | |
183 | } | |
184 | ||
185 | impl Weekday { | |
186 | pub fn num_days_from_monday(&self) -> u32 { | |
187 | use Weekday::*; | |
188 | match *self { | |
189 | Mon => 0, | |
190 | Tue => 1, | |
191 | Wed => 2, | |
192 | Thu => 3, | |
193 | Fri => 4, | |
194 | Sat => 5, | |
195 | Sun => 6, | |
196 | } | |
197 | } | |
198 | ||
199 | pub fn num_days_from_sunday(&self) -> u32 { | |
200 | use Weekday::*; | |
201 | match *self { | |
202 | Sun => 0, | |
203 | Mon => 1, | |
204 | Tue => 2, | |
205 | Wed => 3, | |
206 | Thu => 4, | |
207 | Fri => 5, | |
208 | Sat => 6, | |
209 | } | |
210 | } | |
211 | } | |
212 | ||
0731742a | 213 | /// `GroupBy` implementation. |
5bcae85e SL |
214 | struct GroupBy<It: Iterator, F> { |
215 | it: std::iter::Peekable<It>, | |
216 | f: F, | |
217 | } | |
218 | ||
219 | impl<It, F> Clone for GroupBy<It, F> | |
2c00a5a8 XL |
220 | where |
221 | It: Iterator + Clone, | |
222 | It::Item: Clone, | |
223 | F: Clone, | |
224 | { | |
225 | fn clone(&self) -> Self { | |
5bcae85e SL |
226 | GroupBy { |
227 | it: self.it.clone(), | |
2c00a5a8 | 228 | f: self.f.clone(), |
5bcae85e SL |
229 | } |
230 | } | |
231 | } | |
232 | ||
233 | impl<'a, G, It: 'a, F: 'a> Iterator for GroupBy<It, F> | |
234 | where It: Iterator + Clone, | |
235 | It::Item: Clone, | |
236 | F: Clone + FnMut(&It::Item) -> G, | |
237 | G: Eq + Clone | |
238 | { | |
239 | type Item = (G, InGroup<std::iter::Peekable<It>, F, G>); | |
240 | ||
241 | fn next(&mut self) -> Option<Self::Item> { | |
242 | self.it.peek().map(&mut self.f).map(|key| { | |
243 | let start = self.it.clone(); | |
244 | while let Some(k) = self.it.peek().map(&mut self.f) { | |
245 | if key != k { | |
246 | break; | |
247 | } | |
248 | self.it.next(); | |
249 | } | |
250 | ||
251 | (key.clone(), InGroup { | |
252 | it: start, | |
253 | f: self.f.clone(), | |
254 | g: key | |
255 | }) | |
256 | }) | |
257 | } | |
258 | } | |
259 | ||
260 | #[derive(Copy, Clone)] | |
261 | struct InGroup<It, F, G> { | |
262 | it: It, | |
263 | f: F, | |
264 | g: G | |
265 | } | |
266 | ||
267 | impl<It: Iterator, F: FnMut(&It::Item) -> G, G: Eq> Iterator for InGroup<It, F, G> { | |
268 | type Item = It::Item; | |
269 | ||
270 | fn next(&mut self) -> Option<It::Item> { | |
271 | self.it.next().and_then(|x| { | |
272 | if (self.f)(&x) == self.g { Some(x) } else { None } | |
273 | }) | |
274 | } | |
275 | } | |
276 | ||
277 | trait IteratorExt: Iterator + Sized { | |
2c00a5a8 XL |
278 | fn group_by<G, F>(self, f: F) -> GroupBy<Self, F> |
279 | where F: Clone + FnMut(&Self::Item) -> G, | |
5bcae85e SL |
280 | G: Eq |
281 | { | |
2c00a5a8 | 282 | GroupBy { it: self.peekable(), f } |
5bcae85e SL |
283 | } |
284 | ||
285 | fn join(mut self, sep: &str) -> String | |
286 | where Self::Item: std::fmt::Display { | |
287 | let mut s = String::new(); | |
288 | if let Some(e) = self.next() { | |
13cf67c4 | 289 | write!(s, "{}", e).unwrap(); |
5bcae85e SL |
290 | for e in self { |
291 | s.push_str(sep); | |
13cf67c4 | 292 | write!(s, "{}", e).unwrap(); |
5bcae85e SL |
293 | } |
294 | } | |
295 | s | |
296 | } | |
297 | ||
0731742a | 298 | // HACK(eddyb): only needed because `impl Trait` can't be |
5bcae85e SL |
299 | // used with trait methods: `.foo()` becomes `.__(foo)`. |
300 | fn __<F, R>(self, f: F) -> R | |
301 | where F: FnOnce(Self) -> R { | |
302 | f(self) | |
303 | } | |
304 | } | |
305 | ||
306 | impl<It> IteratorExt for It where It: Iterator {} | |
307 | ||
0731742a | 308 | /// Generates an iterator that yields exactly `n` spaces. |
5bcae85e SL |
309 | fn spaces(n: usize) -> std::iter::Take<std::iter::Repeat<char>> { |
310 | std::iter::repeat(' ').take(n) | |
311 | } | |
312 | ||
313 | fn test_spaces() { | |
314 | assert_eq!(spaces(0).collect::<String>(), ""); | |
315 | assert_eq!(spaces(10).collect::<String>(), " ") | |
316 | } | |
317 | ||
5bcae85e | 318 | /// Returns an iterator of dates in a given year. |
5bcae85e SL |
319 | fn dates_in_year(year: i32) -> impl Iterator<Item=NaiveDate>+Clone { |
320 | InGroup { | |
321 | it: NaiveDate::from_ymd(year, 1, 1).., | |
2c00a5a8 | 322 | f: |d: &NaiveDate| d.year(), |
5bcae85e SL |
323 | g: year |
324 | } | |
325 | } | |
326 | ||
327 | fn test_dates_in_year() { | |
328 | { | |
329 | let mut dates = dates_in_year(2013); | |
330 | assert_eq!(dates.next(), Some(NaiveDate::from_ymd(2013, 1, 1))); | |
331 | ||
0731742a | 332 | // Check increment. |
5bcae85e SL |
333 | assert_eq!(dates.next(), Some(NaiveDate::from_ymd(2013, 1, 2))); |
334 | ||
0731742a | 335 | // Check monthly roll-over. |
5bcae85e SL |
336 | for _ in 3..31 { |
337 | assert!(dates.next() != None); | |
338 | } | |
339 | ||
340 | assert_eq!(dates.next(), Some(NaiveDate::from_ymd(2013, 1, 31))); | |
341 | assert_eq!(dates.next(), Some(NaiveDate::from_ymd(2013, 2, 1))); | |
342 | } | |
343 | ||
344 | { | |
0731742a | 345 | // Check length of year. |
5bcae85e SL |
346 | let mut dates = dates_in_year(2013); |
347 | for _ in 0..365 { | |
348 | assert!(dates.next() != None); | |
349 | } | |
350 | assert_eq!(dates.next(), None); | |
351 | } | |
352 | ||
353 | { | |
0731742a | 354 | // Check length of leap year. |
5bcae85e SL |
355 | let mut dates = dates_in_year(1984); |
356 | for _ in 0..366 { | |
357 | assert!(dates.next() != None); | |
358 | } | |
359 | assert_eq!(dates.next(), None); | |
360 | } | |
361 | } | |
362 | ||
5bcae85e SL |
363 | /// Convenience trait for verifying that a given type iterates over |
364 | /// `NaiveDate`s. | |
5bcae85e SL |
365 | trait DateIterator: Iterator<Item=NaiveDate> + Clone {} |
366 | impl<It> DateIterator for It where It: Iterator<Item=NaiveDate> + Clone {} | |
367 | ||
368 | fn test_group_by() { | |
369 | let input = [ | |
370 | [1, 1], | |
371 | [1, 1], | |
372 | [1, 2], | |
373 | [2, 2], | |
374 | [2, 3], | |
375 | [2, 3], | |
376 | [3, 3] | |
377 | ]; | |
378 | ||
379 | let by_x = input.iter().cloned().group_by(|a| a[0]); | |
380 | let expected_1: &[&[[i32; 2]]] = &[ | |
381 | &[[1, 1], [1, 1], [1, 2]], | |
382 | &[[2, 2], [2, 3], [2, 3]], | |
383 | &[[3, 3]] | |
384 | ]; | |
385 | for ((_, a), b) in by_x.zip(expected_1.iter().cloned()) { | |
386 | assert_eq!(&a.collect::<Vec<_>>()[..], b); | |
387 | } | |
388 | ||
389 | let by_y = input.iter().cloned().group_by(|a| a[1]); | |
390 | let expected_2: &[&[[i32; 2]]] = &[ | |
391 | &[[1, 1], [1, 1]], | |
392 | &[[1, 2], [2, 2]], | |
393 | &[[2, 3], [2, 3], [3, 3]] | |
394 | ]; | |
395 | for ((_, a), b) in by_y.zip(expected_2.iter().cloned()) { | |
396 | assert_eq!(&a.collect::<Vec<_>>()[..], b); | |
397 | } | |
398 | } | |
399 | ||
5bcae85e | 400 | /// Groups an iterator of dates by month. |
abe05a73 | 401 | fn by_month(it: impl Iterator<Item=NaiveDate> + Clone) |
0731742a | 402 | -> impl Iterator<Item=(u32, impl Iterator<Item=NaiveDate> + Clone)> + Clone |
abe05a73 | 403 | { |
5bcae85e SL |
404 | it.group_by(|d| d.month()) |
405 | } | |
406 | ||
407 | fn test_by_month() { | |
408 | let mut months = dates_in_year(2013).__(by_month); | |
409 | for (month, (_, mut date)) in (1..13).zip(&mut months) { | |
410 | assert_eq!(date.nth(0).unwrap(), NaiveDate::from_ymd(2013, month, 1)); | |
411 | } | |
412 | assert!(months.next().is_none()); | |
413 | } | |
414 | ||
5bcae85e | 415 | /// Groups an iterator of dates by week. |
abe05a73 XL |
416 | fn by_week(it: impl DateIterator) |
417 | -> impl Iterator<Item=(u32, impl DateIterator)> + Clone | |
418 | { | |
5bcae85e SL |
419 | // We go forward one day because `isoweekdate` considers the week to start on a Monday. |
420 | it.group_by(|d| d.succ().isoweekdate().1) | |
421 | } | |
422 | ||
423 | fn test_isoweekdate() { | |
424 | fn weeks_uniq(year: i32) -> Vec<((i32, u32), u32)> { | |
425 | let mut weeks = dates_in_year(year).map(|d| d.isoweekdate()) | |
426 | .map(|(y,w,_)| (y,w)); | |
427 | let mut result = vec![]; | |
428 | let mut accum = (weeks.next().unwrap(), 1); | |
429 | for yw in weeks { | |
430 | if accum.0 == yw { | |
431 | accum.1 += 1; | |
432 | } else { | |
433 | result.push(accum); | |
434 | accum = (yw, 1); | |
435 | } | |
436 | } | |
437 | result.push(accum); | |
438 | result | |
439 | } | |
440 | ||
441 | let wu_1984 = weeks_uniq(1984); | |
442 | assert_eq!(&wu_1984[..2], &[((1983, 52), 1), ((1984, 1), 7)]); | |
443 | assert_eq!(&wu_1984[wu_1984.len()-2..], &[((1984, 52), 7), ((1985, 1), 1)]); | |
444 | ||
445 | let wu_2013 = weeks_uniq(2013); | |
446 | assert_eq!(&wu_2013[..2], &[((2013, 1), 6), ((2013, 2), 7)]); | |
447 | assert_eq!(&wu_2013[wu_2013.len()-2..], &[((2013, 52), 7), ((2014, 1), 2)]); | |
448 | ||
449 | let wu_2015 = weeks_uniq(2015); | |
450 | assert_eq!(&wu_2015[..2], &[((2015, 1), 4), ((2015, 2), 7)]); | |
451 | assert_eq!(&wu_2015[wu_2015.len()-2..], &[((2015, 52), 7), ((2015, 53), 4)]); | |
452 | } | |
453 | ||
454 | fn test_by_week() { | |
455 | let mut weeks = dates_in_year(2013).__(by_week); | |
456 | assert_eq!( | |
457 | &*weeks.next().unwrap().1.collect::<Vec<_>>(), | |
458 | &[ | |
459 | NaiveDate::from_ymd(2013, 1, 1), | |
460 | NaiveDate::from_ymd(2013, 1, 2), | |
461 | NaiveDate::from_ymd(2013, 1, 3), | |
462 | NaiveDate::from_ymd(2013, 1, 4), | |
463 | NaiveDate::from_ymd(2013, 1, 5), | |
464 | ] | |
465 | ); | |
466 | assert_eq!( | |
467 | &*weeks.next().unwrap().1.collect::<Vec<_>>(), | |
468 | &[ | |
469 | NaiveDate::from_ymd(2013, 1, 6), | |
470 | NaiveDate::from_ymd(2013, 1, 7), | |
471 | NaiveDate::from_ymd(2013, 1, 8), | |
472 | NaiveDate::from_ymd(2013, 1, 9), | |
473 | NaiveDate::from_ymd(2013, 1, 10), | |
474 | NaiveDate::from_ymd(2013, 1, 11), | |
475 | NaiveDate::from_ymd(2013, 1, 12), | |
476 | ] | |
477 | ); | |
478 | assert_eq!(weeks.next().unwrap().1.nth(0).unwrap(), NaiveDate::from_ymd(2013, 1, 13)); | |
479 | } | |
480 | ||
481 | /// The number of columns per day in the formatted output. | |
482 | const COLS_PER_DAY: u32 = 3; | |
483 | ||
484 | /// The number of columns per week in the formatted output. | |
485 | const COLS_PER_WEEK: u32 = 7 * COLS_PER_DAY; | |
486 | ||
5bcae85e | 487 | /// Formats an iterator of weeks into an iterator of strings. |
abe05a73 | 488 | fn format_weeks(it: impl Iterator<Item = impl DateIterator>) -> impl Iterator<Item=String> { |
5bcae85e SL |
489 | it.map(|week| { |
490 | let mut buf = String::with_capacity((COLS_PER_DAY * COLS_PER_WEEK + 2) as usize); | |
491 | ||
492 | // Format each day into its own cell and append to target string. | |
493 | let mut last_day = 0; | |
494 | let mut first = true; | |
495 | for d in week { | |
496 | last_day = d.weekday().num_days_from_sunday(); | |
497 | ||
498 | // Insert enough filler to align the first day with its respective day-of-week. | |
499 | if first { | |
500 | buf.extend(spaces((COLS_PER_DAY * last_day) as usize)); | |
501 | first = false; | |
502 | } | |
503 | ||
13cf67c4 | 504 | write!(buf, " {:>2}", d.day()).unwrap(); |
5bcae85e SL |
505 | } |
506 | ||
507 | // Insert more filler at the end to fill up the remainder of the week, | |
0731742a | 508 | // if its a short week (e.g., at the end of the month). |
5bcae85e SL |
509 | buf.extend(spaces((COLS_PER_DAY * (6 - last_day)) as usize)); |
510 | buf | |
511 | }) | |
512 | } | |
513 | ||
514 | fn test_format_weeks() { | |
515 | let jan_2013 = dates_in_year(2013) | |
516 | .__(by_month).next() // pick January 2013 for testing purposes | |
517 | // NOTE: This `map` is because `next` returns an `Option<_>`. | |
518 | .map(|(_, month)| | |
519 | month.__(by_week) | |
520 | .map(|(_, weeks)| weeks) | |
521 | .__(format_weeks) | |
522 | .join("\n")); | |
523 | ||
524 | assert_eq!( | |
525 | jan_2013.as_ref().map(|s| &**s), | |
526 | Some(" 1 2 3 4 5\n\ | |
527 | \x20 6 7 8 9 10 11 12\n\ | |
528 | \x2013 14 15 16 17 18 19\n\ | |
529 | \x2020 21 22 23 24 25 26\n\ | |
530 | \x2027 28 29 30 31 ") | |
531 | ); | |
532 | } | |
533 | ||
0731742a | 534 | /// Formats the name of a month, centered on `COLS_PER_WEEK`. |
5bcae85e SL |
535 | fn month_title(month: u32) -> String { |
536 | const MONTH_NAMES: &'static [&'static str] = &[ | |
537 | "January", "February", "March", "April", "May", "June", | |
538 | "July", "August", "September", "October", "November", "December" | |
539 | ]; | |
540 | assert_eq!(MONTH_NAMES.len(), 12); | |
541 | ||
542 | // Determine how many spaces before and after the month name | |
543 | // we need to center it over the formatted weeks in the month. | |
544 | let name = MONTH_NAMES[(month - 1) as usize]; | |
545 | assert!(name.len() < COLS_PER_WEEK as usize); | |
546 | let before = (COLS_PER_WEEK as usize - name.len()) / 2; | |
547 | let after = COLS_PER_WEEK as usize - name.len() - before; | |
548 | ||
0731742a | 549 | // Note: being slightly more verbose to avoid extra allocations. |
5bcae85e SL |
550 | let mut result = String::with_capacity(COLS_PER_WEEK as usize); |
551 | result.extend(spaces(before)); | |
552 | result.push_str(name); | |
553 | result.extend(spaces(after)); | |
554 | result | |
555 | } | |
556 | ||
557 | fn test_month_title() { | |
558 | assert_eq!(month_title(1).len(), COLS_PER_WEEK as usize); | |
559 | } | |
560 | ||
5bcae85e | 561 | /// Formats a month. |
abe05a73 | 562 | fn format_month(it: impl DateIterator) -> impl Iterator<Item=String> { |
5bcae85e SL |
563 | let mut month_days = it.peekable(); |
564 | let title = month_title(month_days.peek().unwrap().month()); | |
565 | ||
566 | Some(title).into_iter() | |
567 | .chain(month_days.__(by_week) | |
568 | .map(|(_, week)| week) | |
569 | .__(format_weeks)) | |
570 | } | |
571 | ||
572 | fn test_format_month() { | |
573 | let month_fmt = dates_in_year(2013) | |
574 | .__(by_month).next() // Pick January as a test case | |
575 | .map(|(_, days)| days.into_iter() | |
576 | .__(format_month) | |
577 | .join("\n")); | |
578 | ||
579 | assert_eq!( | |
580 | month_fmt.as_ref().map(|s| &**s), | |
581 | Some(" January \n\ | |
582 | \x20 1 2 3 4 5\n\ | |
583 | \x20 6 7 8 9 10 11 12\n\ | |
584 | \x2013 14 15 16 17 18 19\n\ | |
585 | \x2020 21 22 23 24 25 26\n\ | |
586 | \x2027 28 29 30 31 ") | |
587 | ); | |
588 | } | |
589 | ||
5bcae85e | 590 | /// Formats an iterator of months. |
abe05a73 XL |
591 | fn format_months(it: impl Iterator<Item = impl DateIterator>) |
592 | -> impl Iterator<Item=impl Iterator<Item=String>> | |
593 | { | |
5bcae85e SL |
594 | it.map(format_month) |
595 | } | |
596 | ||
5bcae85e SL |
597 | /// Takes an iterator of iterators of strings; the sub-iterators are consumed |
598 | /// in lock-step, with their elements joined together. | |
5bcae85e | 599 | trait PasteBlocks: Iterator + Sized |
0731742a | 600 | where Self::Item: Iterator<Item = String> { |
5bcae85e SL |
601 | fn paste_blocks(self, sep_width: usize) -> PasteBlocksIter<Self::Item> { |
602 | PasteBlocksIter { | |
603 | iters: self.collect(), | |
604 | cache: vec![], | |
605 | col_widths: None, | |
606 | sep_width: sep_width, | |
607 | } | |
608 | } | |
609 | } | |
610 | ||
611 | impl<It> PasteBlocks for It where It: Iterator, It::Item: Iterator<Item=String> {} | |
612 | ||
613 | struct PasteBlocksIter<StrIt> | |
614 | where StrIt: Iterator<Item=String> { | |
615 | iters: Vec<StrIt>, | |
616 | cache: Vec<Option<String>>, | |
617 | col_widths: Option<Vec<usize>>, | |
618 | sep_width: usize, | |
619 | } | |
620 | ||
621 | impl<StrIt> Iterator for PasteBlocksIter<StrIt> | |
622 | where StrIt: Iterator<Item=String> { | |
623 | type Item = String; | |
624 | ||
625 | fn next(&mut self) -> Option<String> { | |
626 | self.cache.clear(); | |
627 | ||
628 | // `cache` is now the next line from each iterator. | |
629 | self.cache.extend(self.iters.iter_mut().map(|it| it.next())); | |
630 | ||
631 | // If every line in `cache` is `None`, we have nothing further to do. | |
632 | if self.cache.iter().all(|e| e.is_none()) { return None } | |
633 | ||
634 | // Get the column widths if we haven't already. | |
635 | let col_widths = match self.col_widths { | |
636 | Some(ref v) => &**v, | |
637 | None => { | |
638 | self.col_widths = Some(self.cache.iter() | |
639 | .map(|ms| ms.as_ref().map(|s| s.len()).unwrap_or(0)) | |
640 | .collect()); | |
641 | &**self.col_widths.as_ref().unwrap() | |
642 | } | |
643 | }; | |
644 | ||
645 | // Fill in any `None`s with spaces. | |
646 | let mut parts = col_widths.iter().cloned().zip(self.cache.iter_mut()) | |
647 | .map(|(w,ms)| ms.take().unwrap_or_else(|| spaces(w).collect())); | |
648 | ||
649 | // Join them all together. | |
650 | let first = parts.next().unwrap_or(String::new()); | |
651 | let sep_width = self.sep_width; | |
652 | Some(parts.fold(first, |mut accum, next| { | |
653 | accum.extend(spaces(sep_width)); | |
654 | accum.push_str(&next); | |
655 | accum | |
656 | })) | |
657 | } | |
658 | } | |
659 | ||
660 | fn test_paste_blocks() { | |
661 | let row = dates_in_year(2013) | |
662 | .__(by_month).map(|(_, days)| days) | |
663 | .take(3) | |
664 | .__(format_months) | |
665 | .paste_blocks(1) | |
666 | .join("\n"); | |
667 | assert_eq!( | |
668 | &*row, | |
669 | " January February March \n\ | |
670 | \x20 1 2 3 4 5 1 2 1 2\n\ | |
671 | \x20 6 7 8 9 10 11 12 3 4 5 6 7 8 9 3 4 5 6 7 8 9\n\ | |
672 | \x2013 14 15 16 17 18 19 10 11 12 13 14 15 16 10 11 12 13 14 15 16\n\ | |
673 | \x2020 21 22 23 24 25 26 17 18 19 20 21 22 23 17 18 19 20 21 22 23\n\ | |
674 | \x2027 28 29 30 31 24 25 26 27 28 24 25 26 27 28 29 30\n\ | |
675 | \x20 31 " | |
676 | ); | |
677 | } | |
678 | ||
5bcae85e | 679 | /// Produces an iterator that yields `n` elements at a time. |
5bcae85e SL |
680 | trait Chunks: Iterator + Sized { |
681 | fn chunks(self, n: usize) -> ChunksIter<Self> { | |
682 | assert!(n > 0); | |
683 | ChunksIter { | |
684 | it: self, | |
685 | n: n, | |
686 | } | |
687 | } | |
688 | } | |
689 | ||
690 | impl<It> Chunks for It where It: Iterator {} | |
691 | ||
692 | struct ChunksIter<It> | |
693 | where It: Iterator { | |
694 | it: It, | |
695 | n: usize, | |
696 | } | |
697 | ||
0731742a | 698 | // Note: `chunks` in Rust is more-or-less impossible without overhead of some kind. |
5bcae85e SL |
699 | // Aliasing rules mean you need to add dynamic borrow checking, and the design of |
700 | // `Iterator` means that you need to have the iterator's state kept in an allocation | |
701 | // that is jointly owned by the iterator itself and the sub-iterator. | |
702 | // As such, I've chosen to cop-out and just heap-allocate each chunk. | |
703 | ||
704 | impl<It> Iterator for ChunksIter<It> | |
705 | where It: Iterator { | |
706 | type Item = Vec<It::Item>; | |
707 | ||
708 | fn next(&mut self) -> Option<Vec<It::Item>> { | |
0731742a | 709 | let first = self.it.next()?; |
5bcae85e SL |
710 | |
711 | let mut result = Vec::with_capacity(self.n); | |
712 | result.push(first); | |
713 | ||
714 | Some((&mut self.it).take(self.n-1) | |
715 | .fold(result, |mut acc, next| { acc.push(next); acc })) | |
716 | } | |
717 | } | |
718 | ||
719 | fn test_chunks() { | |
720 | let r = &[1, 2, 3, 4, 5, 6, 7]; | |
721 | let c = r.iter().cloned().chunks(3).collect::<Vec<_>>(); | |
722 | assert_eq!(&*c, &[vec![1, 2, 3], vec![4, 5, 6], vec![7]]); | |
723 | } | |
724 | ||
5bcae85e | 725 | /// Formats a year. |
5bcae85e SL |
726 | fn format_year(year: i32, months_per_row: usize) -> String { |
727 | const COL_SPACING: usize = 1; | |
728 | ||
729 | // Start by generating all dates for the given year. | |
730 | dates_in_year(year) | |
731 | ||
732 | // Group them by month and throw away month number. | |
733 | .__(by_month).map(|(_, days)| days) | |
734 | ||
735 | // Group the months into horizontal rows. | |
736 | .chunks(months_per_row) | |
737 | ||
0731742a | 738 | // Format each row... |
5bcae85e | 739 | .map(|r| r.into_iter() |
0731742a | 740 | // ... by formatting each month ... |
5bcae85e SL |
741 | .__(format_months) |
742 | ||
0731742a | 743 | // ... and horizontally pasting each respective month's lines together. |
5bcae85e SL |
744 | .paste_blocks(COL_SPACING) |
745 | .join("\n") | |
746 | ) | |
747 | ||
0731742a | 748 | // Insert a blank line between each row. |
5bcae85e SL |
749 | .join("\n\n") |
750 | } | |
751 | ||
752 | fn test_format_year() { | |
753 | const MONTHS_PER_ROW: usize = 3; | |
754 | ||
755 | macro_rules! assert_eq_cal { | |
756 | ($lhs:expr, $rhs:expr) => { | |
757 | if $lhs != $rhs { | |
758 | println!("got:\n```\n{}\n```\n", $lhs.replace(" ", ".")); | |
759 | println!("expected:\n```\n{}\n```", $rhs.replace(" ", ".")); | |
760 | panic!("calendars didn't match!"); | |
761 | } | |
762 | } | |
763 | } | |
764 | ||
765 | assert_eq_cal!(&format_year(1984, MONTHS_PER_ROW), "\ | |
766 | \x20 January February March \n\ | |
767 | \x20 1 2 3 4 5 6 7 1 2 3 4 1 2 3\n\ | |
768 | \x20 8 9 10 11 12 13 14 5 6 7 8 9 10 11 4 5 6 7 8 9 10\n\ | |
769 | \x2015 16 17 18 19 20 21 12 13 14 15 16 17 18 11 12 13 14 15 16 17\n\ | |
770 | \x2022 23 24 25 26 27 28 19 20 21 22 23 24 25 18 19 20 21 22 23 24\n\ | |
771 | \x2029 30 31 26 27 28 29 25 26 27 28 29 30 31\n\ | |
772 | \n\ | |
773 | \x20 April May June \n\ | |
774 | \x20 1 2 3 4 5 6 7 1 2 3 4 5 1 2\n\ | |
775 | \x20 8 9 10 11 12 13 14 6 7 8 9 10 11 12 3 4 5 6 7 8 9\n\ | |
776 | \x2015 16 17 18 19 20 21 13 14 15 16 17 18 19 10 11 12 13 14 15 16\n\ | |
777 | \x2022 23 24 25 26 27 28 20 21 22 23 24 25 26 17 18 19 20 21 22 23\n\ | |
778 | \x2029 30 27 28 29 30 31 24 25 26 27 28 29 30\n\ | |
779 | \n\ | |
780 | \x20 July August September \n\ | |
781 | \x20 1 2 3 4 5 6 7 1 2 3 4 1\n\ | |
782 | \x20 8 9 10 11 12 13 14 5 6 7 8 9 10 11 2 3 4 5 6 7 8\n\ | |
783 | \x2015 16 17 18 19 20 21 12 13 14 15 16 17 18 9 10 11 12 13 14 15\n\ | |
784 | \x2022 23 24 25 26 27 28 19 20 21 22 23 24 25 16 17 18 19 20 21 22\n\ | |
785 | \x2029 30 31 26 27 28 29 30 31 23 24 25 26 27 28 29\n\ | |
786 | \x20 30 \n\ | |
787 | \n\ | |
788 | \x20 October November December \n\ | |
789 | \x20 1 2 3 4 5 6 1 2 3 1\n\ | |
790 | \x20 7 8 9 10 11 12 13 4 5 6 7 8 9 10 2 3 4 5 6 7 8\n\ | |
791 | \x2014 15 16 17 18 19 20 11 12 13 14 15 16 17 9 10 11 12 13 14 15\n\ | |
792 | \x2021 22 23 24 25 26 27 18 19 20 21 22 23 24 16 17 18 19 20 21 22\n\ | |
793 | \x2028 29 30 31 25 26 27 28 29 30 23 24 25 26 27 28 29\n\ | |
794 | \x20 30 31 "); | |
795 | ||
796 | assert_eq_cal!(&format_year(2015, MONTHS_PER_ROW), "\ | |
797 | \x20 January February March \n\ | |
798 | \x20 1 2 3 1 2 3 4 5 6 7 1 2 3 4 5 6 7\n\ | |
799 | \x20 4 5 6 7 8 9 10 8 9 10 11 12 13 14 8 9 10 11 12 13 14\n\ | |
800 | \x2011 12 13 14 15 16 17 15 16 17 18 19 20 21 15 16 17 18 19 20 21\n\ | |
801 | \x2018 19 20 21 22 23 24 22 23 24 25 26 27 28 22 23 24 25 26 27 28\n\ | |
802 | \x2025 26 27 28 29 30 31 29 30 31 \n\ | |
803 | \n\ | |
804 | \x20 April May June \n\ | |
805 | \x20 1 2 3 4 1 2 1 2 3 4 5 6\n\ | |
806 | \x20 5 6 7 8 9 10 11 3 4 5 6 7 8 9 7 8 9 10 11 12 13\n\ | |
807 | \x2012 13 14 15 16 17 18 10 11 12 13 14 15 16 14 15 16 17 18 19 20\n\ | |
808 | \x2019 20 21 22 23 24 25 17 18 19 20 21 22 23 21 22 23 24 25 26 27\n\ | |
809 | \x2026 27 28 29 30 24 25 26 27 28 29 30 28 29 30 \n\ | |
810 | \x20 31 \n\ | |
811 | \n\ | |
812 | \x20 July August September \n\ | |
813 | \x20 1 2 3 4 1 1 2 3 4 5\n\ | |
814 | \x20 5 6 7 8 9 10 11 2 3 4 5 6 7 8 6 7 8 9 10 11 12\n\ | |
815 | \x2012 13 14 15 16 17 18 9 10 11 12 13 14 15 13 14 15 16 17 18 19\n\ | |
816 | \x2019 20 21 22 23 24 25 16 17 18 19 20 21 22 20 21 22 23 24 25 26\n\ | |
817 | \x2026 27 28 29 30 31 23 24 25 26 27 28 29 27 28 29 30 \n\ | |
818 | \x20 30 31 \n\ | |
819 | \n\ | |
820 | \x20 October November December \n\ | |
821 | \x20 1 2 3 1 2 3 4 5 6 7 1 2 3 4 5\n\ | |
822 | \x20 4 5 6 7 8 9 10 8 9 10 11 12 13 14 6 7 8 9 10 11 12\n\ | |
823 | \x2011 12 13 14 15 16 17 15 16 17 18 19 20 21 13 14 15 16 17 18 19\n\ | |
824 | \x2018 19 20 21 22 23 24 22 23 24 25 26 27 28 20 21 22 23 24 25 26\n\ | |
825 | \x2025 26 27 28 29 30 31 29 30 27 28 29 30 31 "); | |
826 | } | |
827 | ||
828 | fn main() { | |
829 | // Run tests. | |
830 | test_spaces(); | |
831 | test_dates_in_year(); | |
832 | test_group_by(); | |
833 | test_by_month(); | |
834 | test_isoweekdate(); | |
835 | test_by_week(); | |
836 | test_format_weeks(); | |
837 | test_month_title(); | |
838 | test_format_month(); | |
839 | test_paste_blocks(); | |
840 | test_chunks(); | |
841 | test_format_year(); | |
842 | } |