]> git.ipfire.org Git - thirdparty/util-linux.git/blob - misc-utils/cal.c
cal: fix signed integer overflow [AddressSanitizer]
[thirdparty/util-linux.git] / misc-utils / cal.c
1 /*
2 * Copyright (c) 1989, 1993, 1994
3 * The Regents of the University of California. All rights reserved.
4 *
5 * This code is derived from software contributed to Berkeley by
6 * Kim Letkeman.
7 *
8 * Redistribution and use in source and binary forms, with or without
9 * modification, are permitted provided that the following conditions
10 * are met:
11 * 1. Redistributions of source code must retain the above copyright
12 * notice, this list of conditions and the following disclaimer.
13 * 2. Redistributions in binary form must reproduce the above copyright
14 * notice, this list of conditions and the following disclaimer in the
15 * documentation and/or other materials provided with the distribution.
16 * 3. All advertising materials mentioning features or use of this software
17 * must display the following acknowledgement:
18 * This product includes software developed by the University of
19 * California, Berkeley and its contributors.
20 * 4. Neither the name of the University nor the names of its contributors
21 * may be used to endorse or promote products derived from this software
22 * without specific prior written permission.
23 *
24 * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
25 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
26 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
27 * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
28 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
29 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
30 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
31 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
32 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
33 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
34 * SUCH DAMAGE.
35 */
36
37 /* 1999-02-01 Jean-Francois Bignolles: added option '-m' to display
38 * monday as the first day of the week.
39 * 1999-02-22 Arkadiusz Miƛkiewicz <misiek@pld.ORG.PL>
40 * - added Native Language Support
41 *
42 * 2000-09-01 Michael Charles Pruznick <dummy@netwiz.net>
43 * Added "-3" option to print prev/next month with current.
44 * Added over-ridable default NUM_MONTHS and "-1" option to
45 * get traditional output when -3 is the default. I hope that
46 * enough people will like -3 as the default that one day the
47 * product can be shipped that way.
48 *
49 * 2001-05-07 Pablo Saratxaga <pablo@mandrakesoft.com>
50 * Fixed the bugs with multi-byte charset (zg: cjk, utf-8)
51 * displaying. made the 'month year' ("%s %d") header translatable
52 * so it can be adapted to conventions used by different languages
53 * added support to read "first_weekday" locale information
54 * still to do: support for 'cal_direction' (will require a major
55 * rewrite of the displaying) and proper handling of RTL scripts
56 */
57
58 #include <sys/types.h>
59
60 #include <ctype.h>
61 #include <getopt.h>
62 #include <stdio.h>
63 #include <stdlib.h>
64 #include <string.h>
65 #include <time.h>
66 #include <unistd.h>
67 #include <errno.h>
68
69 #include "c.h"
70 #include "closestream.h"
71 #include "colors.h"
72 #include "nls.h"
73 #include "mbsalign.h"
74 #include "strutils.h"
75
76 #if defined(HAVE_LIBNCURSES) || defined(HAVE_LIBNCURSESW)
77 # ifdef HAVE_NCURSES_H
78 # include <ncurses.h>
79 # elif defined(HAVE_NCURSES_NCURSES_H)
80 # include <ncurses/ncurses.h>
81 # endif
82 # include <term.h>
83
84 static void my_setupterm(const char *term, int fildes, int *errret)
85 {
86 setupterm((char *)term, fildes, errret);
87 }
88
89 static void my_putstring(char *s)
90 {
91 putp(s);
92 }
93
94 static const char *my_tgetstr(char *s __attribute__((__unused__)), char *ss)
95 {
96 const char *ret = tigetstr(ss);
97 if (!ret || ret == (char *)-1)
98 return "";
99
100 return ret;
101 }
102
103 #elif defined(HAVE_LIBTERMCAP)
104 # include <termcap.h>
105
106 static char termbuffer[4096];
107 static char tcbuffer[4096];
108 static char *strbuf = termbuffer;
109
110 static void my_setupterm(const char *term, int fildes __attribute__((__unused__)), int *errret)
111 {
112 *errret = tgetent(tcbuffer, term);
113 }
114
115 static void my_putstring(char *s)
116 {
117 tputs(s, 1, putchar);
118 }
119
120 static const char *my_tgetstr(char *s, char *ss __attribute__((__unused__)))
121 {
122 const char *ret = tgetstr(s, &strbuf);
123 if (!ret)
124 return "";
125
126 return ret;
127 }
128
129 #else /* ! (HAVE_LIBTERMCAP || HAVE_LIBNCURSES || HAVE_LIBNCURSESW) */
130
131 static void my_putstring(char *s)
132 {
133 fputs(s, stdout);
134 }
135
136 #endif /* end of LIBTERMCAP / NCURSES */
137
138 #if defined(HAVE_LIBNCURSES) || defined(HAVE_LIBNCURSESW) || defined(HAVE_LIBTERMCAP)
139 static const char *term="";
140 #endif
141
142 static const char *Senter="", *Sexit="";/* enter and exit standout mode */
143
144 #include "widechar.h"
145
146 /* allow compile-time define to over-ride default */
147 #ifndef NUM_MONTHS
148 # define NUM_MONTHS 1
149 #endif
150
151 #if ( NUM_MONTHS != 1 && NUM_MONTHS !=3 )
152 # error NUM_MONTHS must be 1 or 3
153 #endif
154
155 enum {
156 SUNDAY = 0,
157 MONDAY,
158 TUESDAY,
159 WEDNESDAY,
160 THURSDAY,
161 FRIDAY,
162 SATURDAY,
163 DAYS_IN_WEEK,
164 NONEDAY
165 };
166
167 #define FIRST_WEEKDAY SATURDAY /* Jan 1st, 1 was a Saturday */
168 #define REFORMATION_YEAR 1752 /* Signed-off-by: Lord Chesterfield */
169 #define REFORMATION_MONTH 9 /* September */
170 #define FIRST_MISSING_DAY 639799 /* 3 Sep 1752 */
171 #define NUMBER_MISSING_DAYS 11 /* 11 day correction */
172 #define YDAY_AFTER_MISSING 258 /* 14th in Sep 1752 */
173
174 #define DAYS_IN_YEAR 365 /* the common case, leap years are calculated */
175 #define MONTHS_IN_YEAR 12
176 #define DAYS_IN_MONTH 31
177 #define MAXDAYS 42 /* slots in a month array */
178 #define SPACE -1 /* used in day array */
179
180 #define SMALLEST_YEAR 1
181
182 #define DAY_LEN 3 /* 3 spaces per day */
183 #define WEEK_LEN (DAYS_IN_WEEK * DAY_LEN)
184 #define HEAD_SEP 2
185 #define MONTH_COLS 3 /* month columns in year view */
186 #define WNUM_LEN 3
187
188 #define TODAY_FLAG 0x400 /* flag day for highlighting */
189
190 #define FMT_ST_CHARS 300 /* 90 suffices in most locales */
191
192 static const int days_in_month[2][13] = {
193 {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31},
194 {0, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31},
195 };
196
197 enum {
198 WEEK_NUM_DISABLED = 0,
199 WEEK_NUM_MASK=0xff,
200 WEEK_NUM_ISO=0x100,
201 WEEK_NUM_US=0x200,
202 };
203
204 /* utf-8 can have up to 6 bytes per char; and an extra byte for ending \0 */
205 static char day_headings[(WEEK_LEN + 1) * 6 + 1];
206
207 struct cal_request {
208 int day;
209 int month;
210 int32_t year;
211 int week;
212 };
213
214 struct cal_control {
215 const char *full_month[MONTHS_IN_YEAR]; /* month names */
216 int colormode; /* day and week number highlight */
217 int num_months; /* number of months horizontally in print out */
218 int weekstart; /* day the week starts, often Sun or Mon */
219 int weektype; /* WEEK_TYPE_{NONE,ISO,US} */
220 size_t day_width; /* day width in characters in printout */
221 size_t week_width; /* 7 * day_width + possible week num */
222 int gutter_width; /* spaces in between horizontal month outputs */
223 struct cal_request req; /* the times user is interested */
224 unsigned int julian:1, /* julian output */
225 yflag:1, /* print whole year */
226 header_hint:1; /* does month name + year need two lines to fit */
227 };
228
229 struct cal_month {
230 int days[MAXDAYS]; /* the day numbers, or SPACE */
231 int weeks[MAXDAYS / DAYS_IN_WEEK];
232 int month;
233 int32_t year;
234 struct cal_month *next;
235 };
236
237 /* function prototypes */
238 static int leap_year(int32_t year);
239 static void headers_init(struct cal_control *ctl);
240 static void set_consecutive_months(struct cal_month *month, int m, int32_t y);
241 static void cal_fill_month(struct cal_month *month, const struct cal_control *ctl);
242 static void cal_output_header(struct cal_month *month, const struct cal_control *ctl);
243 static void cal_output_months(struct cal_month *month, const struct cal_control *ctl);
244 static void monthly(const struct cal_control *ctl);
245 static void monthly3(const struct cal_control *ctl);
246 static void yearly(const struct cal_control *ctl);
247 static int day_in_year(int day, int month, int32_t year);
248 static int day_in_week(int day, int month, int32_t year);
249 static int week_number(int day, int month, int32_t year, const struct cal_control *ctl);
250 static int week_to_day(const struct cal_control *ctl);
251 static int center_str(const char *src, char *dest, size_t dest_size, size_t width);
252 static void center(const char *str, size_t len, int separate);
253 static void __attribute__((__noreturn__)) usage(FILE *out);
254
255 int main(int argc, char **argv)
256 {
257 struct tm *local_time;
258 time_t now;
259 int ch;
260 static struct cal_control ctl = {
261 .weekstart = SUNDAY,
262 .num_months = NUM_MONTHS,
263 .colormode = UL_COLORMODE_UNDEF,
264 .weektype = WEEK_NUM_DISABLED,
265 .day_width = DAY_LEN,
266 .gutter_width = 2,
267 .req.day = 0,
268 .req.month = 0
269 };
270
271 enum {
272 OPT_COLOR = CHAR_MAX + 1
273 };
274
275 static const struct option longopts[] = {
276 {"one", no_argument, NULL, '1'},
277 {"three", no_argument, NULL, '3'},
278 {"sunday", no_argument, NULL, 's'},
279 {"monday", no_argument, NULL, 'm'},
280 {"julian", no_argument, NULL, 'j'},
281 {"year", no_argument, NULL, 'y'},
282 {"week", optional_argument, NULL, 'w'},
283 {"color", optional_argument, NULL, OPT_COLOR},
284 {"version", no_argument, NULL, 'V'},
285 {"help", no_argument, NULL, 'h'},
286 {NULL, 0, NULL, 0}
287 };
288
289 setlocale(LC_ALL, "");
290 bindtextdomain(PACKAGE, LOCALEDIR);
291 textdomain(PACKAGE);
292 atexit(close_stdout);
293
294 #if defined(HAVE_LIBNCURSES) || defined(HAVE_LIBNCURSESW) || defined(HAVE_LIBTERMCAP)
295 if ((term = getenv("TERM"))) {
296 int ret;
297 my_setupterm(term, STDOUT_FILENO, &ret);
298 if (ret > 0) {
299 Senter = my_tgetstr("so","smso");
300 Sexit = my_tgetstr("se","rmso");
301 }
302 }
303 #endif
304
305 /*
306 * The traditional Unix cal utility starts the week at Sunday,
307 * while ISO 8601 starts at Monday. We read the start day from
308 * the locale database, which can be overridden with the
309 * -s (Sunday) or -m (Monday) options.
310 */
311 #if HAVE_DECL__NL_TIME_WEEK_1STDAY
312 /*
313 * You need to use 2 locale variables to get the first day of the week.
314 * This is needed to support first_weekday=2 and first_workday=1 for
315 * the rare case where working days span across 2 weeks.
316 * This shell script shows the combinations and calculations involved:
317 *
318 * for LANG in en_US ru_RU fr_FR csb_PL POSIX; do
319 * printf "%s:\t%s + %s -1 = " $LANG $(locale week-1stday first_weekday)
320 * date -d"$(locale week-1stday) +$(($(locale first_weekday)-1))day" +%w
321 * done
322 *
323 * en_US: 19971130 + 1 -1 = 0 #0 = sunday
324 * ru_RU: 19971130 + 2 -1 = 1
325 * fr_FR: 19971201 + 1 -1 = 1
326 * csb_PL: 19971201 + 2 -1 = 2
327 * POSIX: 19971201 + 7 -1 = 0
328 */
329 {
330 int wfd;
331 union { unsigned int word; char *string; } val;
332 val.string = nl_langinfo(_NL_TIME_WEEK_1STDAY);
333
334 wfd = val.word;
335 wfd = day_in_week(wfd % 100, (wfd / 100) % 100, wfd / (100 * 100));
336 ctl.weekstart = (wfd + *nl_langinfo(_NL_TIME_FIRST_WEEKDAY) - 1) % DAYS_IN_WEEK;
337 }
338 #endif
339
340 while ((ch = getopt_long(argc, argv, "13mjsywVh", longopts, NULL)) != -1)
341 switch(ch) {
342 case '1':
343 ctl.num_months = 1; /* default */
344 break;
345 case '3':
346 ctl.num_months = 3;
347 break;
348 case 's':
349 ctl.weekstart = SUNDAY; /* default */
350 break;
351 case 'm':
352 ctl.weekstart = MONDAY;
353 break;
354 case 'j':
355 ctl.julian = 1;
356 ctl.day_width = DAY_LEN + 1;
357 break;
358 case 'y':
359 ctl.yflag = 1;
360 break;
361 case 'w':
362 if (optarg) {
363 ctl.req.week = strtos32_or_err(optarg,
364 _("invalid week argument"));
365 if (ctl.req.week < 1 || 53 < ctl.req.week)
366 errx(EXIT_FAILURE,_("illegal week value: use 1-53"));
367 }
368 ctl.weektype = WEEK_NUM_US; /* default per weekstart */
369 break;
370 case OPT_COLOR:
371 if (optarg)
372 ctl.colormode = colormode_or_err(optarg,
373 _("unsupported color mode"));
374 break;
375 case 'V':
376 printf(UTIL_LINUX_VERSION);
377 return EXIT_SUCCESS;
378 case 'h':
379 usage(stdout);
380 case '?':
381 default:
382 usage(stderr);
383 }
384 argc -= optind;
385 argv += optind;
386
387 if (ctl.weektype) {
388 ctl.weektype = ctl.req.week & WEEK_NUM_MASK;
389 ctl.weektype |= (ctl.weekstart == MONDAY ? WEEK_NUM_ISO : WEEK_NUM_US);
390 ctl.week_width = (ctl.day_width * DAYS_IN_WEEK) + WNUM_LEN;
391 } else
392 ctl.week_width = ctl.day_width * DAYS_IN_WEEK;
393
394 time(&now);
395 local_time = localtime(&now);
396
397 switch(argc) {
398 case 3:
399 ctl.req.day = strtos32_or_err(*argv++, _("illegal day value"));
400 if (ctl.req.day < 1 || DAYS_IN_MONTH < ctl.req.day)
401 errx(EXIT_FAILURE, _("illegal day value: use 1-%d"), DAYS_IN_MONTH);
402 /* FALLTHROUGH */
403 case 2:
404 ctl.req.month = strtos32_or_err(*argv++, _("illegal month value: use 1-12"));
405 if (ctl.req.month < 1 || MONTHS_IN_YEAR < ctl.req.month)
406 errx(EXIT_FAILURE, _("illegal month value: use 1-12"));
407 /* FALLTHROUGH */
408 case 1:
409 ctl.req.year = strtos32_or_err(*argv++, _("illegal year value"));
410 if (ctl.req.year < SMALLEST_YEAR)
411 errx(EXIT_FAILURE, _("illegal year value: use positive integer"));
412 if (ctl.req.year == INT32_MAX)
413 errx(EXIT_FAILURE, _("illegal year value"));
414 if (ctl.req.day) {
415 int dm = days_in_month[leap_year(ctl.req.year)][ctl.req.month];
416 if (ctl.req.day > dm)
417 errx(EXIT_FAILURE, _("illegal day value: use 1-%d"), dm);
418 ctl.req.day = day_in_year(ctl.req.day, ctl.req.month, ctl.req.year);
419 } else if ((int32_t) (local_time->tm_year + 1900) == ctl.req.year) {
420 ctl.req.day = local_time->tm_yday + 1;
421 }
422 if (!ctl.req.month && !ctl.req.week) {
423 ctl.req.month = local_time->tm_mon + 1;
424 ctl.yflag = 1;
425 }
426 break;
427 case 0:
428 ctl.req.day = local_time->tm_yday + 1;
429 ctl.req.year = local_time->tm_year + 1900;
430 ctl.req.month = local_time->tm_mon + 1;
431 break;
432 default:
433 usage(stderr);
434 }
435
436 if (0 < ctl.req.week) {
437 int yday = week_to_day(&ctl);
438 int leap = leap_year(ctl.req.year);
439 int m = 1;
440
441 if (yday < 1)
442 errx(EXIT_FAILURE, _("illegal week value: year %d "
443 "doesn't have week %d"),
444 ctl.req.year, ctl.req.week);
445 while (m <= 12 && yday > days_in_month[leap][m])
446 yday -= days_in_month[leap][m++];
447 if (m > 12) {
448 /* In some years (e.g. 2010 in ISO mode) it's possible
449 * to have a remnant of week 53 starting the year yet
450 * the year in question ends during 52, in this case
451 * we're assuming that early remnant is being referred
452 * to if 53 is given as argument. */
453 if (ctl.req.week != week_number(31, 12, ctl.req.year - 1, &ctl))
454 errx(EXIT_FAILURE,
455 _("illegal week value: year %d "
456 "doesn't have week %d"),
457 ctl.req.year, ctl.req.week);
458 }
459 if (!ctl.req.month)
460 ctl.req.month = 12 < m ? 1 : m;
461 }
462
463 headers_init(&ctl);
464
465 if (!colors_init(ctl.colormode, "cal")) {
466 ctl.req.day = 0;
467 ctl.weektype &= ~WEEK_NUM_MASK;
468 }
469
470 if (ctl.yflag) {
471 if (ctl.julian)
472 ctl.num_months = MONTH_COLS - 1;
473 else
474 ctl.num_months = MONTH_COLS;
475 ctl.gutter_width = 3;
476 yearly(&ctl);
477 } else if (ctl.num_months == 1)
478 monthly(&ctl);
479 else if (ctl.num_months == 3)
480 monthly3(&ctl);
481
482 return EXIT_SUCCESS;
483 }
484
485 /* leap year -- account for gregorian reformation in 1752 */
486 static int leap_year(int32_t year)
487 {
488 if (year <= REFORMATION_YEAR)
489 return !(year % 4);
490 else
491 return ( !(year % 4) && (year % 100) ) || !(year % 400);
492 }
493
494 static void headers_init(struct cal_control *ctl)
495 {
496 size_t i, wd;
497 char *cur_dh = day_headings;
498 char tmp[FMT_ST_CHARS];
499 size_t year_len;
500
501 year_len = snprintf(tmp, sizeof(tmp), "%d", ctl->req.year);
502
503 for (i = 0; i < DAYS_IN_WEEK; i++) {
504 size_t space_left;
505 wd = (i + ctl->weekstart) % DAYS_IN_WEEK;
506
507 if (i)
508 strcat(cur_dh++, " ");
509 space_left = sizeof(day_headings) - (cur_dh - day_headings);
510
511 if (space_left <= (ctl->day_width - 1))
512 break;
513 cur_dh += center_str(nl_langinfo(ABDAY_1 + wd), cur_dh,
514 space_left, ctl->day_width - 1);
515 }
516
517 for (i = 0; i < MONTHS_IN_YEAR; i++) {
518 ctl->full_month[i] = nl_langinfo(MON_1 + i);
519 /* The +1 after year_len is space in between month and year. */
520 if (ctl->week_width < strlen(ctl->full_month[i]) + year_len + 1)
521 ctl->header_hint = 1;
522 }
523 }
524
525 static void set_consecutive_months(struct cal_month *month, int m, int32_t y)
526 {
527 struct cal_month *i;
528 for (i = month; i; i = i->next) {
529 i->month = m++;
530 i->year = y;
531 if (MONTHS_IN_YEAR < m) {
532 m = 1;
533 y++;
534 }
535 }
536 }
537
538 static void cal_fill_month(struct cal_month *month, const struct cal_control *ctl)
539 {
540 int first_week_day = day_in_week(1, month->month, month->year);
541 int month_days;
542 int i, j, weeklines = 0;
543
544 if (ctl->julian)
545 j = day_in_year(1, month->month, month->year);
546 else
547 j = 1;
548 month_days = j + days_in_month[leap_year(month->year)][month->month];
549
550 /* True when Sunday is not first day in the output week. */
551 if (ctl->weekstart) {
552 first_week_day -= ctl->weekstart;
553 if (first_week_day < 0)
554 first_week_day = DAYS_IN_WEEK - ctl->weekstart;
555 month_days += ctl->weekstart - 1;
556 }
557
558 /* Fill day array. */
559 for (i = 0; i < MAXDAYS; i++) {
560 if (0 < first_week_day) {
561 month->days[i] = SPACE;
562 first_week_day--;
563 continue;
564 }
565 if (j < month_days) {
566 if (month->year == 1752 && month->month == 9 && (j == 3 || j == 247))
567 j += NUMBER_MISSING_DAYS;
568 month->days[i] = j;
569 j++;
570 continue;
571 }
572 month->days[i] = SPACE;
573 weeklines++;
574 }
575
576 /* Add week numbers */
577 if (ctl->weektype) {
578 int weeknum = week_number(1, month->month, month->year, ctl);
579 weeklines = MAXDAYS / DAYS_IN_WEEK - weeklines / DAYS_IN_WEEK;
580 for (i = 0; i < MAXDAYS / DAYS_IN_WEEK; i++) {
581 if (0 < weeklines)
582 month->weeks[i] = weeknum++;
583 else
584 month->weeks[i] = SPACE;
585 weeklines--;
586 if (52 < weeknum && i == 0)
587 weeknum = week_number(month->days[DAYS_IN_WEEK * (i + 1)], 1, month->year, ctl);
588 else if (52 < weeknum)
589 weeknum = week_number(31, 12, month->year, ctl);
590 }
591 }
592 }
593
594 static void cal_output_header(struct cal_month *month, const struct cal_control *ctl)
595 {
596 char out[FMT_ST_CHARS];
597 struct cal_month *i;
598
599 if (ctl->header_hint || ctl->yflag) {
600 for (i = month; i; i = i->next) {
601 sprintf(out, _("%s"), ctl->full_month[i->month - 1]);
602 center(out, ctl->week_width - 1, i->next == NULL ? 0 : ctl->gutter_width);
603 }
604 if (!ctl->yflag) {
605 my_putstring("\n");
606 for (i = month; i; i = i->next) {
607 sprintf(out, _("%d"), i->year);
608 center(out, ctl->week_width - 1, i->next == NULL ? 0 : ctl->gutter_width);
609 }
610 }
611 } else {
612 for (i = month; i; i = i->next) {
613 sprintf(out, _("%s %d"), ctl->full_month[i->month - 1], i->year);
614 center(out, ctl->week_width - 1, i->next == NULL ? 0 : ctl->gutter_width);
615 }
616 }
617 my_putstring("\n");
618 for (i = month; i; i = i->next) {
619 if (ctl->weektype) {
620 if (ctl->julian)
621 sprintf(out, "%*s%s", (int)ctl->day_width - 1, "", day_headings);
622 else
623 sprintf(out, "%*s%s", (int)ctl->day_width, "", day_headings);
624 my_putstring(out);
625 } else
626 my_putstring(day_headings);
627 if (i->next != NULL) {
628 sprintf(out, "%*s", ctl->gutter_width, "");
629 my_putstring(out);
630 }
631 }
632 my_putstring("\n");
633 }
634
635 static void cal_output_months(struct cal_month *month, const struct cal_control *ctl)
636 {
637 char out[FMT_ST_CHARS];
638 int reqday, week_line, d;
639 int skip;
640 struct cal_month *i;
641
642 for (week_line = 0; week_line < MAXDAYS / DAYS_IN_WEEK; week_line++) {
643 for (i = month; i; i = i->next) {
644 /* Determine the day that should be highlighted. */
645 reqday = 0;
646 if (i->month == ctl->req.month && i->year == ctl->req.year) {
647 if (ctl->julian)
648 reqday = ctl->req.day;
649 else
650 reqday =
651 ctl->req.day + 1 - day_in_year(1, i->month,
652 i->year);
653 }
654
655 if (ctl->weektype) {
656 if (0 < i->weeks[week_line]) {
657 if ((ctl->weektype & WEEK_NUM_MASK) ==
658 i->weeks[week_line])
659 sprintf(out, "%s%2d%s", Senter, i->weeks[week_line],
660 Sexit);
661 else
662 sprintf(out, "%2d", i->weeks[week_line]);
663 } else
664 sprintf(out, "%2s", "");
665 my_putstring(out);
666 skip = ctl->day_width;
667 } else
668 /* First day of the week is one char narrower than the other days,
669 * unless week number is printed. */
670 skip = ctl->day_width - 1;
671
672 for (d = DAYS_IN_WEEK * week_line;
673 d < DAYS_IN_WEEK * week_line + DAYS_IN_WEEK; d++) {
674 if (0 < i->days[d]) {
675 if (reqday == i->days[d])
676 sprintf(out, "%*s%s%*d%s", skip - (ctl->julian ? 3 : 2),
677 "", Senter, (ctl->julian ? 3 : 2),
678 i->days[d], Sexit);
679 else
680 sprintf(out, "%*d", skip, i->days[d]);
681 } else
682 sprintf(out, "%*s", skip, "");
683 my_putstring(out);
684 if (skip < (int)ctl->day_width)
685 skip++;
686 }
687 if (i->next != NULL) {
688 sprintf(out, "%*s", ctl->gutter_width, "");
689 my_putstring(out);
690 }
691 }
692 if (i == NULL) {
693 sprintf(out, "%*s\n", ctl->gutter_width - (ctl->yflag ? 0 : 1), "");
694 my_putstring(out);
695 }
696 }
697 }
698
699 static void monthly(const struct cal_control *ctl)
700 {
701 struct cal_month month;
702
703 month.month = ctl->req.month;
704 month.year = ctl->req.year;
705 month.next = NULL;
706
707 cal_fill_month(&month, ctl);
708
709 cal_output_header(&month, ctl);
710 cal_output_months(&month, ctl);
711 }
712
713 static void monthly3(const struct cal_control *ctl)
714 {
715 struct cal_month m1, m2, m3, *i;
716 int first_month;
717 int32_t first_year;
718
719 m1.next = &m2;
720 m2.next = &m3;
721 m3.next = NULL;
722
723 if (ctl->req.month == 1) {
724 first_month = MONTHS_IN_YEAR;
725 first_year = ctl->req.year - 1;
726 } else {
727 first_month = ctl->req.month - 1;
728 first_year = ctl->req.year;
729 }
730
731 set_consecutive_months(&m1, first_month, first_year);
732 for (i = &m1; i; i = i->next)
733 cal_fill_month(i, ctl);
734 cal_output_header(&m1, ctl);
735 cal_output_months(&m1, ctl);
736 }
737
738 static void yearly(const struct cal_control *ctl)
739 {
740 struct cal_month m1, m2, m3, *i;
741 int month;
742 char out[FMT_ST_CHARS];
743 int year_width = 0;
744
745 m1.next = &m2;
746 if (ctl->julian)
747 m2.next = NULL;
748 else {
749 m2.next = &m3;
750 m3.next = NULL;
751 }
752
753 /* year header */
754 for (i = &m1; i; i = i->next)
755 year_width += ctl->week_width + 1;
756 if (ctl->julian)
757 year_width--;
758 sprintf(out, "%d", ctl->req.year);
759 center(out, year_width, 0);
760 my_putstring("\n\n");
761
762 for (month = 1; month < MONTHS_IN_YEAR; month += ctl->julian ? 2 : 3) {
763 set_consecutive_months(&m1, month, ctl->req.year);
764 for (i = &m1; i; i = i->next)
765 cal_fill_month(i, ctl);
766 cal_output_header(&m1, ctl);
767 cal_output_months(&m1, ctl);
768 }
769 /* Is empty line at the end year output really needed? */
770 my_putstring("\n");
771 }
772
773 /*
774 * day_in_year --
775 * return the 1 based day number within the year
776 */
777 static int day_in_year(int day, int month, int32_t year)
778 {
779 int i, leap;
780
781 leap = leap_year(year);
782 for (i = 1; i < month; i++)
783 day += days_in_month[leap][i];
784 return day;
785 }
786
787 /*
788 * day_in_week
789 * return the 0 based day number for any date from 1 Jan. 1 to
790 * 31 Dec. 9999. Assumes the Gregorian reformation eliminates
791 * 3 Sep. 1752 through 13 Sep. 1752, and returns invalid weekday
792 * during the period of 11 days.
793 */
794 static int day_in_week(int day, int month, int32_t year)
795 {
796 static const int reform[] = {
797 SUNDAY, WEDNESDAY, TUESDAY, FRIDAY, SUNDAY, WEDNESDAY,
798 FRIDAY, MONDAY, THURSDAY, SATURDAY, TUESDAY, THURSDAY
799 };
800 static const int old[] = {
801 FRIDAY, MONDAY, SUNDAY, WEDNESDAY, FRIDAY, MONDAY,
802 WEDNESDAY, SATURDAY, TUESDAY, THURSDAY, SUNDAY, TUESDAY
803 };
804 if (year != 1753)
805 year -= month < 3;
806 else
807 year -= (month < 3) + 14;
808 if (REFORMATION_YEAR < year
809 || (year == REFORMATION_YEAR && 9 < month)
810 || (year == REFORMATION_YEAR && month == 9 && 13 < day)) {
811 long long_year = year;
812 return (long_year + (year / 4) - (year / 100) + (year / 400) + reform[month - 1] +
813 day) % 7;
814 }
815 if (year < REFORMATION_YEAR
816 || (year == REFORMATION_YEAR && month < 9)
817 || (year == REFORMATION_YEAR && month == 9 && day < 3))
818 return (year + year / 4 + old[month - 1] + day) % 7;
819 return NONEDAY;
820 }
821
822 /*
823 * week_number
824 * return the week number of a given date, 1..53.
825 * Supports ISO-8601 and North American modes.
826 * Day may be given as Julian day of the year mode, in which
827 * case the month is disregarded entirely.
828 */
829 static int week_number(int day, int month, int32_t year, const struct cal_control *ctl)
830 {
831 int fday = 0, yday;
832 int wday = day_in_week(1, 1, year);
833
834 if (ctl->weektype & WEEK_NUM_ISO)
835 fday = wday + (wday >= FRIDAY ? -2 : 5);
836 else
837 /* WEEK_NUM_US
838 * - according to gcal, the first Sun is in the first week
839 * - according to wikipedia, the first Sat is in the first week
840 */
841 fday = wday + (wday == SUNDAY ? 6 : -1);
842
843 /* For julian dates the month can be set to 1, the global julian
844 * variable cannot be relied upon here, because we may recurse
845 * internally for 31.12. which would not work. */
846 if (day > 31)
847 month = 1;
848
849 yday = day_in_year(day,month,year);
850 if (year == REFORMATION_YEAR) {
851 if (yday >= YDAY_AFTER_MISSING)
852 fday -= NUMBER_MISSING_DAYS;
853 }
854
855 /* Last year is last year */
856 if (yday + fday < 7)
857 return week_number(31, 12, year - 1, ctl);
858
859 /* Or it could be part of the next year. The reformation year had less
860 * days than 365 making this check invalid, but reformation year ended
861 * on Sunday and in week 51, so it's ok here. */
862 if (ctl->weektype == WEEK_NUM_ISO && yday >= 363
863 && day_in_week(day, month, year) >= MONDAY
864 && day_in_week(day, month, year) <= WEDNESDAY
865 && day_in_week(31, 12, year) >= MONDAY
866 && day_in_week(31, 12, year) <= WEDNESDAY)
867 return week_number(1, 1, year + 1, ctl);
868
869 return (yday + fday) / 7;
870 }
871
872 /*
873 * week_to_day
874 * return the yday of the first day in a given week inside
875 * the given year. This may be something other than Monday
876 * for ISO-8601 modes. For North American numbering this
877 * always returns a Sunday.
878 */
879 static int week_to_day(const struct cal_control *ctl)
880 {
881 int yday, wday;
882
883 wday = day_in_week(1, 1, ctl->req.year);
884 yday = ctl->req.week * 7 - wday;
885
886 if (ctl->weektype & WEEK_NUM_ISO)
887 yday -= (wday >= FRIDAY ? -2 : 5);
888 else
889 yday -= (wday == SUNDAY ? 6 : -1); /* WEEK_NUM_US */
890 if (yday <= 0)
891 return 1;
892
893 return yday;
894 }
895
896 /*
897 * Center string, handling multibyte characters appropriately.
898 * In addition if the string is too large for the width it's truncated.
899 * The number of trailing spaces may be 1 less than the number of leading spaces.
900 */
901 static int center_str(const char* src, char* dest,
902 size_t dest_size, size_t width)
903 {
904 return mbsalign(src, dest, dest_size, &width,
905 MBS_ALIGN_CENTER, MBA_UNIBYTE_FALLBACK);
906 }
907
908 static void center(const char *str, size_t len, int separate)
909 {
910 char lineout[FMT_ST_CHARS];
911
912 center_str(str, lineout, ARRAY_SIZE(lineout), len);
913 my_putstring(lineout);
914
915 if (separate) {
916 snprintf(lineout, sizeof(lineout), "%*s", separate, "");
917 my_putstring(lineout);
918 }
919 }
920
921 static void __attribute__ ((__noreturn__)) usage(FILE * out)
922 {
923 fputs(USAGE_HEADER, out);
924 fprintf(out, _(" %s [options] [[[day] month] year]\n"), program_invocation_short_name);
925
926 fputs(USAGE_SEPARATOR, out);
927 fputs(_("Display a calendar, or some part of it.\n"), out);
928 fputs(_("Without any arguments, display the current month.\n"), out);
929
930 fputs(USAGE_OPTIONS, out);
931 fputs(_(" -1, --one show only a single month (default)\n"), out);
932 fputs(_(" -3, --three show three months spanning the date\n"), out);
933 fputs(_(" -s, --sunday Sunday as first day of week\n"), out);
934 fputs(_(" -m, --monday Monday as first day of week\n"), out);
935 fputs(_(" -j, --julian output Julian dates\n"), out);
936 fputs(_(" -y, --year show the whole year\n"), out);
937 fputs(_(" -w, --week[=<num>] show US or ISO-8601 week numbers\n"), out);
938 fputs(_(" --color[=<when>] colorize messages (auto, always or never)\n"), out);
939
940 fputs(USAGE_SEPARATOR, out);
941 fputs(USAGE_HELP, out);
942 fputs(USAGE_VERSION, out);
943 fprintf(out, USAGE_MAN_TAIL("cal(1)"));
944
945 exit(out == stderr ? EXIT_FAILURE : EXIT_SUCCESS);
946 }