]> git.ipfire.org Git - thirdparty/util-linux.git/blame - misc-utils/cal.c
lib/mbalign: fix unsigned integer overflow [AddressSanitizer]
[thirdparty/util-linux.git] / misc-utils / cal.c
CommitLineData
6dbe3af9
KZ
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
5c36a0eb 37/* 1999-02-01 Jean-Francois Bignolles: added option '-m' to display
d162fcb5 38 * monday as the first day of the week.
b50945d4 39 * 1999-02-22 Arkadiusz Miśkiewicz <misiek@pld.ORG.PL>
7eda085c
KZ
40 * - added Native Language Support
41 *
d162fcb5 42 * 2000-09-01 Michael Charles Pruznick <dummy@netwiz.net>
66ee8158
KZ
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.
e8f26419
KZ
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
5c36a0eb
KZ
56 */
57
6dbe3af9
KZ
58#include <sys/types.h>
59
60#include <ctype.h>
022df321 61#include <getopt.h>
6dbe3af9
KZ
62#include <stdio.h>
63#include <stdlib.h>
64#include <string.h>
65#include <time.h>
66#include <unistd.h>
37d6897f 67#include <errno.h>
17282538
KZ
68
69#include "c.h"
c05a80ca 70#include "closestream.h"
7b353df2 71#include "colors.h"
7eda085c 72#include "nls.h"
104b92f8 73#include "mbsalign.h"
022df321 74#include "strutils.h"
726f69e2 75
310170b8 76#if defined(HAVE_LIBNCURSES) || defined(HAVE_LIBNCURSESW)
4aefe5e8
SK
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
84static void my_setupterm(const char *term, int fildes, int *errret)
85{
86 setupterm((char *)term, fildes, errret);
d162fcb5
KZ
87}
88
4aefe5e8
SK
89static void my_putstring(char *s)
90{
91 putp(s);
d162fcb5
KZ
92}
93
4aefe5e8
SK
94static 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 "";
d4be073d
KZ
99
100 return ret;
d162fcb5
KZ
101}
102
48d7b13a 103#elif defined(HAVE_LIBTERMCAP)
4aefe5e8 104# include <termcap.h>
d162fcb5 105
d4be073d
KZ
106static char termbuffer[4096];
107static char tcbuffer[4096];
108static char *strbuf = termbuffer;
d162fcb5 109
7bd3336f 110static void my_setupterm(const char *term, int fildes __attribute__((__unused__)), int *errret)
4aefe5e8
SK
111{
112 *errret = tgetent(tcbuffer, term);
d162fcb5
KZ
113}
114
4aefe5e8
SK
115static void my_putstring(char *s)
116{
117 tputs(s, 1, putchar);
d162fcb5
KZ
118}
119
4aefe5e8
SK
120static const char *my_tgetstr(char *s, char *ss __attribute__((__unused__)))
121{
122 const char *ret = tgetstr(s, &strbuf);
123 if (!ret)
124 return "";
d4be073d
KZ
125
126 return ret;
d162fcb5
KZ
127}
128
4aefe5e8 129#else /* ! (HAVE_LIBTERMCAP || HAVE_LIBNCURSES || HAVE_LIBNCURSESW) */
f4061938 130
4aefe5e8
SK
131static void my_putstring(char *s)
132{
133 fputs(s, stdout);
f4061938
KZ
134}
135
4aefe5e8 136#endif /* end of LIBTERMCAP / NCURSES */
f4061938 137
a814f0bb 138#if defined(HAVE_LIBNCURSES) || defined(HAVE_LIBNCURSESW) || defined(HAVE_LIBTERMCAP)
d4be073d 139static const char *term="";
a814f0bb
KZ
140#endif
141
142static const char *Senter="", *Sexit="";/* enter and exit standout mode */
d162fcb5 143
e8f26419 144#include "widechar.h"
95f1bdee 145
66ee8158
KZ
146/* allow compile-time define to over-ride default */
147#ifndef NUM_MONTHS
d4be073d 148# define NUM_MONTHS 1
66ee8158
KZ
149#endif
150
151#if ( NUM_MONTHS != 1 && NUM_MONTHS !=3 )
d4be073d 152# error NUM_MONTHS must be 1 or 3
66ee8158
KZ
153#endif
154
5f845cb7
SK
155enum {
156 SUNDAY = 0,
157 MONDAY,
158 TUESDAY,
159 WEDNESDAY,
160 THURSDAY,
161 FRIDAY,
162 SATURDAY,
d68f076f
SK
163 DAYS_IN_WEEK,
164 NONEDAY
5f845cb7 165};
6dbe3af9 166
5f845cb7 167#define FIRST_WEEKDAY SATURDAY /* Jan 1st, 1 was a Saturday */
5f845cb7
SK
168#define REFORMATION_YEAR 1752 /* Signed-off-by: Lord Chesterfield */
169#define REFORMATION_MONTH 9 /* September */
d162fcb5
KZ
170#define FIRST_MISSING_DAY 639799 /* 3 Sep 1752 */
171#define NUMBER_MISSING_DAYS 11 /* 11 day correction */
c36c4a4e 172#define YDAY_AFTER_MISSING 258 /* 14th in Sep 1752 */
6dbe3af9 173
5f845cb7
SK
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
f06602a4 177#define MAXDAYS 42 /* slots in a month array */
6dbe3af9
KZ
178#define SPACE -1 /* used in day array */
179
5f845cb7 180#define SMALLEST_YEAR 1
5f845cb7
SK
181
182#define DAY_LEN 3 /* 3 spaces per day */
183#define WEEK_LEN (DAYS_IN_WEEK * DAY_LEN)
184#define HEAD_SEP 2
b283141c 185#define MONTH_COLS 3 /* month columns in year view */
aae4f87e 186#define WNUM_LEN 3
5f845cb7 187
5f845cb7
SK
188#define TODAY_FLAG 0x400 /* flag day for highlighting */
189
5f845cb7 190#define FMT_ST_CHARS 300 /* 90 suffices in most locales */
5f845cb7 191
1beb933e 192static const int days_in_month[2][13] = {
6dbe3af9
KZ
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
c36c4a4e
TK
197enum {
198 WEEK_NUM_DISABLED = 0,
aae4f87e
TK
199 WEEK_NUM_MASK=0xff,
200 WEEK_NUM_ISO=0x100,
201 WEEK_NUM_US=0x200,
c36c4a4e 202};
e8f26419
KZ
203
204/* utf-8 can have up to 6 bytes per char; and an extra byte for ending \0 */
71ff238e 205static char day_headings[(WEEK_LEN + 1) * 6 + 1];
6dbe3af9 206
ffc56357
SK
207struct cal_request {
208 int day;
209 int month;
4a7424a5 210 int32_t year;
ffc56357
SK
211 int week;
212};
213
b549058b
SK
214struct 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} */
71ff238e
SK
220 size_t day_width; /* day width in characters in printout */
221 size_t week_width; /* 7 * day_width + possible week num */
95f4adde 222 int gutter_width; /* spaces in between horizontal month outputs */
ffc56357 223 struct cal_request req; /* the times user is interested */
b549058b 224 unsigned int julian:1, /* julian output */
efce94ec
SK
225 yflag:1, /* print whole year */
226 header_hint:1; /* does month name + year need two lines to fit */
b549058b 227};
6dbe3af9 228
852c8d21
SK
229struct cal_month {
230 int days[MAXDAYS]; /* the day numbers, or SPACE */
231 int weeks[MAXDAYS / DAYS_IN_WEEK];
232 int month;
4a7424a5 233 int32_t year;
852c8d21
SK
234 struct cal_month *next;
235};
236
5f845cb7 237/* function prototypes */
4a7424a5 238static int leap_year(int32_t year);
b549058b 239static void headers_init(struct cal_control *ctl);
4a7424a5 240static void set_consecutive_months(struct cal_month *month, int m, int32_t y);
852c8d21
SK
241static void cal_fill_month(struct cal_month *month, const struct cal_control *ctl);
242static void cal_output_header(struct cal_month *month, const struct cal_control *ctl);
243static void cal_output_months(struct cal_month *month, const struct cal_control *ctl);
ffc56357 244static void monthly(const struct cal_control *ctl);
ffc56357 245static void monthly3(const struct cal_control *ctl);
ffc56357 246static void yearly(const struct cal_control *ctl);
4a7424a5
SK
247static int day_in_year(int day, int month, int32_t year);
248static int day_in_week(int day, int month, int32_t year);
249static int week_number(int day, int month, int32_t year, const struct cal_control *ctl);
ffc56357 250static int week_to_day(const struct cal_control *ctl);
0106c9e2
SK
251static int center_str(const char *src, char *dest, size_t dest_size, size_t width);
252static void center(const char *str, size_t len, int separate);
253static void __attribute__((__noreturn__)) usage(FILE *out);
6dbe3af9 254
d4be073d
KZ
255int main(int argc, char **argv)
256{
6dbe3af9
KZ
257 struct tm *local_time;
258 time_t now;
ffc56357 259 int ch;
b549058b
SK
260 static struct cal_control ctl = {
261 .weekstart = SUNDAY,
262 .num_months = NUM_MONTHS,
d0c9ddc3 263 .colormode = UL_COLORMODE_UNDEF,
71ff238e 264 .weektype = WEEK_NUM_DISABLED,
ffc56357 265 .day_width = DAY_LEN,
95f4adde 266 .gutter_width = 2,
ffc56357
SK
267 .req.day = 0,
268 .req.month = 0
b549058b 269 };
7b353df2
SK
270
271 enum {
272 OPT_COLOR = CHAR_MAX + 1
273 };
eb63b9b8 274
022df321
SK
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'},
aae4f87e 282 {"week", optional_argument, NULL, 'w'},
7b353df2 283 {"color", optional_argument, NULL, OPT_COLOR},
022df321
SK
284 {"version", no_argument, NULL, 'V'},
285 {"help", no_argument, NULL, 'h'},
286 {NULL, 0, NULL, 0}
287 };
288
7eda085c
KZ
289 setlocale(LC_ALL, "");
290 bindtextdomain(PACKAGE, LOCALEDIR);
291 textdomain(PACKAGE);
c05a80ca 292 atexit(close_stdout);
e8f26419 293
310170b8 294#if defined(HAVE_LIBNCURSES) || defined(HAVE_LIBNCURSESW) || defined(HAVE_LIBTERMCAP)
d162fcb5
KZ
295 if ((term = getenv("TERM"))) {
296 int ret;
5f845cb7 297 my_setupterm(term, STDOUT_FILENO, &ret);
d162fcb5
KZ
298 if (ret > 0) {
299 Senter = my_tgetstr("so","smso");
300 Sexit = my_tgetstr("se","rmso");
301 }
302 }
303#endif
304
612721db 305/*
0a9bead0
PB
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.
612721db 310 */
69cabd72 311#if HAVE_DECL__NL_TIME_WEEK_1STDAY
0a9bead0
PB
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:
33c17354
SK
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
0a9bead0
PB
328 */
329 {
aebb9522
KZ
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;
0a9bead0 335 wfd = day_in_week(wfd % 100, (wfd / 100) % 100, wfd / (100 * 100));
b549058b 336 ctl.weekstart = (wfd + *nl_langinfo(_NL_TIME_FIRST_WEEKDAY) - 1) % DAYS_IN_WEEK;
0a9bead0 337 }
e8f26419 338#endif
d162fcb5 339
af7c483e 340 while ((ch = getopt_long(argc, argv, "13mjsywVh", longopts, NULL)) != -1)
6dbe3af9 341 switch(ch) {
d162fcb5 342 case '1':
b549058b 343 ctl.num_months = 1; /* default */
d162fcb5
KZ
344 break;
345 case '3':
b549058b 346 ctl.num_months = 3;
d162fcb5 347 break;
ffc43748 348 case 's':
b549058b 349 ctl.weekstart = SUNDAY; /* default */
ffc43748 350 break;
5c36a0eb 351 case 'm':
b549058b 352 ctl.weekstart = MONDAY;
5c36a0eb 353 break;
6dbe3af9 354 case 'j':
b549058b 355 ctl.julian = 1;
71ff238e 356 ctl.day_width = DAY_LEN + 1;
6dbe3af9
KZ
357 break;
358 case 'y':
b549058b 359 ctl.yflag = 1;
6dbe3af9 360 break;
c36c4a4e 361 case 'w':
aae4f87e 362 if (optarg) {
ffc56357 363 ctl.req.week = strtos32_or_err(optarg,
aae4f87e 364 _("invalid week argument"));
ffc56357 365 if (ctl.req.week < 1 || 53 < ctl.req.week)
aae4f87e
TK
366 errx(EXIT_FAILURE,_("illegal week value: use 1-53"));
367 }
b549058b 368 ctl.weektype = WEEK_NUM_US; /* default per weekstart */
c36c4a4e 369 break;
7b353df2 370 case OPT_COLOR:
201b39f0 371 if (optarg)
b549058b 372 ctl.colormode = colormode_or_err(optarg,
201b39f0 373 _("unsupported color mode"));
7b353df2 374 break;
eb63b9b8 375 case 'V':
e421313d 376 printf(UTIL_LINUX_VERSION);
37d6897f 377 return EXIT_SUCCESS;
022df321
SK
378 case 'h':
379 usage(stdout);
6dbe3af9
KZ
380 case '?':
381 default:
022df321 382 usage(stderr);
6dbe3af9
KZ
383 }
384 argc -= optind;
385 argv += optind;
386
b549058b 387 if (ctl.weektype) {
ffc56357 388 ctl.weektype = ctl.req.week & WEEK_NUM_MASK;
b549058b 389 ctl.weektype |= (ctl.weekstart == MONDAY ? WEEK_NUM_ISO : WEEK_NUM_US);
71ff238e
SK
390 ctl.week_width = (ctl.day_width * DAYS_IN_WEEK) + WNUM_LEN;
391 } else
392 ctl.week_width = ctl.day_width * DAYS_IN_WEEK;
c36c4a4e 393
a43145e1
RP
394 time(&now);
395 local_time = localtime(&now);
396
6dbe3af9 397 switch(argc) {
d7a92b89 398 case 3:
ffc56357
SK
399 ctl.req.day = strtos32_or_err(*argv++, _("illegal day value"));
400 if (ctl.req.day < 1 || DAYS_IN_MONTH < ctl.req.day)
5f845cb7 401 errx(EXIT_FAILURE, _("illegal day value: use 1-%d"), DAYS_IN_MONTH);
d7a92b89 402 /* FALLTHROUGH */
6dbe3af9 403 case 2:
ffc56357
SK
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)
022df321 406 errx(EXIT_FAILURE, _("illegal month value: use 1-12"));
6dbe3af9
KZ
407 /* FALLTHROUGH */
408 case 1:
4a7424a5 409 ctl.req.year = strtos32_or_err(*argv++, _("illegal year value"));
ffc56357 410 if (ctl.req.year < SMALLEST_YEAR)
e44fe471 411 errx(EXIT_FAILURE, _("illegal year value: use positive integer"));
4a7424a5
SK
412 if (ctl.req.year == INT32_MAX)
413 errx(EXIT_FAILURE, _("illegal year value"));
ffc56357
SK
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)
022df321 417 errx(EXIT_FAILURE, _("illegal day value: use 1-%d"), dm);
ffc56357 418 ctl.req.day = day_in_year(ctl.req.day, ctl.req.month, ctl.req.year);
4a7424a5 419 } else if ((int32_t) (local_time->tm_year + 1900) == ctl.req.year) {
ffc56357 420 ctl.req.day = local_time->tm_yday + 1;
d7a92b89 421 }
95f4adde
SK
422 if (!ctl.req.month && !ctl.req.week) {
423 ctl.req.month = local_time->tm_mon + 1;
b549058b 424 ctl.yflag = 1;
95f4adde 425 }
6dbe3af9
KZ
426 break;
427 case 0:
ffc56357
SK
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;
6dbe3af9
KZ
431 break;
432 default:
022df321 433 usage(stderr);
6dbe3af9 434 }
aae4f87e 435
ffc56357
SK
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;
c1732e62 440
aae4f87e 441 if (yday < 1)
4a7424a5 442 errx(EXIT_FAILURE, _("illegal week value: year %d "
c1732e62 443 "doesn't have week %d"),
ffc56357
SK
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) {
c1732e62
KZ
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. */
ffc56357 453 if (ctl.req.week != week_number(31, 12, ctl.req.year - 1, &ctl))
c1732e62 454 errx(EXIT_FAILURE,
4a7424a5 455 _("illegal week value: year %d "
c1732e62 456 "doesn't have week %d"),
ffc56357 457 ctl.req.year, ctl.req.week);
aae4f87e 458 }
ffc56357
SK
459 if (!ctl.req.month)
460 ctl.req.month = 12 < m ? 1 : m;
aae4f87e
TK
461 }
462
b549058b 463 headers_init(&ctl);
6dbe3af9 464
d0c9ddc3 465 if (!colors_init(ctl.colormode, "cal")) {
ffc56357 466 ctl.req.day = 0;
b549058b 467 ctl.weektype &= ~WEEK_NUM_MASK;
aae4f87e 468 }
d7a92b89 469
71ff238e
SK
470 if (ctl.yflag) {
471 if (ctl.julian)
472 ctl.num_months = MONTH_COLS - 1;
473 else
474 ctl.num_months = MONTH_COLS;
95f4adde 475 ctl.gutter_width = 3;
ffc56357 476 yearly(&ctl);
71ff238e 477 } else if (ctl.num_months == 1)
ffc56357 478 monthly(&ctl);
b549058b 479 else if (ctl.num_months == 3)
ffc56357 480 monthly3(&ctl);
37d6897f
KZ
481
482 return EXIT_SUCCESS;
6dbe3af9
KZ
483}
484
3b66dfd6 485/* leap year -- account for gregorian reformation in 1752 */
4a7424a5 486static int leap_year(int32_t year)
3b66dfd6 487{
5f845cb7 488 if (year <= REFORMATION_YEAR)
3b66dfd6
SK
489 return !(year % 4);
490 else
491 return ( !(year % 4) && (year % 100) ) || !(year % 400);
492}
493
b549058b 494static void headers_init(struct cal_control *ctl)
6dbe3af9 495{
71ff238e 496 size_t i, wd;
b283141c 497 char *cur_dh = day_headings;
efce94ec
SK
498 char tmp[FMT_ST_CHARS];
499 size_t year_len;
500
4a7424a5 501 year_len = snprintf(tmp, sizeof(tmp), "%d", ctl->req.year);
33c17354 502
5f845cb7 503 for (i = 0; i < DAYS_IN_WEEK; i++) {
add1924d 504 size_t space_left;
b549058b 505 wd = (i + ctl->weekstart) % DAYS_IN_WEEK;
33c17354
SK
506
507 if (i)
508 strcat(cur_dh++, " ");
add1924d
KZ
509 space_left = sizeof(day_headings) - (cur_dh - day_headings);
510
71ff238e 511 if (space_left <= (ctl->day_width - 1))
33c17354 512 break;
add1924d 513 cur_dh += center_str(nl_langinfo(ABDAY_1 + wd), cur_dh,
71ff238e 514 space_left, ctl->day_width - 1);
33c17354
SK
515 }
516
efce94ec 517 for (i = 0; i < MONTHS_IN_YEAR; i++) {
b549058b 518 ctl->full_month[i] = nl_langinfo(MON_1 + i);
efce94ec
SK
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 }
6dbe3af9
KZ
523}
524
4a7424a5 525static void set_consecutive_months(struct cal_month *month, int m, int32_t y)
d4be073d 526{
f60117b5
SK
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++;
94b26919 534 }
66ee8158
KZ
535 }
536}
537
852c8d21 538static void cal_fill_month(struct cal_month *month, const struct cal_control *ctl)
d4be073d 539{
852c8d21
SK
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 }
66ee8158 575
852c8d21
SK
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 }
66ee8158
KZ
591 }
592}
593
852c8d21
SK
594static 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
95f4adde 599 if (ctl->header_hint || ctl->yflag) {
852c8d21
SK
600 for (i = month; i; i = i->next) {
601 sprintf(out, _("%s"), ctl->full_month[i->month - 1]);
95f4adde 602 center(out, ctl->week_width - 1, i->next == NULL ? 0 : ctl->gutter_width);
852c8d21 603 }
95f4adde 604 if (!ctl->yflag) {
07ac4aa9 605 my_putstring("\n");
95f4adde 606 for (i = month; i; i = i->next) {
4a7424a5 607 sprintf(out, _("%d"), i->year);
95f4adde
SK
608 center(out, ctl->week_width - 1, i->next == NULL ? 0 : ctl->gutter_width);
609 }
852c8d21
SK
610 }
611 } else {
612 for (i = month; i; i = i->next) {
4a7424a5 613 sprintf(out, _("%s %d"), ctl->full_month[i->month - 1], i->year);
95f4adde 614 center(out, ctl->week_width - 1, i->next == NULL ? 0 : ctl->gutter_width);
852c8d21
SK
615 }
616 }
07ac4aa9 617 my_putstring("\n");
852c8d21
SK
618 for (i = month; i; i = i->next) {
619 if (ctl->weektype) {
620 if (ctl->julian)
07ac4aa9 621 sprintf(out, "%*s%s", (int)ctl->day_width - 1, "", day_headings);
852c8d21 622 else
07ac4aa9
RM
623 sprintf(out, "%*s%s", (int)ctl->day_width, "", day_headings);
624 my_putstring(out);
852c8d21 625 } else
07ac4aa9
RM
626 my_putstring(day_headings);
627 if (i->next != NULL) {
628 sprintf(out, "%*s", ctl->gutter_width, "");
629 my_putstring(out);
630 }
852c8d21 631 }
07ac4aa9 632 my_putstring("\n");
852c8d21
SK
633}
634
635static void cal_output_months(struct cal_month *month, const struct cal_control *ctl)
636{
07ac4aa9 637 char out[FMT_ST_CHARS];
852c8d21
SK
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])
07ac4aa9 659 sprintf(out, "%s%2d%s", Senter, i->weeks[week_line],
852c8d21
SK
660 Sexit);
661 else
07ac4aa9 662 sprintf(out, "%2d", i->weeks[week_line]);
852c8d21 663 } else
07ac4aa9
RM
664 sprintf(out, "%2s", "");
665 my_putstring(out);
852c8d21
SK
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])
07ac4aa9 676 sprintf(out, "%*s%s%*d%s", skip - (ctl->julian ? 3 : 2),
852c8d21
SK
677 "", Senter, (ctl->julian ? 3 : 2),
678 i->days[d], Sexit);
679 else
07ac4aa9 680 sprintf(out, "%*d", skip, i->days[d]);
852c8d21 681 } else
07ac4aa9
RM
682 sprintf(out, "%*s", skip, "");
683 my_putstring(out);
852c8d21
SK
684 if (skip < (int)ctl->day_width)
685 skip++;
686 }
07ac4aa9
RM
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);
852c8d21 695 }
852c8d21
SK
696 }
697}
698
699static 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
ffc56357 713static void monthly3(const struct cal_control *ctl)
d4be073d 714{
f60117b5
SK
715 struct cal_month m1, m2, m3, *i;
716 int first_month;
4a7424a5 717 int32_t first_year;
f60117b5
SK
718
719 m1.next = &m2;
720 m2.next = &m3;
721 m3.next = NULL;
722
ffc56357 723 if (ctl->req.month == 1) {
f60117b5
SK
724 first_month = MONTHS_IN_YEAR;
725 first_year = ctl->req.year - 1;
d162fcb5 726 } else {
f60117b5
SK
727 first_month = ctl->req.month - 1;
728 first_year = ctl->req.year;
e66ba1bf 729 }
94b26919 730
f60117b5
SK
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);
6dbe3af9
KZ
736}
737
ffc56357 738static void yearly(const struct cal_control *ctl)
d4be073d 739{
95f4adde
SK
740 struct cal_month m1, m2, m3, *i;
741 int month;
742 char out[FMT_ST_CHARS];
743 int year_width = 0;
add1924d 744
95f4adde
SK
745 m1.next = &m2;
746 if (ctl->julian)
747 m2.next = NULL;
748 else {
749 m2.next = &m3;
750 m3.next = NULL;
6dbe3af9 751 }
6dbe3af9 752
95f4adde
SK
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--;
4a7424a5 758 sprintf(out, "%d", ctl->req.year);
95f4adde 759 center(out, year_width, 0);
07ac4aa9 760 my_putstring("\n\n");
95f4adde
SK
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);
d162fcb5 768 }
95f4adde 769 /* Is empty line at the end year output really needed? */
07ac4aa9 770 my_putstring("\n");
6dbe3af9
KZ
771}
772
773/*
774 * day_in_year --
775 * return the 1 based day number within the year
776 */
4a7424a5 777static int day_in_year(int day, int month, int32_t year)
d4be073d 778{
6dbe3af9
KZ
779 int i, leap;
780
781 leap = leap_year(year);
782 for (i = 1; i < month; i++)
783 day += days_in_month[leap][i];
d162fcb5 784 return day;
6dbe3af9
KZ
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
d68f076f
SK
791 * 3 Sep. 1752 through 13 Sep. 1752, and returns invalid weekday
792 * during the period of 11 days.
6dbe3af9 793 */
4a7424a5 794static int day_in_week(int day, int month, int32_t year)
91ac9ef5
SK
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 };
ff90b006
SK
804 if (year != 1753)
805 year -= month < 3;
91ac9ef5 806 else
ff90b006
SK
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 return (year + (year / 4) - (year / 100) + (year / 400) + reform[month - 1] +
812 day) % 7;
813 if (year < REFORMATION_YEAR
814 || (year == REFORMATION_YEAR && month < 9)
815 || (year == REFORMATION_YEAR && month == 9 && day < 3))
816 return (year + year / 4 + old[month - 1] + day) % 7;
91ac9ef5 817 return NONEDAY;
6dbe3af9
KZ
818}
819
c36c4a4e
TK
820/*
821 * week_number
822 * return the week number of a given date, 1..53.
823 * Supports ISO-8601 and North American modes.
824 * Day may be given as Julian day of the year mode, in which
825 * case the month is disregarded entirely.
826 */
4a7424a5 827static int week_number(int day, int month, int32_t year, const struct cal_control *ctl)
c1732e62
KZ
828{
829 int fday = 0, yday;
830 int wday = day_in_week(1, 1, year);
831
b549058b 832 if (ctl->weektype & WEEK_NUM_ISO)
c1732e62
KZ
833 fday = wday + (wday >= FRIDAY ? -2 : 5);
834 else
835 /* WEEK_NUM_US
836 * - according to gcal, the first Sun is in the first week
837 * - according to wikipedia, the first Sat is in the first week
838 */
839 fday = wday + (wday == SUNDAY ? 6 : -1);
840
841 /* For julian dates the month can be set to 1, the global julian
842 * variable cannot be relied upon here, because we may recurse
843 * internally for 31.12. which would not work. */
844 if (day > 31)
845 month = 1;
846
c36c4a4e 847 yday = day_in_year(day,month,year);
c1732e62 848 if (year == REFORMATION_YEAR) {
c36c4a4e
TK
849 if (yday >= YDAY_AFTER_MISSING)
850 fday -= NUMBER_MISSING_DAYS;
c36c4a4e 851 }
c1732e62
KZ
852
853 /* Last year is last year */
854 if (yday + fday < 7)
b549058b 855 return week_number(31, 12, year - 1, ctl);
c1732e62
KZ
856
857 /* Or it could be part of the next year. The reformation year had less
858 * days than 365 making this check invalid, but reformation year ended
859 * on Sunday and in week 51, so it's ok here. */
b549058b 860 if (ctl->weektype == WEEK_NUM_ISO && yday >= 363
c1732e62
KZ
861 && day_in_week(day, month, year) >= MONDAY
862 && day_in_week(day, month, year) <= WEDNESDAY
863 && day_in_week(31, 12, year) >= MONDAY
864 && day_in_week(31, 12, year) <= WEDNESDAY)
b549058b 865 return week_number(1, 1, year + 1, ctl);
c1732e62
KZ
866
867 return (yday + fday) / 7;
c36c4a4e
TK
868}
869
aae4f87e
TK
870/*
871 * week_to_day
872 * return the yday of the first day in a given week inside
873 * the given year. This may be something other than Monday
874 * for ISO-8601 modes. For North American numbering this
875 * always returns a Sunday.
876 */
ffc56357 877static int week_to_day(const struct cal_control *ctl)
c1732e62 878{
aae4f87e 879 int yday, wday;
c1732e62 880
ffc56357
SK
881 wday = day_in_week(1, 1, ctl->req.year);
882 yday = ctl->req.week * 7 - wday;
c1732e62 883
b549058b 884 if (ctl->weektype & WEEK_NUM_ISO)
c1732e62
KZ
885 yday -= (wday >= FRIDAY ? -2 : 5);
886 else
887 yday -= (wday == SUNDAY ? 6 : -1); /* WEEK_NUM_US */
888 if (yday <= 0)
aae4f87e
TK
889 return 1;
890
891 return yday;
892}
893
d162fcb5
KZ
894/*
895 * Center string, handling multibyte characters appropriately.
896 * In addition if the string is too large for the width it's truncated.
ff87defc 897 * The number of trailing spaces may be 1 less than the number of leading spaces.
d162fcb5 898 */
d4be073d
KZ
899static int center_str(const char* src, char* dest,
900 size_t dest_size, size_t width)
d162fcb5 901{
104b92f8
PB
902 return mbsalign(src, dest, dest_size, &width,
903 MBS_ALIGN_CENTER, MBA_UNIBYTE_FALLBACK);
d162fcb5
KZ
904}
905
d4be073d 906static void center(const char *str, size_t len, int separate)
6dbe3af9 907{
d162fcb5 908 char lineout[FMT_ST_CHARS];
e66ba1bf 909
17282538 910 center_str(str, lineout, ARRAY_SIZE(lineout), len);
e66ba1bf
KZ
911 my_putstring(lineout);
912
913 if (separate) {
914 snprintf(lineout, sizeof(lineout), "%*s", separate, "");
915 my_putstring(lineout);
916 }
6dbe3af9
KZ
917}
918
022df321 919static void __attribute__ ((__noreturn__)) usage(FILE * out)
6dbe3af9 920{
201d43fd
SK
921 fputs(USAGE_HEADER, out);
922 fprintf(out, _(" %s [options] [[[day] month] year]\n"), program_invocation_short_name);
d4be073d 923
233ad1fa
PB
924 fputs(USAGE_SEPARATOR, out);
925 fputs(_("Display a calendar, or some part of it.\n"), out);
926 fputs(_("Without any arguments, display the current month.\n"), out);
927
201d43fd 928 fputs(USAGE_OPTIONS, out);
233ad1fa
PB
929 fputs(_(" -1, --one show only a single month (default)\n"), out);
930 fputs(_(" -3, --three show three months spanning the date\n"), out);
201d43fd
SK
931 fputs(_(" -s, --sunday Sunday as first day of week\n"), out);
932 fputs(_(" -m, --monday Monday as first day of week\n"), out);
933 fputs(_(" -j, --julian output Julian dates\n"), out);
233ad1fa 934 fputs(_(" -y, --year show the whole year\n"), out);
af7c483e 935 fputs(_(" -w, --week[=<num>] show US or ISO-8601 week numbers\n"), out);
201d43fd 936 fputs(_(" --color[=<when>] colorize messages (auto, always or never)\n"), out);
d4be073d 937
201d43fd
SK
938 fputs(USAGE_SEPARATOR, out);
939 fputs(USAGE_HELP, out);
940 fputs(USAGE_VERSION, out);
941 fprintf(out, USAGE_MAN_TAIL("cal(1)"));
d4be073d 942
022df321 943 exit(out == stderr ? EXIT_FAILURE : EXIT_SUCCESS);
6dbe3af9 944}