| /* |
| * Copyright (c) 1989, 1993, 1994 |
| * The Regents of the University of California. All rights reserved. |
| * |
| * This code is derived from software contributed to Berkeley by |
| * Kim Letkeman. |
| * |
| * Redistribution and use in source and binary forms, with or without |
| * modification, are permitted provided that the following conditions |
| * are met: |
| * 1. Redistributions of source code must retain the above copyright |
| * notice, this list of conditions and the following disclaimer. |
| * 2. Redistributions in binary form must reproduce the above copyright |
| * notice, this list of conditions and the following disclaimer in the |
| * documentation and/or other materials provided with the distribution. |
| * 3. All advertising materials mentioning features or use of this software |
| * must display the following acknowledgement: |
| * This product includes software developed by the University of |
| * California, Berkeley and its contributors. |
| * 4. Neither the name of the University nor the names of its contributors |
| * may be used to endorse or promote products derived from this software |
| * without specific prior written permission. |
| * |
| * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND |
| * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE |
| * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE |
| * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE |
| * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL |
| * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS |
| * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) |
| * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT |
| * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY |
| * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF |
| * SUCH DAMAGE. |
| */ |
| |
| /* 1999-02-01 Jean-Francois Bignolles: added option '-m' to display |
| * monday as the first day of the week. |
| * 1999-02-22 Arkadiusz MiĆkiewicz <misiek@pld.ORG.PL> |
| * - added Native Language Support |
| * |
| * 2000-09-01 Michael Charles Pruznick <dummy@netwiz.net> |
| * Added "-3" option to print prev/next month with current. |
| * Added overridable default MONTHS_IN_ROW and "-1" option to |
| * get traditional output when -3 is the default. I hope that |
| * enough people will like -3 as the default that one day the |
| * product can be shipped that way. |
| * |
| * 2001-05-07 Pablo Saratxaga <pablo@mandrakesoft.com> |
| * Fixed the bugs with multi-byte charset (zg: cjk, utf-8) |
| * displaying. made the 'month year' ("%s %d") header translatable |
| * so it can be adapted to conventions used by different languages |
| * added support to read "first_weekday" locale information |
| * still to do: support for 'cal_direction' (will require a major |
| * rewrite of the displaying) and proper handling of RTL scripts |
| */ |
| |
| #include <sys/types.h> |
| |
| #include <ctype.h> |
| #include <getopt.h> |
| #include <stdint.h> |
| #include <stdio.h> |
| #include <stdlib.h> |
| #include <string.h> |
| #include <time.h> |
| #include <unistd.h> |
| #include <errno.h> |
| |
| #include "c.h" |
| #include "closestream.h" |
| #include "colors.h" |
| #include "nls.h" |
| #include "mbsalign.h" |
| #include "strutils.h" |
| #include "optutils.h" |
| #include "timeutils.h" |
| #include "ttyutils.h" |
| |
| #define DOY_MONTH_WIDTH 27 /* -j month width */ |
| #define DOM_MONTH_WIDTH 20 /* month width */ |
| |
| static int has_term = 0; |
| static const char *Senter = "", *Sexit = ""; /* enter and exit standout mode */ |
| |
| #if defined(HAVE_LIBNCURSES) || defined(HAVE_LIBNCURSESW) |
| # if defined(HAVE_NCURSESW_TERM_H) |
| # include <ncursesw/term.h> |
| # elif defined(HAVE_NCURSES_TERM_H) |
| # include <ncurses/term.h> |
| # elif defined(HAVE_TERM_H) |
| # include <term.h> |
| # endif |
| #endif |
| |
| static int setup_terminal(char *term |
| #if !defined(HAVE_LIBNCURSES) && !defined(HAVE_LIBNCURSESW) |
| __attribute__((__unused__)) |
| #endif |
| ) |
| { |
| #if defined(HAVE_LIBNCURSES) || defined(HAVE_LIBNCURSESW) |
| int ret; |
| |
| if (setupterm(term, STDOUT_FILENO, &ret) != 0 || ret != 1) |
| return -1; |
| #endif |
| return 0; |
| } |
| |
| static void my_putstring(char *s) |
| { |
| #if defined(HAVE_LIBNCURSES) || defined(HAVE_LIBNCURSESW) |
| if (has_term) |
| putp(s); |
| else |
| #endif |
| fputs(s, stdout); |
| } |
| |
| static const char *my_tgetstr(char *ss |
| #if !defined(HAVE_LIBNCURSES) && !defined(HAVE_LIBNCURSESW) |
| __attribute__((__unused__)) |
| #endif |
| ) |
| { |
| const char *ret = NULL; |
| |
| #if defined(HAVE_LIBNCURSES) || defined(HAVE_LIBNCURSESW) |
| if (has_term) |
| ret = tigetstr(ss); |
| #endif |
| if (!ret || ret == (char *)-1) |
| return ""; |
| return ret; |
| } |
| |
| #include "widechar.h" |
| |
| enum { |
| GREGORIAN = INT32_MIN, |
| ISO = INT32_MIN, |
| GB1752 = 1752, |
| DEFAULT_REFORM_YEAR = 1752, |
| JULIAN = INT32_MAX |
| }; |
| |
| enum { |
| SUNDAY = 0, |
| MONDAY, |
| TUESDAY, |
| WEDNESDAY, |
| THURSDAY, |
| FRIDAY, |
| SATURDAY, |
| DAYS_IN_WEEK, |
| NONEDAY |
| }; |
| |
| enum { |
| JANUARY = 1, |
| FEBRUARY, |
| MARCH, |
| APRIL, |
| MAY, |
| JUNE, |
| JULY, |
| AUGUST, |
| SEPTEMBER, |
| OCTOBER, |
| NOVEMBER, |
| DECEMBER |
| }; |
| |
| #define REFORMATION_MONTH SEPTEMBER |
| #define NUMBER_MISSING_DAYS 11 /* 11 day correction */ |
| #define YDAY_AFTER_MISSING 258 /* 14th in Sep 1752 */ |
| |
| #define MONTHS_IN_YEAR DECEMBER |
| #define DAYS_IN_MONTH 31 |
| #define MAXDAYS 42 /* slots in a month array */ |
| #define SPACE -1 /* used in day array */ |
| |
| #define SMALLEST_YEAR 1 |
| |
| #define DAY_LEN 3 /* 3 spaces per day */ |
| #define WEEK_LEN (DAYS_IN_WEEK * DAY_LEN) |
| #define MONTHS_IN_YEAR_ROW 3 /* month columns in year view */ |
| #define WNUM_LEN 3 |
| |
| #define FMT_ST_CHARS 300 /* 90 suffices in most locales */ |
| |
| static const int days_in_month[2][13] = { |
| {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}, |
| {0, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}, |
| }; |
| |
| enum { |
| WEEK_NUM_DISABLED = 0, |
| WEEK_NUM_MASK=0xff, |
| WEEK_NUM_ISO=0x100, |
| WEEK_NUM_US=0x200, |
| }; |
| |
| /* utf-8 can have up to 6 bytes per char; and an extra byte for ending \0 */ |
| static char day_headings[(WEEK_LEN + 1) * 6 + 1]; |
| |
| struct cal_request { |
| int day; |
| int month; |
| int32_t year; |
| int week; |
| int start_month; |
| }; |
| |
| struct cal_control { |
| const char *full_month[MONTHS_IN_YEAR]; /* month names */ |
| const char *abbr_month[MONTHS_IN_YEAR]; /* abbreviated month names */ |
| |
| int reform_year; /* Gregorian reform year */ |
| int colormode; /* day and week number highlight */ |
| int num_months; /* number of requested months */ |
| int span_months; /* span the date */ |
| int months_in_row; /* number of months horizontally in print out */ |
| int weekstart; /* day the week starts, often Sun or Mon */ |
| int weektype; /* WEEK_TYPE_{NONE,ISO,US} */ |
| size_t day_width; /* day width in characters in printout */ |
| size_t week_width; /* 7 * day_width + possible week num */ |
| int gutter_width; /* spaces in between horizontal month outputs */ |
| struct cal_request req; /* the times user is interested */ |
| unsigned int julian:1, /* julian output */ |
| header_year:1, /* print year number */ |
| header_hint:1; /* does month name + year need two lines to fit */ |
| }; |
| |
| struct cal_month { |
| int days[MAXDAYS]; /* the day numbers, or SPACE */ |
| int weeks[MAXDAYS / DAYS_IN_WEEK]; |
| int month; |
| int32_t year; |
| struct cal_month *next; |
| }; |
| |
| /* function prototypes */ |
| static int leap_year(const struct cal_control *ctl, int32_t year); |
| static int monthname_to_number(struct cal_control *ctl, const char *name); |
| static void headers_init(struct cal_control *ctl); |
| static void cal_fill_month(struct cal_month *month, const struct cal_control *ctl); |
| static void cal_output_header(struct cal_month *month, const struct cal_control *ctl); |
| static void cal_output_months(struct cal_month *month, const struct cal_control *ctl); |
| static void monthly(const struct cal_control *ctl); |
| static void yearly(const struct cal_control *ctl); |
| static int day_in_year(const struct cal_control *ctl, int day, |
| int month, int32_t year); |
| static int day_in_week(const struct cal_control *ctl, int day, |
| int month, int32_t year); |
| static int week_number(int day, int month, int32_t year, const struct cal_control *ctl); |
| static int week_to_day(const struct cal_control *ctl); |
| static int center_str(const char *src, char *dest, size_t dest_size, size_t width); |
| static void center(const char *str, size_t len, int separate); |
| static int parse_reform_year(const char *reform_year); |
| static void __attribute__((__noreturn__)) usage(void); |
| |
| #ifdef TEST_CAL |
| static time_t cal_time(time_t *t) |
| { |
| char *str = getenv("CAL_TEST_TIME"); |
| |
| if (str) { |
| uint64_t x = strtou64_or_err(str, "failed to parse CAL_TEST_TIME"); |
| |
| *t = x; |
| return *t; |
| } |
| |
| return time(t); |
| } |
| #else |
| # define cal_time(t) time(t) |
| #endif |
| |
| int main(int argc, char **argv) |
| { |
| struct tm *local_time; |
| char *term; |
| time_t now; |
| int ch = 0, yflag = 0, Yflag = 0; |
| |
| static struct cal_control ctl = { |
| .reform_year = DEFAULT_REFORM_YEAR, |
| .weekstart = SUNDAY, |
| .span_months = 0, |
| .colormode = UL_COLORMODE_UNDEF, |
| .weektype = WEEK_NUM_DISABLED, |
| .day_width = DAY_LEN, |
| .gutter_width = 2, |
| .req.day = 0, |
| .req.month = 0 |
| }; |
| |
| enum { |
| OPT_COLOR = CHAR_MAX + 1, |
| OPT_ISO, |
| OPT_REFORM |
| }; |
| |
| static const struct option longopts[] = { |
| {"one", no_argument, NULL, '1'}, |
| {"three", no_argument, NULL, '3'}, |
| {"sunday", no_argument, NULL, 's'}, |
| {"monday", no_argument, NULL, 'm'}, |
| {"julian", no_argument, NULL, 'j'}, |
| {"months", required_argument, NULL, 'n'}, |
| {"span", no_argument, NULL, 'S'}, |
| {"year", no_argument, NULL, 'y'}, |
| {"week", optional_argument, NULL, 'w'}, |
| {"color", optional_argument, NULL, OPT_COLOR}, |
| {"reform", required_argument, NULL, OPT_REFORM}, |
| {"iso", no_argument, NULL, OPT_ISO}, |
| {"version", no_argument, NULL, 'V'}, |
| {"twelve", no_argument, NULL, 'Y'}, |
| {"help", no_argument, NULL, 'h'}, |
| {NULL, 0, NULL, 0} |
| }; |
| |
| static const ul_excl_t excl[] = { /* rows and cols in ASCII order */ |
| { 'Y','n','y' }, |
| { 0 } |
| }; |
| int excl_st[ARRAY_SIZE(excl)] = UL_EXCL_STATUS_INIT; |
| |
| setlocale(LC_ALL, ""); |
| bindtextdomain(PACKAGE, LOCALEDIR); |
| textdomain(PACKAGE); |
| atexit(close_stdout); |
| |
| term = getenv("TERM"); |
| if (term) { |
| has_term = setup_terminal(term) == 0; |
| if (has_term) { |
| Senter = my_tgetstr("smso"); |
| Sexit = my_tgetstr("rmso"); |
| } |
| } |
| |
| /* |
| * The traditional Unix cal utility starts the week at Sunday, |
| * while ISO 8601 starts at Monday. We read the start day from |
| * the locale database, which can be overridden with the |
| * -s (Sunday) or -m (Monday) options. |
| */ |
| #if HAVE_DECL__NL_TIME_WEEK_1STDAY |
| /* |
| * You need to use 2 locale variables to get the first day of the week. |
| * This is needed to support first_weekday=2 and first_workday=1 for |
| * the rare case where working days span across 2 weeks. |
| * This shell script shows the combinations and calculations involved: |
| * |
| * for LANG in en_US ru_RU fr_FR csb_PL POSIX; do |
| * printf "%s:\t%s + %s -1 = " $LANG $(locale week-1stday first_weekday) |
| * date -d"$(locale week-1stday) +$(($(locale first_weekday)-1))day" +%w |
| * done |
| * |
| * en_US: 19971130 + 1 -1 = 0 #0 = sunday |
| * ru_RU: 19971130 + 2 -1 = 1 |
| * fr_FR: 19971201 + 1 -1 = 1 |
| * csb_PL: 19971201 + 2 -1 = 2 |
| * POSIX: 19971201 + 7 -1 = 0 |
| */ |
| { |
| int wfd; |
| union { unsigned int word; char *string; } val; |
| val.string = nl_langinfo(_NL_TIME_WEEK_1STDAY); |
| |
| wfd = val.word; |
| wfd = day_in_week(&ctl, wfd % 100, (wfd / 100) % 100, |
| wfd / (100 * 100)); |
| ctl.weekstart = (wfd + *nl_langinfo(_NL_TIME_FIRST_WEEKDAY) - 1) % DAYS_IN_WEEK; |
| } |
| #endif |
| while ((ch = getopt_long(argc, argv, "13mjn:sSywYVh", longopts, NULL)) != -1) { |
| |
| err_exclusive_options(ch, longopts, excl, excl_st); |
| |
| switch(ch) { |
| case '1': |
| ctl.num_months = 1; |
| break; |
| case '3': |
| ctl.num_months = 3; |
| ctl.span_months = 1; |
| break; |
| case 's': |
| ctl.weekstart = SUNDAY; /* default */ |
| break; |
| case 'm': |
| ctl.weekstart = MONDAY; |
| break; |
| case 'j': |
| ctl.julian = 1; |
| ctl.day_width = DAY_LEN + 1; |
| break; |
| case 'y': |
| yflag = 1; |
| break; |
| case 'Y': |
| Yflag = 1; |
| break; |
| case 'n': |
| ctl.num_months = strtou32_or_err(optarg, |
| _("invalid month argument")); |
| break; |
| case 'S': |
| ctl.span_months = 1; |
| break; |
| case 'w': |
| if (optarg) { |
| ctl.req.week = strtos32_or_err(optarg, |
| _("invalid week argument")); |
| if (ctl.req.week < 1 || 54 < ctl.req.week) |
| errx(EXIT_FAILURE,_("illegal week value: use 1-54")); |
| } |
| ctl.weektype = WEEK_NUM_US; /* default per weekstart */ |
| break; |
| case OPT_COLOR: |
| ctl.colormode = UL_COLORMODE_AUTO; |
| if (optarg) |
| ctl.colormode = colormode_or_err(optarg, |
| _("unsupported color mode")); |
| break; |
| case OPT_REFORM: |
| ctl.reform_year = parse_reform_year(optarg); |
| break; |
| case OPT_ISO: |
| ctl.reform_year = ISO; |
| break; |
| case 'V': |
| printf(UTIL_LINUX_VERSION); |
| return EXIT_SUCCESS; |
| case 'h': |
| usage(); |
| default: |
| errtryhelp(EXIT_FAILURE); |
| } |
| } |
| |
| argc -= optind; |
| argv += optind; |
| |
| if (ctl.weektype) { |
| ctl.weektype = ctl.req.week & WEEK_NUM_MASK; |
| ctl.weektype |= (ctl.weekstart == MONDAY ? WEEK_NUM_ISO : WEEK_NUM_US); |
| ctl.week_width = (ctl.day_width * DAYS_IN_WEEK) + WNUM_LEN; |
| } else |
| ctl.week_width = ctl.day_width * DAYS_IN_WEEK; |
| |
| if (argc == 1 && !isdigit_string(*argv)) { |
| usec_t x; |
| /* cal <timestamp> */ |
| if (parse_timestamp(*argv, &x) == 0) |
| now = (time_t) (x / 1000000); |
| /* cal <monthname> */ |
| else if ((ctl.req.month = monthname_to_number(&ctl, *argv)) > 0) |
| cal_time(&now); /* this year */ |
| else |
| errx(EXIT_FAILURE, _("failed to parse timestamp or unknown month name: %s"), *argv); |
| argc = 0; |
| } else |
| cal_time(&now); |
| |
| local_time = localtime(&now); |
| |
| switch(argc) { |
| case 3: |
| ctl.req.day = strtos32_or_err(*argv++, _("illegal day value")); |
| if (ctl.req.day < 1 || DAYS_IN_MONTH < ctl.req.day) |
| errx(EXIT_FAILURE, _("illegal day value: use 1-%d"), DAYS_IN_MONTH); |
| /* fallthrough */ |
| case 2: |
| if (isdigit(**argv)) |
| ctl.req.month = strtos32_or_err(*argv++, _("illegal month value: use 1-12")); |
| else { |
| ctl.req.month = monthname_to_number(&ctl, *argv); |
| if (ctl.req.month < 0) |
| errx(EXIT_FAILURE, _("unknown month name: %s"), *argv); |
| argv++; |
| } |
| if (ctl.req.month < 1 || MONTHS_IN_YEAR < ctl.req.month) |
| errx(EXIT_FAILURE, _("illegal month value: use 1-12")); |
| /* fallthrough */ |
| case 1: |
| ctl.req.year = strtos32_or_err(*argv++, _("illegal year value")); |
| if (ctl.req.year < SMALLEST_YEAR) |
| errx(EXIT_FAILURE, _("illegal year value: use positive integer")); |
| if (ctl.req.year == JULIAN) |
| errx(EXIT_FAILURE, _("illegal year value")); |
| if (ctl.req.day) { |
| int dm = days_in_month[leap_year(&ctl, ctl.req.year)] |
| [ctl.req.month]; |
| if (ctl.req.day > dm) |
| errx(EXIT_FAILURE, _("illegal day value: use 1-%d"), dm); |
| ctl.req.day = day_in_year(&ctl, ctl.req.day, |
| ctl.req.month, ctl.req.year); |
| } else if ((int32_t) (local_time->tm_year + 1900) == ctl.req.year) { |
| ctl.req.day = local_time->tm_yday + 1; |
| } |
| if (!ctl.req.month && !ctl.req.week) { |
| ctl.req.month = local_time->tm_mon + 1; |
| if (!ctl.num_months) |
| yflag = 1; |
| } |
| break; |
| case 0: |
| ctl.req.day = local_time->tm_yday + 1; |
| ctl.req.year = local_time->tm_year + 1900; |
| if (!ctl.req.month) |
| ctl.req.month = local_time->tm_mon + 1; |
| break; |
| default: |
| warnx(_("bad usage")); |
| errtryhelp(EXIT_FAILURE); |
| } |
| |
| if (0 < ctl.req.week) { |
| int yday = week_to_day(&ctl); |
| int leap = leap_year(&ctl, ctl.req.year); |
| int m = 1; |
| |
| if (yday < 1) |
| errx(EXIT_FAILURE, _("illegal week value: year %d " |
| "doesn't have week %d"), |
| ctl.req.year, ctl.req.week); |
| while (m <= DECEMBER && yday > days_in_month[leap][m]) |
| yday -= days_in_month[leap][m++]; |
| if (DECEMBER < m && ctl.weektype & WEEK_NUM_ISO) { |
| /* In some years (e.g. 2010 in ISO mode) it's possible |
| * to have a remnant of week 53 starting the year yet |
| * the year in question ends during 52, in this case |
| * we're assuming that early remnant is being referred |
| * to if 53 is given as argument. */ |
| if (ctl.req.week != week_number(31, DECEMBER, ctl.req.year - 1, &ctl)) |
| errx(EXIT_FAILURE, |
| _("illegal week value: year %d " |
| "doesn't have week %d"), |
| ctl.req.year, ctl.req.week); |
| } |
| if (!ctl.req.month) |
| ctl.req.month = MONTHS_IN_YEAR < m ? 1 : m; |
| } |
| |
| headers_init(&ctl); |
| |
| if (!colors_init(ctl.colormode, "cal")) { |
| ctl.req.day = 0; |
| ctl.weektype &= ~WEEK_NUM_MASK; |
| } |
| |
| if (yflag || Yflag) { |
| ctl.gutter_width = 3; |
| if (!ctl.num_months) |
| ctl.num_months = MONTHS_IN_YEAR; |
| if (yflag) { |
| ctl.req.start_month = 1; /* start from Jan */ |
| ctl.header_year = 1; /* print year number */ |
| } |
| } |
| |
| if (ctl.num_months > 1 && ctl.months_in_row == 0) { |
| ctl.months_in_row = MONTHS_IN_YEAR_ROW; /* default */ |
| |
| if (isatty(STDOUT_FILENO)) { |
| int w = get_terminal_width(STDOUT_FILENO); |
| int mw = ctl.julian ? DOY_MONTH_WIDTH : DOM_MONTH_WIDTH; |
| int extra = ((w / mw) - 1) * ctl.gutter_width; |
| int new_n = (w - extra) / mw; |
| |
| if (new_n < MONTHS_IN_YEAR_ROW) |
| ctl.months_in_row = new_n; |
| } |
| } else if (!ctl.months_in_row) |
| ctl.months_in_row = 1; |
| |
| if (!ctl.num_months) |
| ctl.num_months = 1; /* display at least one month */ |
| |
| if (yflag || Yflag) |
| yearly(&ctl); |
| else |
| monthly(&ctl); |
| |
| return EXIT_SUCCESS; |
| } |
| |
| /* leap year -- account for gregorian reformation in 1752 */ |
| static int leap_year(const struct cal_control *ctl, int32_t year) |
| { |
| if (year <= ctl->reform_year) |
| return !(year % 4); |
| else |
| return ( !(year % 4) && (year % 100) ) || !(year % 400); |
| } |
| |
| static void init_monthnames(struct cal_control *ctl) |
| { |
| size_t i; |
| |
| if (ctl->full_month[0] != NULL) |
| return; /* already initialized */ |
| |
| for (i = 0; i < MONTHS_IN_YEAR; i++) |
| ctl->full_month[i] = nl_langinfo(ALTMON_1 + i); |
| } |
| |
| static void init_abbr_monthnames(struct cal_control *ctl) |
| { |
| size_t i; |
| |
| if (ctl->abbr_month[0] != NULL) |
| return; /* already initialized */ |
| |
| for (i = 0; i < MONTHS_IN_YEAR; i++) |
| ctl->abbr_month[i] = nl_langinfo(_NL_ABALTMON_1 + i); |
| } |
| |
| static int monthname_to_number(struct cal_control *ctl, const char *name) |
| { |
| size_t i; |
| |
| init_monthnames(ctl); |
| for (i = 0; i < MONTHS_IN_YEAR; i++) |
| if (strcasecmp(ctl->full_month[i], name) == 0) |
| return i + 1; |
| |
| init_abbr_monthnames(ctl); |
| for (i = 0; i < MONTHS_IN_YEAR; i++) |
| if (strcasecmp(ctl->abbr_month[i], name) == 0) |
| return i + 1; |
| |
| return -EINVAL; |
| } |
| |
| static void headers_init(struct cal_control *ctl) |
| { |
| size_t i, wd; |
| char *cur_dh = day_headings; |
| char tmp[FMT_ST_CHARS]; |
| int year_len; |
| |
| year_len = snprintf(tmp, sizeof(tmp), "%04d", ctl->req.year); |
| |
| if (year_len < 0 || (size_t)year_len >= sizeof(tmp)) { |
| /* XXX impossible error */ |
| return; |
| } |
| |
| for (i = 0; i < DAYS_IN_WEEK; i++) { |
| size_t space_left; |
| wd = (i + ctl->weekstart) % DAYS_IN_WEEK; |
| |
| if (i) |
| strcat(cur_dh++, " "); |
| space_left = sizeof(day_headings) - (cur_dh - day_headings); |
| |
| if (space_left <= (ctl->day_width - 1)) |
| break; |
| cur_dh += center_str(nl_langinfo(ABDAY_1 + wd), cur_dh, |
| space_left, ctl->day_width - 1); |
| } |
| |
| init_monthnames(ctl); |
| |
| for (i = 0; i < MONTHS_IN_YEAR; i++) { |
| /* The +1 after year_len is space in between month and year. */ |
| if (ctl->week_width < strlen(ctl->full_month[i]) + year_len + 1) |
| ctl->header_hint = 1; |
| } |
| } |
| |
| static void cal_fill_month(struct cal_month *month, const struct cal_control *ctl) |
| { |
| int first_week_day = day_in_week(ctl, 1, month->month, month->year); |
| int month_days; |
| int i, j, weeklines = 0; |
| |
| if (ctl->julian) |
| j = day_in_year(ctl, 1, month->month, month->year); |
| else |
| j = 1; |
| month_days = j + days_in_month[leap_year(ctl, month->year)][month->month]; |
| |
| /* True when Sunday is not first day in the output week. */ |
| if (ctl->weekstart) { |
| first_week_day -= ctl->weekstart; |
| if (first_week_day < 0) |
| first_week_day = DAYS_IN_WEEK - ctl->weekstart; |
| month_days += ctl->weekstart - 1; |
| } |
| |
| /* Fill day array. */ |
| for (i = 0; i < MAXDAYS; i++) { |
| if (0 < first_week_day) { |
| month->days[i] = SPACE; |
| first_week_day--; |
| continue; |
| } |
| if (j < month_days) { |
| if (month->year == ctl->reform_year && |
| month->month == REFORMATION_MONTH && |
| (j == 3 || j == 247)) |
| j += NUMBER_MISSING_DAYS; |
| month->days[i] = j; |
| j++; |
| continue; |
| } |
| month->days[i] = SPACE; |
| weeklines++; |
| } |
| |
| /* Add week numbers */ |
| if (ctl->weektype) { |
| int weeknum = week_number(1, month->month, month->year, ctl); |
| weeklines = MAXDAYS / DAYS_IN_WEEK - weeklines / DAYS_IN_WEEK; |
| for (i = 0; i < MAXDAYS / DAYS_IN_WEEK; i++) { |
| if (0 < weeklines) { |
| if (52 < weeknum) |
| weeknum = week_number(month->days[i * DAYS_IN_WEEK], month->month, month->year, ctl); |
| month->weeks[i] = weeknum++; |
| } else |
| month->weeks[i] = SPACE; |
| weeklines--; |
| } |
| } |
| } |
| |
| static void cal_output_header(struct cal_month *month, const struct cal_control *ctl) |
| { |
| char out[FMT_ST_CHARS]; |
| struct cal_month *i; |
| |
| if (ctl->header_hint || ctl->header_year) { |
| for (i = month; i; i = i->next) { |
| sprintf(out, "%s", ctl->full_month[i->month - 1]); |
| center(out, ctl->week_width - 1, i->next == NULL ? 0 : ctl->gutter_width); |
| } |
| if (!ctl->header_year) { |
| my_putstring("\n"); |
| for (i = month; i; i = i->next) { |
| sprintf(out, "%04d", i->year); |
| center(out, ctl->week_width - 1, i->next == NULL ? 0 : ctl->gutter_width); |
| } |
| } |
| } else { |
| for (i = month; i; i = i->next) { |
| sprintf(out, "%s %04d", ctl->full_month[i->month - 1], i->year); |
| center(out, ctl->week_width - 1, i->next == NULL ? 0 : ctl->gutter_width); |
| } |
| } |
| my_putstring("\n"); |
| for (i = month; i; i = i->next) { |
| if (ctl->weektype) { |
| if (ctl->julian) |
| sprintf(out, "%*s%s", (int)ctl->day_width - 1, "", day_headings); |
| else |
| sprintf(out, "%*s%s", (int)ctl->day_width, "", day_headings); |
| my_putstring(out); |
| } else |
| my_putstring(day_headings); |
| if (i->next != NULL) { |
| sprintf(out, "%*s", ctl->gutter_width, ""); |
| my_putstring(out); |
| } |
| } |
| my_putstring("\n"); |
| } |
| |
| static void cal_output_months(struct cal_month *month, const struct cal_control *ctl) |
| { |
| char out[FMT_ST_CHARS]; |
| int reqday, week_line, d; |
| int skip; |
| struct cal_month *i; |
| |
| for (week_line = 0; week_line < MAXDAYS / DAYS_IN_WEEK; week_line++) { |
| for (i = month; i; i = i->next) { |
| /* Determine the day that should be highlighted. */ |
| reqday = 0; |
| if (i->month == ctl->req.month && i->year == ctl->req.year) { |
| if (ctl->julian) |
| reqday = ctl->req.day; |
| else |
| reqday = ctl->req.day + 1 - |
| day_in_year(ctl, 1, i->month, |
| i->year); |
| } |
| |
| if (ctl->weektype) { |
| if (0 < i->weeks[week_line]) { |
| if ((ctl->weektype & WEEK_NUM_MASK) == |
| i->weeks[week_line]) |
| sprintf(out, "%s%2d%s", Senter, i->weeks[week_line], |
| Sexit); |
| else |
| sprintf(out, "%2d", i->weeks[week_line]); |
| } else |
| sprintf(out, "%2s", ""); |
| my_putstring(out); |
| skip = ctl->day_width; |
| } else |
| /* First day of the week is one char narrower than the other days, |
| * unless week number is printed. */ |
| skip = ctl->day_width - 1; |
| |
| for (d = DAYS_IN_WEEK * week_line; |
| d < DAYS_IN_WEEK * week_line + DAYS_IN_WEEK; d++) { |
| if (0 < i->days[d]) { |
| if (reqday == i->days[d]) |
| sprintf(out, "%*s%s%*d%s", skip - (ctl->julian ? 3 : 2), |
| "", Senter, (ctl->julian ? 3 : 2), |
| i->days[d], Sexit); |
| else |
| sprintf(out, "%*d", skip, i->days[d]); |
| } else |
| sprintf(out, "%*s", skip, ""); |
| my_putstring(out); |
| if (skip < (int)ctl->day_width) |
| skip++; |
| } |
| if (i->next != NULL) { |
| sprintf(out, "%*s", ctl->gutter_width, ""); |
| my_putstring(out); |
| } |
| } |
| if (i == NULL) |
| my_putstring("\n"); |
| } |
| } |
| |
| static void monthly(const struct cal_control *ctl) |
| { |
| struct cal_month m1,m2,m3, *m; |
| int i, rows, new_month, month = ctl->req.start_month ? ctl->req.start_month : ctl->req.month; |
| int32_t year = ctl->req.year; |
| |
| /* cal -3, cal -Y --span, etc. */ |
| if (ctl->span_months) { |
| new_month = month - ctl->num_months / 2; |
| if (new_month < 1) { |
| month = new_month + MONTHS_IN_YEAR; |
| year--; |
| } |
| else |
| month = new_month; |
| } |
| |
| m1.next = (ctl->months_in_row > 1) ? &m2 : NULL; |
| m2.next = (ctl->months_in_row > 2) ? &m3 : NULL; |
| m3.next = NULL; |
| |
| rows = (ctl->num_months - 1) / ctl->months_in_row; |
| for (i = 0; i < rows + 1 ; i++){ |
| if (i == rows){ |
| switch (ctl->num_months % ctl->months_in_row){ |
| case 1: |
| m1.next = NULL; |
| /* fallthrough */ |
| case 2: |
| m2.next = NULL; |
| /* fallthrough */ |
| } |
| } |
| for (m = &m1; m; m = m->next){ |
| m->month = month++; |
| m->year = year; |
| if (MONTHS_IN_YEAR < month) { |
| year++; |
| month = 1; |
| } |
| cal_fill_month(m, ctl); |
| } |
| cal_output_header(&m1, ctl); |
| cal_output_months(&m1, ctl); |
| } |
| } |
| |
| static void yearly(const struct cal_control *ctl) |
| { |
| char out[FMT_ST_CHARS]; |
| int year_width = 0; |
| |
| year_width += (ctl->week_width + 1) * (ctl->julian ? 2 : 3); |
| if (ctl->julian) |
| year_width--; |
| |
| if (ctl->header_year) { |
| sprintf(out, "%04d", ctl->req.year); |
| center(out, year_width, 0); |
| my_putstring("\n\n"); |
| } |
| monthly(ctl); |
| } |
| |
| /* |
| * day_in_year -- |
| * return the 1 based day number within the year |
| */ |
| static int day_in_year(const struct cal_control *ctl, |
| int day, int month, int32_t year) |
| { |
| int i, leap; |
| |
| leap = leap_year(ctl, year); |
| for (i = 1; i < month; i++) |
| day += days_in_month[leap][i]; |
| return day; |
| } |
| |
| /* |
| * day_in_week |
| * return the 0 based day number for any date from 1 Jan. 1 to |
| * 31 Dec. 9999. Assumes the Gregorian reformation eliminates |
| * 3 Sep. 1752 through 13 Sep. 1752, and returns invalid weekday |
| * during the period of 11 days. |
| */ |
| static int day_in_week(const struct cal_control *ctl, int day, |
| int month, int32_t year) |
| { |
| /* |
| * The magic constants in the reform[] array are, in a simplified |
| * sense, the remaining days after slicing into one week periods the total |
| * days from the beginning of the year to the target month. That is, |
| * weeks + reform[] days gets us to the target month. The exception is, |
| * that for the months past February 'DOY - 1' must be used. |
| * |
| * DoY (Day of Year): total days to the target month |
| * |
| * Month 1 2 3 4 5 6 7 8 9 10 11 12 |
| * DoY 0 31 59 90 120 151 181 212 243 273 304 334 |
| * DoY % 7 0 3 |
| * DoY - 1 % 7 - -- 2 5 0 3 5 1 4 6 2 4 |
| * reform[] = { 0, 3, 2, 5, 0, 3, 5, 1, 4, 6, 2, 4 }; |
| * |
| * Note: these calculations are for non leap years. |
| */ |
| static const int reform[] = { 0, 3, 2, 5, 0, 3, 5, 1, 4, 6, 2, 4 }; |
| static const int old[] = { 5, 1, 0, 3, 5, 1, 3, 6, 2, 4, 0, 2 }; |
| |
| if (year != ctl->reform_year + 1) |
| year -= month < MARCH; |
| else |
| year -= (month < MARCH) + 14; |
| if (ctl->reform_year < year |
| || (year == ctl->reform_year && REFORMATION_MONTH < month) |
| || (year == ctl->reform_year |
| && month == REFORMATION_MONTH && 13 < day)) { |
| return ((int64_t) year + (year / 4) |
| - (year / 100) + (year / 400) |
| + reform[month - 1] + day) % DAYS_IN_WEEK; |
| } |
| if (year < ctl->reform_year |
| || (year == ctl->reform_year && month < REFORMATION_MONTH) |
| || (year == ctl->reform_year && month == REFORMATION_MONTH && day < 3)) |
| return ((int64_t) year + year / 4 + old[month - 1] + day) |
| % DAYS_IN_WEEK; |
| return NONEDAY; |
| } |
| |
| /* |
| * week_number |
| * return the week number of a given date, 1..54. |
| * Supports ISO-8601 and North American modes. |
| * Day may be given as Julian day of the year mode, in which |
| * case the month is disregarded entirely. |
| */ |
| static int week_number(int day, int month, int32_t year, const struct cal_control *ctl) |
| { |
| int fday = 0, yday; |
| const int wday = day_in_week(ctl, 1, JANUARY, year); |
| |
| if (ctl->weektype & WEEK_NUM_ISO) |
| fday = wday + (wday >= FRIDAY ? -2 : 5); |
| else { |
| /* WEEK_NUM_US: Jan 1 is always First week, that may |
| * begin previous year. That means there is very seldom |
| * more than 52 weeks, */ |
| fday = wday + 6; |
| } |
| /* For julian dates the month can be set to 1, the global julian |
| * variable cannot be relied upon here, because we may recurse |
| * internally for 31.12. which would not work. */ |
| if (day > DAYS_IN_MONTH) |
| month = JANUARY; |
| |
| yday = day_in_year(ctl, day, month, year); |
| if (year == ctl->reform_year && yday >= YDAY_AFTER_MISSING) |
| fday -= NUMBER_MISSING_DAYS; |
| |
| /* Last year is last year */ |
| if (yday + fday < DAYS_IN_WEEK) |
| return week_number(31, DECEMBER, year - 1, ctl); |
| |
| /* Or it could be part of the next year. The reformation year had less |
| * days than 365 making this check invalid, but reformation year ended |
| * on Sunday and in week 51, so it's ok here. */ |
| if (ctl->weektype == WEEK_NUM_ISO && yday >= 363 |
| && day_in_week(ctl, day, month, year) >= MONDAY |
| && day_in_week(ctl, day, month, year) <= WEDNESDAY |
| && day_in_week(ctl, 31, DECEMBER, year) >= MONDAY |
| && day_in_week(ctl, 31, DECEMBER, year) <= WEDNESDAY) |
| return week_number(1, JANUARY, year + 1, ctl); |
| |
| return (yday + fday) / DAYS_IN_WEEK; |
| } |
| |
| /* |
| * week_to_day |
| * return the yday of the first day in a given week inside |
| * the given year. This may be something other than Monday |
| * for ISO-8601 modes. For North American numbering this |
| * always returns a Sunday. |
| */ |
| static int week_to_day(const struct cal_control *ctl) |
| { |
| int yday, wday; |
| |
| wday = day_in_week(ctl, 1, JANUARY, ctl->req.year); |
| yday = ctl->req.week * DAYS_IN_WEEK - wday; |
| |
| if (ctl->req.year == ctl->reform_year && yday >= YDAY_AFTER_MISSING) |
| yday += NUMBER_MISSING_DAYS; |
| |
| if (ctl->weektype & WEEK_NUM_ISO) |
| yday -= (wday >= FRIDAY ? -2 : 5); |
| else |
| yday -= 6; /* WEEK_NUM_US */ |
| if (yday <= 0) |
| return 1; |
| |
| return yday; |
| } |
| |
| /* |
| * Center string, handling multibyte characters appropriately. |
| * In addition if the string is too large for the width it's truncated. |
| * The number of trailing spaces may be 1 less than the number of leading spaces. |
| */ |
| static int center_str(const char* src, char* dest, |
| size_t dest_size, size_t width) |
| { |
| return mbsalign(src, dest, dest_size, &width, |
| MBS_ALIGN_CENTER, MBA_UNIBYTE_FALLBACK); |
| } |
| |
| static void center(const char *str, size_t len, int separate) |
| { |
| char lineout[FMT_ST_CHARS]; |
| |
| center_str(str, lineout, ARRAY_SIZE(lineout), len); |
| my_putstring(lineout); |
| |
| if (separate) { |
| snprintf(lineout, sizeof(lineout), "%*s", separate, ""); |
| my_putstring(lineout); |
| } |
| } |
| |
| static int parse_reform_year(const char *reform_year) |
| { |
| size_t i; |
| |
| struct reform { |
| char *name; |
| int val; |
| }; |
| |
| struct reform years[] = { |
| {"gregorian", GREGORIAN}, |
| {"iso", ISO}, |
| {"1752", GB1752}, |
| {"julian", JULIAN}, |
| }; |
| |
| for (i = 0; i < ARRAY_SIZE(years); i++) { |
| if (strcasecmp(reform_year, years[i].name) == 0) { |
| return years[i].val; |
| } |
| } |
| errx(EXIT_FAILURE, "invalid --reform value: '%s'", reform_year); |
| } |
| |
| static void __attribute__((__noreturn__)) usage(void) |
| { |
| FILE *out = stdout; |
| fputs(USAGE_HEADER, out); |
| fprintf(out, _(" %s [options] [[[day] month] year]\n"), program_invocation_short_name); |
| fprintf(out, _(" %s [options] <timestamp|monthname>\n"), program_invocation_short_name); |
| |
| fputs(USAGE_SEPARATOR, out); |
| fputs(_("Display a calendar, or some part of it.\n"), out); |
| fputs(_("Without any arguments, display the current month.\n"), out); |
| |
| fputs(USAGE_OPTIONS, out); |
| fputs(_(" -1, --one show only a single month (default)\n"), out); |
| fputs(_(" -3, --three show three months spanning the date\n"), out); |
| fputs(_(" -n, --months <num> show num months starting with date's month\n"), out); |
| fputs(_(" -S, --span span the date when displaying multiple months\n"), out); |
| fputs(_(" -s, --sunday Sunday as first day of week\n"), out); |
| fputs(_(" -m, --monday Monday as first day of week\n"), out); |
| fputs(_(" -j, --julian use day-of-year for all calendars\n"), out); |
| fputs(_(" --reform <val> Gregorian reform date (1752|gregorian|iso|julian)\n"), out); |
| fputs(_(" --iso alias for --reform=iso\n"), out); |
| fputs(_(" -y, --year show the whole year\n"), out); |
| fputs(_(" -Y, --twelve show the next twelve months\n"), out); |
| fputs(_(" -w, --week[=<num>] show US or ISO-8601 week numbers\n"), out); |
| fputs(_(" --color[=<when>] colorize messages (auto, always or never)\n"), out); |
| fprintf(out, |
| " %s\n", USAGE_COLORS_DEFAULT); |
| |
| fputs(USAGE_SEPARATOR, out); |
| printf(USAGE_HELP_OPTIONS(23)); |
| printf(USAGE_MAN_TAIL("cal(1)")); |
| |
| exit(EXIT_SUCCESS); |
| } |