/* date_strftime.c: based on a public-domain implementation of ANSI C library routine strftime, which is originally written by Arnold Robbins. */ #include "ruby/ruby.h" #include "date_tmx.h" #include #include #include #include #if defined(HAVE_SYS_TIME_H) #include #endif #undef strchr /* avoid AIX weirdness */ #define range(low, item, hi) (item) #define add(x,y) (rb_funcall((x), '+', 1, (y))) #define sub(x,y) (rb_funcall((x), '-', 1, (y))) #define mul(x,y) (rb_funcall((x), '*', 1, (y))) #define quo(x,y) (rb_funcall((x), rb_intern("quo"), 1, (y))) #define div(x,y) (rb_funcall((x), rb_intern("div"), 1, (y))) #define mod(x,y) (rb_funcall((x), '%', 1, (y))) static void upcase(char *s, size_t i) { do { if (ISLOWER(*s)) *s = TOUPPER(*s); } while (s++, --i); } static void downcase(char *s, size_t i) { do { if (ISUPPER(*s)) *s = TOLOWER(*s); } while (s++, --i); } /* strftime --- produce formatted time */ static size_t date_strftime_with_tmx(char *s, size_t maxsize, const char *format, const struct tmx *tmx) { char *endp = s + maxsize; char *start = s; const char *sp, *tp; auto char tbuf[100]; ptrdiff_t i; int v, w; size_t colons; int precision, flags; char padding; /* LOCALE_[OE] and COLONS are actually modifiers, not flags */ enum {LEFT, CHCASE, LOWER, UPPER, LOCALE_O, LOCALE_E, COLONS}; #define BIT_OF(n) (1U<<(n)) /* various tables for locale C */ static const char days_l[][10] = { "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", }; static const char months_l[][10] = { "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December", }; static const char ampm[][3] = { "AM", "PM", }; if (s == NULL || format == NULL || tmx == NULL || maxsize == 0) return 0; /* quick check if we even need to bother */ if (strchr(format, '%') == NULL && strlen(format) + 1 >= maxsize) { err: errno = ERANGE; return 0; } for (; *format && s < endp - 1; format++) { #define FLAG_FOUND() do { \ if (precision > 0 || flags & (BIT_OF(LOCALE_E) | BIT_OF(LOCALE_O) | BIT_OF(COLONS))) \ goto unknown; \ } while (0) #define NEEDS(n) do if (s >= endp || (n) >= endp - s - 1) goto err; while (0) #define FILL_PADDING(i) do { \ if (!(flags & BIT_OF(LEFT)) && precision > (i)) { \ NEEDS(precision); \ memset(s, padding ? padding : ' ', precision - (i)); \ s += precision - (i); \ } \ else { \ NEEDS(i); \ } \ } while (0); #define FMT(def_pad, def_prec, fmt, val) \ do { \ int l; \ if (precision <= 0) precision = (def_prec); \ if (flags & BIT_OF(LEFT)) precision = 1; \ l = snprintf(s, endp - s, \ ((padding == '0' || (!padding && (def_pad) == '0')) ? \ "%0*"fmt : "%*"fmt), \ precision, (val)); \ if (l < 0) goto err; \ s += l; \ } while (0) #define STRFTIME(fmt) \ do { \ i = date_strftime_with_tmx(s, endp - s, (fmt), tmx); \ if (!i) return 0; \ if (flags & BIT_OF(UPPER)) \ upcase(s, i); \ if (!(flags & BIT_OF(LEFT)) && precision > i) { \ if (start + maxsize < s + precision) { \ errno = ERANGE; \ return 0; \ } \ memmove(s + precision - i, s, i); \ memset(s, padding ? padding : ' ', precision - i); \ s += precision; \ } \ else s += i; \ } while (0) #define FMTV(def_pad, def_prec, fmt, val) \ do { \ VALUE tmp = (val); \ if (FIXNUM_P(tmp)) { \ FMT((def_pad), (def_prec), "l"fmt, FIX2LONG(tmp)); \ } \ else { \ VALUE args[2], result; \ size_t l; \ if (precision <= 0) precision = (def_prec); \ if (flags & BIT_OF(LEFT)) precision = 1; \ args[0] = INT2FIX(precision); \ args[1] = (val); \ if (padding == '0' || (!padding && (def_pad) == '0')) \ result = rb_str_format(2, args, rb_str_new2("%0*"fmt)); \ else \ result = rb_str_format(2, args, rb_str_new2("%*"fmt)); \ l = strlcpy(s, StringValueCStr(result), endp - s); \ if ((size_t)(endp - s) <= l) \ goto err; \ s += l; \ } \ } while (0) if (*format != '%') { *s++ = *format; continue; } tp = tbuf; sp = format; precision = -1; flags = 0; padding = 0; colons = 0; again: switch (*++format) { case '\0': format--; goto unknown; case 'A': /* full weekday name */ case 'a': /* abbreviated weekday name */ if (flags & BIT_OF(CHCASE)) { flags &= ~(BIT_OF(LOWER) | BIT_OF(CHCASE)); flags |= BIT_OF(UPPER); } { int wday = tmx_wday; if (wday < 0 || wday > 6) i = 1, tp = "?"; else { if (*format == 'A') i = strlen(tp = days_l[wday]); else i = 3, tp = days_l[wday]; } } break; case 'B': /* full month name */ case 'b': /* abbreviated month name */ case 'h': /* same as %b */ if (flags & BIT_OF(CHCASE)) { flags &= ~(BIT_OF(LOWER) | BIT_OF(CHCASE)); flags |= BIT_OF(UPPER); } { int mon = tmx_mon; if (mon < 1 || mon > 12) i = 1, tp = "?"; else { if (*format == 'B') i = strlen(tp = months_l[mon - 1]); else i = 3, tp = months_l[mon - 1]; } } break; case 'C': /* century (year/100) */ FMTV('0', 2, "d", div(tmx_year, INT2FIX(100))); continue; case 'c': /* appropriate date and time representation */ STRFTIME("%a %b %e %H:%M:%S %Y"); continue; case 'D': STRFTIME("%m/%d/%y"); continue; case 'd': /* day of the month, 01 - 31 */ case 'e': /* day of month, blank padded */ v = range(1, tmx_mday, 31); FMT((*format == 'd') ? '0' : ' ', 2, "d", v); continue; case 'F': STRFTIME("%Y-%m-%d"); continue; case 'G': /* year of ISO week with century */ case 'Y': /* year with century */ { VALUE year = (*format == 'G') ? tmx_cwyear : tmx_year; if (FIXNUM_P(year)) { long y = FIX2LONG(year); FMT('0', 0 <= y ? 4 : 5, "ld", y); } else { FMTV('0', 4, "d", year); } } continue; case 'g': /* year of ISO week without a century */ case 'y': /* year without a century */ v = NUM2INT(mod((*format == 'g') ? tmx_cwyear : tmx_year, INT2FIX(100))); FMT('0', 2, "d", v); continue; case 'H': /* hour, 24-hour clock, 00 - 23 */ case 'k': /* hour, 24-hour clock, blank pad */ v = range(0, tmx_hour, 23); FMT((*format == 'H') ? '0' : ' ', 2, "d", v); continue; case 'I': /* hour, 12-hour clock, 01 - 12 */ case 'l': /* hour, 12-hour clock, 1 - 12, blank pad */ v = range(0, tmx_hour, 23); if (v == 0) v = 12; else if (v > 12) v -= 12; FMT((*format == 'I') ? '0' : ' ', 2, "d", v); continue; case 'j': /* day of the year, 001 - 366 */ v = range(1, tmx_yday, 366); FMT('0', 3, "d", v); continue; case 'L': /* millisecond */ case 'N': /* nanosecond */ if (*format == 'L') w = 3; else w = 9; if (precision <= 0) precision = w; NEEDS(precision); { VALUE subsec = tmx_sec_fraction; int ww; long n; ww = precision; while (9 <= ww) { subsec = mul(subsec, INT2FIX(1000000000)); ww -= 9; } n = 1; for (; 0 < ww; ww--) n *= 10; if (n != 1) subsec = mul(subsec, INT2FIX(n)); subsec = div(subsec, INT2FIX(1)); if (FIXNUM_P(subsec)) { (void)snprintf(s, endp - s, "%0*ld", precision, FIX2LONG(subsec)); s += precision; } else { VALUE args[2], result; args[0] = INT2FIX(precision); args[1] = subsec; result = rb_str_format(2, args, rb_str_new2("%0*d")); (void)strlcpy(s, StringValueCStr(result), endp - s); s += precision; } } continue; case 'M': /* minute, 00 - 59 */ v = range(0, tmx_min, 59); FMT('0', 2, "d", v); continue; case 'm': /* month, 01 - 12 */ v = range(1, tmx_mon, 12); FMT('0', 2, "d", v); continue; case 'n': /* same as \n */ FILL_PADDING(1); *s++ = '\n'; continue; case 't': /* same as \t */ FILL_PADDING(1); *s++ = '\t'; continue; case 'P': /* am or pm based on 12-hour clock */ case 'p': /* AM or PM based on 12-hour clock */ if ((*format == 'p' && (flags & BIT_OF(CHCASE))) || (*format == 'P' && !(flags & (BIT_OF(CHCASE) | BIT_OF(UPPER))))) { flags &= ~(BIT_OF(UPPER) | BIT_OF(CHCASE)); flags |= BIT_OF(LOWER); } v = range(0, tmx_hour, 23); if (v < 12) tp = ampm[0]; else tp = ampm[1]; i = 2; break; case 'Q': /* milliseconds since Unix epoch */ FMTV('0', 1, "d", tmx_msecs); continue; case 'R': STRFTIME("%H:%M"); continue; case 'r': STRFTIME("%I:%M:%S %p"); continue; case 'S': /* second, 00 - 59 */ v = range(0, tmx_sec, 59); FMT('0', 2, "d", v); continue; case 's': /* seconds since Unix epoch */ FMTV('0', 1, "d", tmx_secs); continue; case 'T': STRFTIME("%H:%M:%S"); continue; case 'U': /* week of year, Sunday is first day of week */ case 'W': /* week of year, Monday is first day of week */ v = range(0, (*format == 'U') ? tmx_wnum0 : tmx_wnum1, 53); FMT('0', 2, "d", v); continue; case 'u': /* weekday, Monday == 1, 1 - 7 */ v = range(1, tmx_cwday, 7); FMT('0', 1, "d", v); continue; case 'V': /* week of year according ISO 8601 */ v = range(1, tmx_cweek, 53); FMT('0', 2, "d", v); continue; case 'v': STRFTIME("%e-%b-%Y"); continue; case 'w': /* weekday, Sunday == 0, 0 - 6 */ v = range(0, tmx_wday, 6); FMT('0', 1, "d", v); continue; case 'X': /* appropriate time representation */ STRFTIME("%H:%M:%S"); continue; case 'x': /* appropriate date representation */ STRFTIME("%m/%d/%y"); continue; case 'Z': /* time zone name or abbreviation */ if (flags & BIT_OF(CHCASE)) { flags &= ~(BIT_OF(UPPER) | BIT_OF(CHCASE)); flags |= BIT_OF(LOWER); } { char *zone = tmx_zone; if (zone == NULL) tp = ""; else tp = zone; i = strlen(tp); } break; case 'z': /* offset from UTC */ { long off, aoff; int hl, hw; off = tmx_offset; aoff = off; if (aoff < 0) aoff = -off; if ((aoff / 3600) < 10) hl = 1; else hl = 2; hw = 2; if (flags & BIT_OF(LEFT) && hl == 1) hw = 1; switch (colons) { case 0: /* %z -> +hhmm */ precision = precision <= (3 + hw) ? hw : precision - 3; NEEDS(precision + 3); break; case 1: /* %:z -> +hh:mm */ precision = precision <= (4 + hw) ? hw : precision - 4; NEEDS(precision + 4); break; case 2: /* %::z -> +hh:mm:ss */ precision = precision <= (7 + hw) ? hw : precision - 7; NEEDS(precision + 7); break; case 3: /* %:::z -> +hh[:mm[:ss]] */ { if (aoff % 3600 == 0) { precision = precision <= (1 + hw) ? hw : precision - 1; NEEDS(precision + 3); } else if (aoff % 60 == 0) { precision = precision <= (4 + hw) ? hw : precision - 4; NEEDS(precision + 4); } else { precision = precision <= (7 + hw) ? hw : precision - 7; NEEDS(precision + 7); } } break; default: format--; goto unknown; } if (padding == ' ' && precision > hl) { i = snprintf(s, endp - s, "%*s", precision - hl, ""); precision = hl; if (i < 0) goto err; s += i; } if (off < 0) { off = -off; *s++ = '-'; } else { *s++ = '+'; } i = snprintf(s, endp - s, "%.*ld", precision, off / 3600); if (i < 0) goto err; s += i; off = off % 3600; if (colons == 3 && off == 0) continue; if (1 <= colons) *s++ = ':'; i = snprintf(s, endp - s, "%02d", (int)(off / 60)); if (i < 0) goto err; s += i; off = off % 60; if (colons == 3 && off == 0) continue; if (2 <= colons) { *s++ = ':'; i = snprintf(s, endp - s, "%02d", (int)off); if (i < 0) goto err; s += i; } } continue; case '+': STRFTIME("%a %b %e %H:%M:%S %Z %Y"); continue; case 'E': /* POSIX locale extensions, ignored for now */ flags |= BIT_OF(LOCALE_E); if (*(format + 1) && strchr("cCxXyY", *(format + 1))) goto again; goto unknown; case 'O': /* POSIX locale extensions, ignored for now */ flags |= BIT_OF(LOCALE_O); if (*(format + 1) && strchr("deHkIlmMSuUVwWy", *(format + 1))) goto again; goto unknown; case ':': flags |= BIT_OF(COLONS); { size_t l = strspn(format, ":"); format += l; if (*format == 'z') { colons = l; format--; goto again; } format -= l; } goto unknown; case '_': FLAG_FOUND(); padding = ' '; goto again; case '-': FLAG_FOUND(); flags |= BIT_OF(LEFT); goto again; case '^': FLAG_FOUND(); flags |= BIT_OF(UPPER); goto again; case '#': FLAG_FOUND(); flags |= BIT_OF(CHCASE); goto again; case '0': FLAG_FOUND(); padding = '0'; case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': { char *e; precision = (int)strtoul(format, &e, 10); format = e - 1; goto again; } case '%': FILL_PADDING(1); *s++ = '%'; continue; default: unknown: i = format - sp + 1; tp = sp; precision = -1; flags = 0; padding = 0; colons = 0; break; } if (i) { FILL_PADDING(i); memcpy(s, tp, i); switch (flags & (BIT_OF(UPPER) | BIT_OF(LOWER))) { case BIT_OF(UPPER): upcase(s, i); break; case BIT_OF(LOWER): downcase(s, i); break; } s += i; } } if (s >= endp) { goto err; } if (*format == '\0') { *s = '\0'; return (s - start); } return 0; } size_t date_strftime(char *s, size_t maxsize, const char *format, const struct tmx *tmx) { return date_strftime_with_tmx(s, maxsize, format, tmx); } /* Local variables: c-file-style: "ruby" End: */