xref: /openbsd-src/usr.bin/cal/cal.c (revision 2b0358df1d88d06ef4139321dd05bd5e05d91eaf)
1 /*	$OpenBSD: cal.c,v 1.24 2009/01/01 21:07:17 otto Exp $	*/
2 /*	$NetBSD: cal.c,v 1.6 1995/03/26 03:10:24 glass Exp $	*/
3 
4 /*
5  * Copyright (c) 1989, 1993, 1994
6  *	The Regents of the University of California.  All rights reserved.
7  *
8  * This code is derived from software contributed to Berkeley by
9  * Kim Letkeman.
10  *
11  * Redistribution and use in source and binary forms, with or without
12  * modification, are permitted provided that the following conditions
13  * are met:
14  * 1. Redistributions of source code must retain the above copyright
15  *    notice, this list of conditions and the following disclaimer.
16  * 2. Redistributions in binary form must reproduce the above copyright
17  *    notice, this list of conditions and the following disclaimer in the
18  *    documentation and/or other materials provided with the distribution.
19  * 3. Neither the name of the University nor the names of its contributors
20  *    may be used to endorse or promote products derived from this software
21  *    without specific prior written permission.
22  *
23  * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
24  * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
25  * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
26  * ARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
27  * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
28  * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
29  * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
30  * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
31  * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
32  * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
33  * SUCH DAMAGE.
34  */
35 
36 #ifndef lint
37 static const char copyright[] =
38 "@(#) Copyright (c) 1989, 1993, 1994\n\
39 	The Regents of the University of California.  All rights reserved.\n";
40 #if 0
41 static char sccsid[] = "@(#)cal.c	8.4 (Berkeley) 4/2/94";
42 #else
43 static const char rcsid[] = "$OpenBSD: cal.c,v 1.24 2009/01/01 21:07:17 otto Exp $";
44 #endif
45 #endif /* not lint */
46 
47 #include <sys/types.h>
48 
49 #include <ctype.h>
50 #include <err.h>
51 #include <stdio.h>
52 #include <stdlib.h>
53 #include <string.h>
54 #include <time.h>
55 #include <tzfile.h>
56 #include <unistd.h>
57 
58 #define	THURSDAY		4		/* for reformation */
59 #define	SATURDAY		6		/* 1 Jan 1 was a Saturday */
60 
61 #define	FIRST_MISSING_DAY	639799		/* 3 Sep 1752 */
62 #define	NUMBER_MISSING_DAYS	11		/* 11 day correction */
63 
64 #define	MAXDAYS			42		/* max slots in a month array */
65 #define	SPACE			-1		/* used in day array */
66 
67 static const int days_in_month[2][13] = {
68 	{0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31},
69 	{0, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31},
70 };
71 
72 const int sep1752s[MAXDAYS] = {
73 	SPACE,	SPACE,	1,	2,	14,	15,	16,
74 	17,	18,	19,	20,	21,	22,	23,
75 	24,	25,	26,	27,	28,	29,	30,
76 	SPACE,	SPACE,	SPACE,	SPACE,	SPACE,	SPACE,	SPACE,
77 	SPACE,	SPACE,	SPACE,	SPACE,	SPACE,	SPACE,	SPACE,
78 	SPACE,	SPACE,	SPACE,	SPACE,	SPACE,	SPACE,	SPACE,
79 }, sep1752m[MAXDAYS] = {
80 	SPACE,	1,	2,	14,	15,	16,	17,
81 	18,	19,	20,	21,	22,	23,	24,
82 	25,	26,	27,	28,	29,	30,	SPACE,
83 	SPACE,	SPACE,	SPACE,	SPACE,	SPACE,	SPACE,	SPACE,
84 	SPACE,	SPACE,	SPACE,	SPACE,	SPACE,	SPACE,	SPACE,
85 	SPACE,	SPACE,	SPACE,	SPACE,	SPACE,	SPACE,	SPACE,
86 }, sep1752js[MAXDAYS] = {
87 	SPACE,	SPACE,	245,	246,	258,	259,	260,
88 	261,	262,	263,	264,	265,	266,	267,
89 	268,	269,	270,	271,	272,	273,	274,
90 	SPACE,	SPACE,	SPACE,	SPACE,	SPACE,	SPACE,	SPACE,
91 	SPACE,	SPACE,	SPACE,	SPACE,	SPACE,	SPACE,	SPACE,
92 	SPACE,	SPACE,	SPACE,	SPACE,	SPACE,	SPACE,	SPACE,
93 }, sep1752jm[MAXDAYS] = {
94 	SPACE,	245,	246,	258,	259,	260,	261,
95 	262,	263,	264,	265,	266,	267,	268,
96 	269,	270,	271,	272,	273,	274,	SPACE,
97 	SPACE,	SPACE,	SPACE,	SPACE,	SPACE,	SPACE,	SPACE,
98 	SPACE,	SPACE,	SPACE,	SPACE,	SPACE,	SPACE,	SPACE,
99 	SPACE,	SPACE,	SPACE,	SPACE,	SPACE,	SPACE,	SPACE,
100 }, empty[MAXDAYS] = {
101 	SPACE,	SPACE,	SPACE,	SPACE,	SPACE,	SPACE,	SPACE,
102 	SPACE,	SPACE,	SPACE,	SPACE,	SPACE,	SPACE,	SPACE,
103 	SPACE,	SPACE,	SPACE,	SPACE,	SPACE,	SPACE,	SPACE,
104 	SPACE,	SPACE,	SPACE,	SPACE,	SPACE,	SPACE,	SPACE,
105 	SPACE,	SPACE,	SPACE,	SPACE,	SPACE,	SPACE,	SPACE,
106 	SPACE,	SPACE,	SPACE,	SPACE,	SPACE,	SPACE,	SPACE,
107 };
108 
109 const char *month_names[12] = {
110 	"January", "February", "March", "April", "May", "June",
111 	"July", "August", "September", "October", "November", "December",
112 };
113 
114 #define	DAY_HEADINGS_S	"Su Mo Tu We Th Fr Sa"
115 #define	DAY_HEADINGS_M	"Mo Tu We Th Fr Sa Su"
116 #define	DAY_HEADINGS_JS	" Su  Mo  Tu  We  Th  Fr  Sa"
117 #define	DAY_HEADINGS_JM	" Mo  Tu  We  Th  Fr  Sa  Su"
118 
119 const int	*sep1752 = NULL;
120 const char	*day_headings = NULL;
121 
122 /* leap year -- account for gregorian reformation in 1752 */
123 #define	leap_year(yr) \
124 	((yr) <= 1752 ? !((yr) % 4) : \
125 	(!((yr) % 4) && ((yr) % 100)) || !((yr) % 400))
126 
127 /* number of centuries since 1700, not inclusive */
128 #define	centuries_since_1700(yr) \
129 	((yr) > 1700 ? (yr) / 100 - 17 : 0)
130 
131 /* number of centuries since 1700 whose modulo of 400 is 0 */
132 #define	quad_centuries_since_1700(yr) \
133 	((yr) > 1600 ? ((yr) - 1600) / 400 : 0)
134 
135 /* number of leap years between year 1 and this year, not inclusive */
136 #define	leap_years_since_year_1(yr) \
137 	((yr) / 4 - centuries_since_1700(yr) + quad_centuries_since_1700(yr))
138 
139 int julian;
140 int mflag = 0;
141 int wflag = 0;
142 
143 void	ascii_day(char *, int);
144 void	center(const char *, int, int);
145 void	day_array(int, int, int *);
146 int	day_in_week(int, int, int);
147 int	day_in_year(int, int, int);
148 int	week(int, int, int);
149 int	isoweek(int, int, int);
150 void	j_yearly(int);
151 void	monthly(int, int);
152 void	trim_trailing_spaces(char *);
153 void	usage(void);
154 void	yearly(int);
155 int	parsemonth(const char *);
156 
157 int
158 main(int argc, char *argv[])
159 {
160 	struct tm *local_time;
161 	time_t now;
162 	int ch, month, year, yflag;
163 	const char *errstr;
164 
165 	yflag = year = 0;
166 	while ((ch = getopt(argc, argv, "jmwy")) != -1)
167 		switch(ch) {
168 		case 'j':
169 			julian = 1;
170 			break;
171 		case 'm':
172 			mflag = 1;
173 			break;
174 		case 'w':
175 			wflag = 1;
176 			break;
177 		case 'y':
178 			yflag = 1;
179 			break;
180 		case '?':
181 		default:
182 			usage();
183 		}
184 	argc -= optind;
185 	argv += optind;
186 
187 	if (julian && wflag)
188 		usage();
189 
190 	day_headings = DAY_HEADINGS_S;
191 	sep1752 = sep1752s;
192 	if (mflag && julian) {
193 		sep1752 = sep1752jm;
194 		day_headings = DAY_HEADINGS_JM;
195 	} else if (mflag) {
196 		sep1752 = sep1752m;
197 		day_headings = DAY_HEADINGS_M;
198 	} else if (julian) {
199 		sep1752 = sep1752js;
200 		day_headings = DAY_HEADINGS_JS;
201 	}
202 
203 	month = 0;
204 	switch(argc) {
205 	case 2:
206 		month = parsemonth(*argv++);
207 		if (!month)
208 			errx(1, "Unable to parse month");
209 		/* FALLTHROUGH */
210 	case 1:
211 		if (argc == 1 && !isdigit(*argv[0])) {
212 			month = parsemonth(*argv);
213 			if (!month)
214 				errx(1, "illegal year value: use 1-9999");
215 			(void)time(&now);
216 			local_time = localtime(&now);
217 			year = local_time->tm_year + TM_YEAR_BASE;
218 		} else {
219 			year = strtonum(*argv, 1, 9999, &errstr);
220 			if (errstr)
221 				errx(1, "illegal year value: use 1-9999");
222 		}
223 		break;
224 	case 0:
225 		(void)time(&now);
226 		local_time = localtime(&now);
227 		year = local_time->tm_year + TM_YEAR_BASE;
228 		if (!yflag)
229 			month = local_time->tm_mon + 1;
230 		break;
231 	default:
232 		usage();
233 	}
234 
235 	if (month)
236 		monthly(month, year);
237 	else if (julian)
238 		j_yearly(year);
239 	else
240 		yearly(year);
241 	exit(0);
242 }
243 
244 #define	DAY_LEN		3		/* 3 spaces per day */
245 #define	J_DAY_LEN	4		/* 4 spaces per day */
246 #define	WEEK_LEN	20		/* 7 * 3 - one space at the end */
247 #define WEEKNUMBER_LEN	5		/* 5 spaces per week number */
248 #define	J_WEEK_LEN	27		/* 7 * 4 - one space at the end */
249 #define	HEAD_SEP	2		/* spaces between day headings */
250 #define	J_HEAD_SEP	2
251 
252 int
253 week(int day, int month, int year)
254 {
255 	int	yearday;
256 	int	firstweekday;
257 	int	weekday;
258 	int	firstday;
259 	int	firstsunday;
260 	int	shift;
261 
262 	if (mflag)
263 		return isoweek(day, month, year);
264 
265 	yearday = day_in_year(day, month, year);
266 	firstweekday = day_in_week(1, 1, year) + 1;
267 	weekday = day_in_week(day, month, year) + 1;
268 	firstday = day_in_year(1, 1, year);
269 	firstsunday = firstday + (8 - firstweekday);
270 
271 	shift = 1;
272 	if (yearday < firstsunday)
273 		return (1);
274 	if (firstweekday > THURSDAY - 1)
275 		shift = 2;
276 	return ((((yearday + 1) - (weekday - 1)) / 7) + shift);
277 }
278 
279 int
280 isoweek(int day, int month, int year)
281 {
282 	/* http://www.tondering.dk/claus/cal/node8.html */
283 	int a, b, c, s, e, f, g, d, n;
284 
285 	a = month <= 2 ? year - 1 : year;
286 	b = a/4 - a/100 + a/400;
287 	c = (a-1)/4 - (a-1)/100 + (a-1)/400;
288 	s = b - c;
289 	if (month <= 2) {
290 		e = 0;
291 		f = day - 1 + 31 * (month-1);
292 	} else {
293 		e = s + 1;
294 		f = day + ((153 * (month-3) + 2) / 5) + 58 + s;
295 	}
296 	g = (a + b) % 7;
297 	d = (f + g - e) % 7;
298 	n = f + 3 - d;
299 
300 	if (n < 0)
301 		return 53 - (g - s) / 5;
302 	else if (n > 364 + s)
303 		return 1;
304 	else
305 		return n/7 + 1;
306 }
307 
308 void
309 monthly(int month, int year)
310 {
311 	int col, row, len, days[MAXDAYS], firstday;
312 	char *p, lineout[30];
313 
314 	day_array(month, year, days);
315 	(void)snprintf(lineout, sizeof(lineout), "%s %d",
316 	    month_names[month - 1], year);
317 	len = strlen(lineout);
318 	(void)printf("%*s%s\n%s\n",
319 	    ((julian ? J_WEEK_LEN : WEEK_LEN) - len) / 2, "",
320 	    lineout, day_headings);
321 	for (row = 0; row < 6; row++) {
322 		firstday = SPACE;
323 		for (col = 0, p = lineout; col < 7; col++,
324 		    p += julian ? J_DAY_LEN : DAY_LEN) {
325 			if (firstday == SPACE && days[row * 7 + col] != SPACE)
326 				firstday = days[row * 7 + col];
327 			ascii_day(p, days[row * 7 + col]);
328 		}
329 		*p = '\0';
330 		trim_trailing_spaces(lineout);
331 		(void)printf("%-20s", lineout);
332 		if (wflag && firstday != SPACE)
333 			printf(" [%2d]", week(firstday, month, year));
334 		printf("\n");
335 	}
336 }
337 
338 void
339 j_yearly(int year)
340 {
341 	int col, *dp, i, month, row, which_cal;
342 	int days[12][MAXDAYS];
343 	char *p, lineout[80];
344 
345 	(void)snprintf(lineout, sizeof(lineout), "%d", year);
346 	center(lineout, J_WEEK_LEN * 2 + J_HEAD_SEP, 0);
347 	(void)printf("\n\n");
348 	for (i = 0; i < 12; i++)
349 		day_array(i + 1, year, days[i]);
350 	(void)memset(lineout, ' ', sizeof(lineout) - 1);
351 	lineout[sizeof(lineout) - 1] = '\0';
352 	for (month = 0; month < 12; month += 2) {
353 		center(month_names[month], J_WEEK_LEN, J_HEAD_SEP);
354 		center(month_names[month + 1], J_WEEK_LEN, 0);
355 		(void)printf("\n%s%*s%s\n", day_headings,
356 		    J_HEAD_SEP, "", day_headings);
357 
358 		for (row = 0; row < 6; row++) {
359 			for (which_cal = 0; which_cal < 2; which_cal++) {
360 				p = lineout + which_cal * (J_WEEK_LEN + 2);
361 				dp = &days[month + which_cal][row * 7];
362 				for (col = 0; col < 7; col++, p += J_DAY_LEN)
363 					ascii_day(p, *dp++);
364 			}
365 			*p = '\0';
366 			trim_trailing_spaces(lineout);
367 			(void)printf("%s\n", lineout);
368 		}
369 	}
370 	(void)printf("\n");
371 }
372 
373 void
374 yearly(int year)
375 {
376 	int col, *dp, i, month, row, which_cal, week_len, wn, firstday;
377 	int days[12][MAXDAYS];
378 	char *p, lineout[81];
379 
380 	week_len = WEEK_LEN;
381 	if (wflag)
382 		week_len += WEEKNUMBER_LEN;
383 	(void)snprintf(lineout, sizeof(lineout), "%d", year);
384 	center(lineout, week_len * 3 + HEAD_SEP * 2, 0);
385 	(void)printf("\n\n");
386 	for (i = 0; i < 12; i++)
387 		day_array(i + 1, year, days[i]);
388 	(void)memset(lineout, ' ', sizeof(lineout) - 1);
389 	lineout[sizeof(lineout) - 1] = '\0';
390 	for (month = 0; month < 12; month += 3) {
391 		center(month_names[month], week_len, HEAD_SEP);
392 		center(month_names[month + 1], week_len, HEAD_SEP);
393 		center(month_names[month + 2], week_len, 0);
394 		(void)printf("\n%s%*s%s%*s%s\n", day_headings,
395 		    HEAD_SEP + (wflag ? WEEKNUMBER_LEN : 0), "", day_headings,
396 		    HEAD_SEP + (wflag ? WEEKNUMBER_LEN : 0), "", day_headings);
397 
398 		for (row = 0; row < 6; row++) {
399 			for (which_cal = 0; which_cal < 3; which_cal++) {
400 				p = lineout + which_cal * (week_len + 2);
401 
402 				dp = &days[month + which_cal][row * 7];
403 				firstday = SPACE;
404 				for (col = 0; col < 7; col++, p += DAY_LEN) {
405 					if (firstday == SPACE && *dp != SPACE)
406 						firstday = *dp;
407 					ascii_day(p, *dp++);
408 				}
409 				if (wflag && firstday != SPACE) {
410 					wn = week(firstday,
411 					    month + which_cal + 1, year);
412 					(void)snprintf(p, 5, "[%2d]", wn);
413 					p += strlen(p);
414 					*p = ' ';
415 				} else
416 					memset(p, ' ', 4);
417 			}
418 			*p = '\0';
419 			trim_trailing_spaces(lineout);
420 			(void)printf("%s\n", lineout);
421 		}
422 	}
423 	(void)printf("\n");
424 }
425 
426 /*
427  * day_array --
428  *	Fill in an array of 42 integers with a calendar.  Assume for a moment
429  *	that you took the (maximum) 6 rows in a calendar and stretched them
430  *	out end to end.  You would have 42 numbers or spaces.  This routine
431  *	builds that array for any month from Jan. 1 through Dec. 9999.
432  */
433 void
434 day_array(int month, int year, int *days)
435 {
436 	int day, dw, dm;
437 
438 	if (month == 9 && year == 1752) {
439 		memmove(days, sep1752, MAXDAYS * sizeof(int));
440 		return;
441 	}
442 	memmove(days, empty, MAXDAYS * sizeof(int));
443 	dm = days_in_month[leap_year(year)][month];
444 	dw = day_in_week(mflag?0:1, month, year);
445 	day = julian ? day_in_year(1, month, year) : 1;
446 	while (dm--)
447 		days[dw++] = day++;
448 }
449 
450 /*
451  * day_in_year --
452  *	return the 1 based day number within the year
453  */
454 int
455 day_in_year(int day, int month, int year)
456 {
457 	int i, leap;
458 
459 	leap = leap_year(year);
460 	for (i = 1; i < month; i++)
461 		day += days_in_month[leap][i];
462 	return (day);
463 }
464 
465 /*
466  * day_in_week
467  *	return the 0 based day number for any date from 1 Jan. 1 to
468  *	31 Dec. 9999.  Assumes the Gregorian reformation eliminates
469  *	3 Sep. 1752 through 13 Sep. 1752.  Returns Thursday for all
470  *	missing days.
471  */
472 int
473 day_in_week(int day, int month, int year)
474 {
475 	long temp;
476 
477 	temp = (long)(year - 1) * 365 + leap_years_since_year_1(year - 1)
478 	    + day_in_year(day, month, year);
479 	if (temp < FIRST_MISSING_DAY)
480 		return ((temp - 1 + SATURDAY) % 7);
481 	if (temp >= (FIRST_MISSING_DAY + NUMBER_MISSING_DAYS))
482 		return (((temp - 1 + SATURDAY) - NUMBER_MISSING_DAYS) % 7);
483 	return (THURSDAY);
484 }
485 
486 void
487 ascii_day(char *p, int day)
488 {
489 	int display, val;
490 	static const char *aday[] = {
491 		"",
492 		" 1", " 2", " 3", " 4", " 5", " 6", " 7",
493 		" 8", " 9", "10", "11", "12", "13", "14",
494 		"15", "16", "17", "18", "19", "20", "21",
495 		"22", "23", "24", "25", "26", "27", "28",
496 		"29", "30", "31",
497 	};
498 
499 	if (day == SPACE) {
500 		memset(p, ' ', julian ? J_DAY_LEN : DAY_LEN);
501 		return;
502 	}
503 	if (julian) {
504 		val = day / 100;
505 		if (val) {
506 			day %= 100;
507 			*p++ = val + '0';
508 			display = 1;
509 		} else {
510 			*p++ = ' ';
511 			display = 0;
512 		}
513 		val = day / 10;
514 		if (val || display)
515 			*p++ = val + '0';
516 		else
517 			*p++ = ' ';
518 		*p++ = day % 10 + '0';
519 	} else {
520 		*p++ = aday[day][0];
521 		*p++ = aday[day][1];
522 	}
523 	*p = ' ';
524 }
525 
526 void
527 trim_trailing_spaces(char *s)
528 {
529 	char *p;
530 
531 	for (p = s; *p; ++p)
532 		continue;
533 	while (p > s && isspace(*--p))
534 		continue;
535 	if (p > s)
536 		++p;
537 	*p = '\0';
538 }
539 
540 void
541 center(const char *str, int len, int separate)
542 {
543 
544 	len -= strlen(str);
545 	(void)printf("%*s%s%*s", len / 2, "", str,
546 	    len / 2 + len % 2 + separate, "");
547 }
548 
549 void
550 usage(void)
551 {
552 
553 	(void)fprintf(stderr, "usage: cal [-jmwy] [month] [year]\n");
554 	exit(1);
555 }
556 
557 int
558 parsemonth(const char *s)
559 {
560 	struct tm tm;
561 	char *cp;
562 	int v;
563 
564 	v = (int)strtol(s, &cp, 10);
565 	if (*cp != '\0') {		/* s wasn't purely numeric */
566 		v = 0;
567 		if ((cp = strptime(s, "%b", &tm)) != NULL && *cp == '\0')
568 			v = tm.tm_mon + 1;
569 	}
570 	if (v <= 0 || v > 12)
571 		errx(1, "invalid month: use 1-12 or a name");
572 	return (v);
573 }
574