xref: /openbsd-src/usr.bin/cal/cal.c (revision 850e275390052b330d93020bf619a739a3c277ac)
1 /*	$OpenBSD: cal.c,v 1.23 2008/04/18 14:41:04 pyr 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.23 2008/04/18 14:41:04 pyr 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 void	j_yearly(int);
150 void	monthly(int, int);
151 void	trim_trailing_spaces(char *);
152 void	usage(void);
153 void	yearly(int);
154 int	parsemonth(const char *);
155 
156 int
157 main(int argc, char *argv[])
158 {
159 	struct tm *local_time;
160 	time_t now;
161 	int ch, month, year, yflag;
162 	const char *errstr;
163 
164 	yflag = year = 0;
165 	while ((ch = getopt(argc, argv, "jmwy")) != -1)
166 		switch(ch) {
167 		case 'j':
168 			julian = 1;
169 			break;
170 		case 'm':
171 			mflag = 1;
172 			break;
173 		case 'w':
174 			wflag = 1;
175 			break;
176 		case 'y':
177 			yflag = 1;
178 			break;
179 		case '?':
180 		default:
181 			usage();
182 		}
183 	argc -= optind;
184 	argv += optind;
185 
186 	if (julian && wflag)
187 		usage();
188 
189 	day_headings = DAY_HEADINGS_S;
190 	sep1752 = sep1752s;
191 	if (mflag && julian) {
192 		sep1752 = sep1752jm;
193 		day_headings = DAY_HEADINGS_JM;
194 	} else if (mflag) {
195 		sep1752 = sep1752m;
196 		day_headings = DAY_HEADINGS_M;
197 	} else if (julian) {
198 		sep1752 = sep1752js;
199 		day_headings = DAY_HEADINGS_JS;
200 	}
201 
202 	month = 0;
203 	switch(argc) {
204 	case 2:
205 		month = parsemonth(*argv++);
206 		if (!month)
207 			errx(1, "Unable to parse month");
208 		/* FALLTHROUGH */
209 	case 1:
210 		if (argc == 1 && !isdigit(*argv[0])) {
211 			month = parsemonth(*argv);
212 			if (!month)
213 				errx(1, "illegal year value: use 1-9999");
214 			(void)time(&now);
215 			local_time = localtime(&now);
216 			year = local_time->tm_year + TM_YEAR_BASE;
217 		} else {
218 			year = strtonum(*argv, 1, 9999, &errstr);
219 			if (errstr)
220 				errx(1, "illegal year value: use 1-9999");
221 		}
222 		break;
223 	case 0:
224 		(void)time(&now);
225 		local_time = localtime(&now);
226 		year = local_time->tm_year + TM_YEAR_BASE;
227 		if (!yflag)
228 			month = local_time->tm_mon + 1;
229 		break;
230 	default:
231 		usage();
232 	}
233 
234 	if (month)
235 		monthly(month, year);
236 	else if (julian)
237 		j_yearly(year);
238 	else
239 		yearly(year);
240 	exit(0);
241 }
242 
243 #define	DAY_LEN		3		/* 3 spaces per day */
244 #define	J_DAY_LEN	4		/* 4 spaces per day */
245 #define	WEEK_LEN	20		/* 7 * 3 - one space at the end */
246 #define WEEKNUMBER_LEN	5		/* 5 spaces per week number */
247 #define	J_WEEK_LEN	27		/* 7 * 4 - one space at the end */
248 #define	HEAD_SEP	2		/* spaces between day headings */
249 #define	J_HEAD_SEP	2
250 
251 int
252 week(int day, int month, int year)
253 {
254 	int	leap;
255 	int	prevleap;
256 	int	yearday;
257 	int	firstweekday;
258 	int	weekday;
259 	int	firstday;
260 	int	firstsunday;
261 	int	shift;
262 
263 	yearday = day_in_year(day, month, year);
264 	firstweekday = day_in_week(1, 1, year) + 1;
265 	weekday = day_in_week(day, month, year) + 1;
266 	leap = leap_year(year);
267 	prevleap = leap_year(year - 1);
268 	firstday = firstsunday = day_in_year(1, 1, year);
269 	firstsunday = firstday + (8 - firstweekday);
270 
271 	if (!mflag)
272 		goto sunbased;
273 
274 	if (yearday <= (8 - firstweekday) && firstweekday > 4) {
275 		if (firstweekday == 5 || (firstweekday == 6 && prevleap))
276 			return (53);
277 		return (52);
278 	}
279 
280 	if (((leap ? 366 : 365) - yearday) < (4 - weekday))
281 		return (1);
282 
283 	return (((yearday + (7 - weekday) + (firstweekday - 1)) / 7)
284 	    - (firstweekday > 4));
285 
286 sunbased:
287 	shift = 1;
288 	if (yearday < firstsunday)
289 		return (1);
290 	if (firstweekday > THURSDAY - 1)
291 		shift = 2;
292 	return ((((yearday + 1) - (weekday - 1)) / 7) + shift);
293 }
294 
295 void
296 monthly(int month, int year)
297 {
298 	int col, row, len, days[MAXDAYS], firstday;
299 	char *p, lineout[30];
300 
301 	day_array(month, year, days);
302 	(void)snprintf(lineout, sizeof(lineout), "%s %d",
303 	    month_names[month - 1], year);
304 	len = strlen(lineout);
305 	(void)printf("%*s%s\n%s\n",
306 	    ((julian ? J_WEEK_LEN : WEEK_LEN) - len) / 2, "",
307 	    lineout, day_headings);
308 	for (row = 0; row < 6; row++) {
309 		firstday = SPACE;
310 		for (col = 0, p = lineout; col < 7; col++,
311 		    p += julian ? J_DAY_LEN : DAY_LEN) {
312 			if (firstday == SPACE && days[row * 7 + col] != SPACE)
313 				firstday = days[row * 7 + col];
314 			ascii_day(p, days[row * 7 + col]);
315 		}
316 		*p = '\0';
317 		trim_trailing_spaces(lineout);
318 		(void)printf("%-20s", lineout);
319 		if (wflag && firstday != SPACE)
320 			printf(" [%2d]", week(firstday - mflag, month, year));
321 		printf("\n");
322 	}
323 }
324 
325 void
326 j_yearly(int year)
327 {
328 	int col, *dp, i, month, row, which_cal;
329 	int days[12][MAXDAYS];
330 	char *p, lineout[80];
331 
332 	(void)snprintf(lineout, sizeof(lineout), "%d", year);
333 	center(lineout, J_WEEK_LEN * 2 + J_HEAD_SEP, 0);
334 	(void)printf("\n\n");
335 	for (i = 0; i < 12; i++)
336 		day_array(i + 1, year, days[i]);
337 	(void)memset(lineout, ' ', sizeof(lineout) - 1);
338 	lineout[sizeof(lineout) - 1] = '\0';
339 	for (month = 0; month < 12; month += 2) {
340 		center(month_names[month], J_WEEK_LEN, J_HEAD_SEP);
341 		center(month_names[month + 1], J_WEEK_LEN, 0);
342 		(void)printf("\n%s%*s%s\n", day_headings,
343 		    J_HEAD_SEP, "", day_headings);
344 
345 		for (row = 0; row < 6; row++) {
346 			for (which_cal = 0; which_cal < 2; which_cal++) {
347 				p = lineout + which_cal * (J_WEEK_LEN + 2);
348 				dp = &days[month + which_cal][row * 7];
349 				for (col = 0; col < 7; col++, p += J_DAY_LEN)
350 					ascii_day(p, *dp++);
351 			}
352 			*p = '\0';
353 			trim_trailing_spaces(lineout);
354 			(void)printf("%s\n", lineout);
355 		}
356 	}
357 	(void)printf("\n");
358 }
359 
360 void
361 yearly(int year)
362 {
363 	int col, *dp, i, month, row, which_cal, week_len, wn, firstday;
364 	int days[12][MAXDAYS];
365 	char *p, lineout[81];
366 
367 	week_len = WEEK_LEN;
368 	if (wflag)
369 		week_len += WEEKNUMBER_LEN;
370 	(void)snprintf(lineout, sizeof(lineout), "%d", year);
371 	center(lineout, week_len * 3 + HEAD_SEP * 2, 0);
372 	(void)printf("\n\n");
373 	for (i = 0; i < 12; i++)
374 		day_array(i + 1, year, days[i]);
375 	(void)memset(lineout, ' ', sizeof(lineout) - 1);
376 	lineout[sizeof(lineout) - 1] = '\0';
377 	for (month = 0; month < 12; month += 3) {
378 		center(month_names[month], week_len, HEAD_SEP);
379 		center(month_names[month + 1], week_len, HEAD_SEP);
380 		center(month_names[month + 2], week_len, 0);
381 		(void)printf("\n%s%*s%s%*s%s\n", day_headings,
382 		    HEAD_SEP + (wflag ? WEEKNUMBER_LEN : 0), "", day_headings,
383 		    HEAD_SEP + (wflag ? WEEKNUMBER_LEN : 0), "", day_headings);
384 
385 		for (row = 0; row < 6; row++) {
386 			for (which_cal = 0; which_cal < 3; which_cal++) {
387 				p = lineout + which_cal * (week_len + 2);
388 
389 				dp = &days[month + which_cal][row * 7];
390 				firstday = SPACE;
391 				for (col = 0; col < 7; col++, p += DAY_LEN) {
392 					if (firstday == SPACE && *dp != SPACE)
393 						firstday = *dp;
394 					ascii_day(p, *dp++);
395 				}
396 				if (wflag && firstday != SPACE) {
397 					wn = week(firstday - mflag,
398 					    month + which_cal + 1, year);
399 					(void)snprintf(p, 5, "[%2d]", wn);
400 					p += strlen(p);
401 					*p = ' ';
402 				} else
403 					memset(p, ' ', 4);
404 			}
405 			*p = '\0';
406 			trim_trailing_spaces(lineout);
407 			(void)printf("%s\n", lineout);
408 		}
409 	}
410 	(void)printf("\n");
411 }
412 
413 /*
414  * day_array --
415  *	Fill in an array of 42 integers with a calendar.  Assume for a moment
416  *	that you took the (maximum) 6 rows in a calendar and stretched them
417  *	out end to end.  You would have 42 numbers or spaces.  This routine
418  *	builds that array for any month from Jan. 1 through Dec. 9999.
419  */
420 void
421 day_array(int month, int year, int *days)
422 {
423 	int day, dw, dm;
424 
425 	if (month == 9 && year == 1752) {
426 		memmove(days, sep1752, MAXDAYS * sizeof(int));
427 		return;
428 	}
429 	memmove(days, empty, MAXDAYS * sizeof(int));
430 	dm = days_in_month[leap_year(year)][month];
431 	dw = day_in_week(mflag?0:1, month, year);
432 	day = julian ? day_in_year(1, month, year) : 1;
433 	while (dm--)
434 		days[dw++] = day++;
435 }
436 
437 /*
438  * day_in_year --
439  *	return the 1 based day number within the year
440  */
441 int
442 day_in_year(int day, int month, int year)
443 {
444 	int i, leap;
445 
446 	leap = leap_year(year);
447 	for (i = 1; i < month; i++)
448 		day += days_in_month[leap][i];
449 	return (day);
450 }
451 
452 /*
453  * day_in_week
454  *	return the 0 based day number for any date from 1 Jan. 1 to
455  *	31 Dec. 9999.  Assumes the Gregorian reformation eliminates
456  *	3 Sep. 1752 through 13 Sep. 1752.  Returns Thursday for all
457  *	missing days.
458  */
459 int
460 day_in_week(int day, int month, int year)
461 {
462 	long temp;
463 
464 	temp = (long)(year - 1) * 365 + leap_years_since_year_1(year - 1)
465 	    + day_in_year(day, month, year);
466 	if (temp < FIRST_MISSING_DAY)
467 		return ((temp - 1 + SATURDAY) % 7);
468 	if (temp >= (FIRST_MISSING_DAY + NUMBER_MISSING_DAYS))
469 		return (((temp - 1 + SATURDAY) - NUMBER_MISSING_DAYS) % 7);
470 	return (THURSDAY);
471 }
472 
473 void
474 ascii_day(char *p, int day)
475 {
476 	int display, val;
477 	static const char *aday[] = {
478 		"",
479 		" 1", " 2", " 3", " 4", " 5", " 6", " 7",
480 		" 8", " 9", "10", "11", "12", "13", "14",
481 		"15", "16", "17", "18", "19", "20", "21",
482 		"22", "23", "24", "25", "26", "27", "28",
483 		"29", "30", "31",
484 	};
485 
486 	if (day == SPACE) {
487 		memset(p, ' ', julian ? J_DAY_LEN : DAY_LEN);
488 		return;
489 	}
490 	if (julian) {
491 		val = day / 100;
492 		if (val) {
493 			day %= 100;
494 			*p++ = val + '0';
495 			display = 1;
496 		} else {
497 			*p++ = ' ';
498 			display = 0;
499 		}
500 		val = day / 10;
501 		if (val || display)
502 			*p++ = val + '0';
503 		else
504 			*p++ = ' ';
505 		*p++ = day % 10 + '0';
506 	} else {
507 		*p++ = aday[day][0];
508 		*p++ = aday[day][1];
509 	}
510 	*p = ' ';
511 }
512 
513 void
514 trim_trailing_spaces(char *s)
515 {
516 	char *p;
517 
518 	for (p = s; *p; ++p)
519 		continue;
520 	while (p > s && isspace(*--p))
521 		continue;
522 	if (p > s)
523 		++p;
524 	*p = '\0';
525 }
526 
527 void
528 center(const char *str, int len, int separate)
529 {
530 
531 	len -= strlen(str);
532 	(void)printf("%*s%s%*s", len / 2, "", str,
533 	    len / 2 + len % 2 + separate, "");
534 }
535 
536 void
537 usage(void)
538 {
539 
540 	(void)fprintf(stderr, "usage: cal [-jmwy] [month] [year]\n");
541 	exit(1);
542 }
543 
544 int
545 parsemonth(const char *s)
546 {
547 	struct tm tm;
548 	char *cp;
549 	int v;
550 
551 	v = (int)strtol(s, &cp, 10);
552 	if (*cp != '\0') {		/* s wasn't purely numeric */
553 		v = 0;
554 		if ((cp = strptime(s, "%b", &tm)) != NULL && *cp == '\0')
555 			v = tm.tm_mon + 1;
556 	}
557 	if (v <= 0 || v > 12)
558 		errx(1, "invalid month: use 1-12 or a name");
559 	return (v);
560 }
561