/* Copyright 1996 Acorn Computers Ltd * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /* locale.c: ANSI draft (X3J11 Oct 86) library header, section 4.3 */ /* Copyright (C) Codemist Ltd., 1988 */ /* version 0.01 */ #include <locale.h> #include <stddef.h> #include <stdio.h> #include <time.h> #include <string.h> #include <stdlib.h> /* multibyte characters & strings */ #include <limits.h> /* for CHAR_MAX */ #include "hostsys.h" #include "kernel.h" #include "territory.h" #include "swis.h" /* #define LC_COLLATE 1 #define LC_CTYPE 2 #define LC_MONETARY 4 #define LC_NUMERIC 8 #define LC_TIME 16 #define LC_ALL 31 */ /* Array indices corresponding to the LC macros above */ #define N_LC_COLLATE 0 #define N_LC_CTYPE 1 #define N_LC_MONETARY 2 #define N_LC_NUMERIC 3 #define N_LC_TIME 4 #define N_LC_MAX 5 extern int _sprintf_lf(char *buff, const char *fmt, ...); extern int __locales[N_LC_MAX]; int __locales[N_LC_MAX] = {0, 0, 0, 0, 0}; /* lc initialised to C for default */ static struct lconv lc = {".", "", "", "", "", "", "", "", "", "", CHAR_MAX,CHAR_MAX,CHAR_MAX,CHAR_MAX,CHAR_MAX,CHAR_MAX,CHAR_MAX,CHAR_MAX}; /* Tables used by strftime() */ static char *abbrweek[] = { "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" }; static char *fullweek[] = { "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday" }; static char *abbrmonth[] = { "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" }; static char *fullmonth[] = { "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December" }; static char *ampmname[] = { "AM", "PM" }; extern void _set_ctype(int territory); extern void _set_strcoll(int territory); static void setlocales(int category, int *values) { int j; for (j = 0; category != 0; category >>= 1, ++j) { if (category & 1) __locales[j] = values[j]; } } static int getsymbol(int territory, int idx, int def) { _kernel_swi_regs r; if (!territory) return def; r.r[0] = territory; r.r[1] = idx; if (!_kernel_swi(Territory_ReadSymbols, &r, &r)) return r.r[0]; return def; } static void _set_numeric(int territory) { decimal_point = (char *)getsymbol(territory, 0, (int)"."); } static void setlconv(int category, int *values) { int territory; if (category & LC_MONETARY) { territory = values[N_LC_MONETARY]; lc.int_curr_symbol = (char *)getsymbol(territory, TERRITORY_INT_CURR_SYMBOL, (int)""); lc.currency_symbol = (char *)getsymbol(territory, TERRITORY_CURRENCY_SYMBOL, (int)""); lc.mon_decimal_point = (char *)getsymbol(territory, TERRITORY_MON_DECIMAL_POINT, (int)""); lc.mon_thousands_sep = (char *)getsymbol(territory, TERRITORY_MON_THOUSANDS_SEP, (int)""); lc.mon_grouping = (char *)getsymbol(territory, TERRITORY_MON_GROUPING, (int)""); lc.positive_sign = (char *)getsymbol(territory, TERRITORY_POSITIVE_SIGN, (int)""); lc.negative_sign = (char *)getsymbol(territory, TERRITORY_NEGATIVE_SIGN, (int)""); lc.int_frac_digits = getsymbol(territory, TERRITORY_INT_FRAC_DIGITS, CHAR_MAX); lc.frac_digits = getsymbol(territory, TERRITORY_FRAC_DIGITS, CHAR_MAX); lc.p_cs_precedes = getsymbol(territory, TERRITORY_P_CS_PRECEDES, CHAR_MAX); lc.p_sep_by_space = getsymbol(territory, TERRITORY_P_SEP_BY_SPACE, CHAR_MAX); lc.n_cs_precedes = getsymbol(territory, TERRITORY_N_CS_PRECEDES, CHAR_MAX); lc.n_sep_by_space = getsymbol(territory, TERRITORY_N_SEP_BY_SPACE, CHAR_MAX); lc.p_sign_posn = getsymbol(territory, TERRITORY_P_SIGN_POSN, CHAR_MAX); lc.n_sign_posn = getsymbol(territory, TERRITORY_N_SIGN_POSN, CHAR_MAX); } if (category & LC_NUMERIC) { territory = values[N_LC_NUMERIC]; lc.decimal_point = (char *)getsymbol(territory, TERRITORY_DECIMAL_POINT, (int)"."); lc.thousands_sep = (char *)getsymbol(territory, TERRITORY_THOUSANDS_SEP, (int)""); lc.grouping = (char *)getsymbol(territory, TERRITORY_GROUPING, (int)""); } } #define LC_STR_SIZE 40 char *setlocale(int category, const char *locale) { static char lc_str[LC_STR_SIZE]; int tmp_locales[N_LC_MAX] = {0, 0, 0, 0, 0}; _kernel_swi_regs r; char *s; int i, n, tz; /* I expect the category to be a bit-map - complain if out of range */ if (((unsigned)category > LC_ALL) || (category == 0)) /* no can do... */ return NULL; if (locale == NULL) { /* get locale */ _sprintf_lf(lc_str, "=%d,%d,%d,%d,%d", __locales[0], __locales[1], __locales[2], __locales[3], __locales[4]); return lc_str; } else { /* set locale */ if (strcmp(locale, "ISO8859-1") == 0) locale = "UK"; if (*locale == '=') { /* ISO9899 7.11.1.1 Parse the string as given by get locale */ s = (char *)(locale + 1); for (i = 0; i < N_LC_MAX; i++) { n = 0; while (*s >= '0' && *s <= '9') { n = n * 10 + *s - '0'; s++; } if (*s == ',') s++; tmp_locales[i] = n; } } else { if (*locale == 0 || strcmp(locale, "C") == 0) { /* ISO9899 7.11.1.1 Use "" for current locale, "C" for minimal locale */ n = 0; tz = 0; if (!*locale && (_kernel_swi(Territory_Number, &r, &r) == NULL)) { n = r.r[0]; r.r[1] = (int)lc_str; r.r[2] = LC_STR_SIZE; if (_kernel_swi(Territory_NumberToName, &r, &r) == NULL) locale = lc_str; } } else { /* Platform specific, permit the territory name and (optional) standard timezone */ strncpy(lc_str, locale, LC_STR_SIZE - 1); s = strchr(lc_str, '/'); /* eg. "USA/PST" */ if (s != NULL) *s = 0; r.r[0] = TERRITORY_UK; /* Names must be in english */ r.r[1] = (int)lc_str; if ((_kernel_swi(Territory_NameToNumber, &r, &r) != NULL) || (r.r[0] == 0)) return NULL; /* Don't know that name */ n = r.r[0]; tz = 0; if (_kernel_swi(Territory_WriteDirection, &r, &r) != NULL) return NULL; /* Check it's loaded (avoids Territory_Exists Z flag faff) */ if (s != NULL) { locale = lc_str; /* eg. "USA" */ s++; /* eg. "PST" */ if (*s) { /* Null timezone taken as 0th */ while (1) { r.r[0] = n; r.r[1] = tz; r.r[4] = (int)TERRITORY_TZ_API_EXT; if (_kernel_swi(Territory_ReadTimeZones, &r, &r) != NULL) return NULL; /* No more timezones to match */ if (strcmp((char *)r.r[0], s) == 0) break; /* Exact match */ if (r.r[4]) break; /* Extended API not supported, use 0th */ tz++; } } } } for (i = 0; i < N_LC_MAX; i++) { if (i == N_LC_TIME) tmp_locales[i] = TERRITORY_ENCODE(n, tz); /* Packed format */ else tmp_locales[i] = n; } } setlocales(category, tmp_locales); setlconv(category, tmp_locales); if (category & LC_CTYPE) _set_ctype(tmp_locales[N_LC_CTYPE]); if (category & LC_COLLATE) _set_strcoll(tmp_locales[N_LC_COLLATE]); if (category & LC_NUMERIC) _set_numeric(tmp_locales[N_LC_NUMERIC]); } return (char *)locale; } struct lconv *localeconv(void) { return &lc; } static int findweek(int yday, int startday, int today) { int days_into_this_week = today - startday; int last_weekstart; if (days_into_this_week < 0) days_into_this_week += 7; last_weekstart = yday - days_into_this_week; if (last_weekstart <= 0) return 0; return last_weekstart/7 + 1; } #define CDT_BUFFSIZE 256 static char *getterritorytimeinfo(int territory, const struct tm *tt, char *fmt, char *buff, int swi) { _kernel_swi_regs r; int tm_block[7]; long long utc_block; /* The tm struct came from either gmtime() or localtime() and therefore already */ /* has any timezone and daylight saving applied to it as implied by the choice of */ /* the 2 functions called. Therefore it needs converting as though it's UTC */ /* already. Note that Territory_ConvertOrdinalsToTime applies its own correction */ /* based on the active territory (not necessarily the same as the locale set for */ /* our client) and since there isn't a UTC equivalent of that SWI until */ /* Territory_ConvertTimeFormats comes along (which might not be available on the */ /* host OS) we must do the opposite correction to the calculated time. Sigh. */ tm_block[0] = 0; tm_block[1] = tt->tm_sec; tm_block[2] = tt->tm_min; tm_block[3] = tt->tm_hour; tm_block[4] = tt->tm_mday; tm_block[5] = tt->tm_mon + 1; tm_block[6] = tt->tm_year + 1900; r.r[0] = TERRITORY_UK; r.r[1] = (int)&utc_block; r.r[2] = (int)tm_block; if (_kernel_swi(Territory_ConvertOrdinalsToTime, &r, &r) != NULL) return "???"; if (_kernel_swi(Territory_ReadCurrentTimeZone, &r, &r) != NULL) return "???"; utc_block = utc_block + r.r[1]; r.r[0] = TERRITORY_EXTRACT(territory); r.r[1] = (int)&utc_block; r.r[2] = (int)buff; r.r[3] = CDT_BUFFSIZE | (1<<30) | (1<<31); /* No DST, R5 cs offset */ r.r[4] = (int)fmt; r.r[5] = 0; if (_kernel_swi(swi, &r, &r) != NULL) return "???"; return buff; } static char *gettimeinfo(int territory, const struct tm *tt, char *fmt, char *buff) { return getterritorytimeinfo(territory, tt, fmt, buff, Territory_ConvertDateAndTime); } static char *gettimedate(int territory, const struct tm *tt, char *buff, int swi) { return getterritorytimeinfo(territory, tt, NULL, buff, swi); } static char *gettimezone(int territory, const struct tm *tt, char *buff, int numeric) { _kernel_swi_regs r; if (tt->tm_isdst < 0) return ""; /* Undetermined */ r.r[0] = TERRITORY_EXTRACT(territory); r.r[1] = TERRITORY_TZ_EXTRACT(territory); r.r[4] = (int)TERRITORY_TZ_API_EXT; if (_kernel_swi(Territory_ReadTimeZones, &r, &r) != NULL) return ""; /* Undetermined */ if (numeric) { int offset = tt->tm_isdst ? r.r[3] : r.r[2]; if (offset < 0) offset = -offset, buff[0] = '-'; else buff[0] = '+'; offset = (offset + 3000) / 6000; /* centiseconds -> minutes */ sprintf(buff+1, "%.2d%.2d", offset / 60, offset % 60); } else { strcpy(buff, tt->tm_isdst ? (char *)r.r[1] : (char *)r.r[0]); } return buff; } static int getdaysinyear(int year) { if (year % 4 != 0) return 365; if (year % 100 != 0) return 366; if (year % 400 != 0) return 365; return 366; } static void getiso8601week(char *buff, int spec, int year, int wday, int yday) { int start_of_week, week; if (--wday < 0) wday += 7; /* convert from Sun = 0 to Mon = 0 */ start_of_week = yday - wday; /* day number (-6 to 365) of start of this week */ do { week = (start_of_week+7+3) / 7; /* basic week number (0-53) */ if (week == 0) { /* This week belongs to last year - go round again */ start_of_week += getdaysinyear(--year); } else if (week == 53 && start_of_week >= getdaysinyear(year)-3) { /* <=3 days of week 53 fall in this year, so we treat it as week 1 of next year */ week = 1; ++year; } } while (week == 0); switch (spec) { case 'g': sprintf(buff, "%.2d", year % 100); break; case 'G': sprintf(buff, "%d", year); break; case 'V': sprintf(buff, "%.2d", week); break; } } size_t strftime(char *s, size_t maxsize, const char *fmt, const struct tm *tt) { int p = 0, c; char *ss, buff[CDT_BUFFSIZE]; int territory; if (maxsize==0) return 0; territory = __locales[N_LC_TIME]; #define push(ch) { s[p++]=(ch); if (p>=maxsize) return 0; } for (;;) { switch (c = *fmt++) { case 0: s[p] = 0; return p; default: push(c); continue; case '%': ss = buff; c = *fmt++; if (c == 'E' || c == 'O') /* Ignore C99 modifiers */ c = *fmt++; switch (c) { default: /* Unknown directive - leave uninterpreted */ push('%'); /* NB undefined behaviour according to ANSI */ fmt--; continue; case 'a': if (territory) ss = gettimeinfo(territory, tt, "%W3", buff); else ss = abbrweek[tt->tm_wday]; break; case 'A': if (territory) ss = gettimeinfo(territory, tt, "%WE", buff); else ss = fullweek[tt->tm_wday]; break; case 'b': case 'h': if (territory) ss = gettimeinfo(territory, tt, "%M3", buff); else ss = abbrmonth[tt->tm_mon]; break; case 'B': if (territory) ss = gettimeinfo(territory, tt, "%MO", buff); else ss = fullmonth[tt->tm_mon]; break; case 'c': if (territory) ss = gettimedate(territory, tt, ss, Territory_ConvertStandardDateAndTime); else /* Format for "C" locale changed as per C99 "%a %b %e %T %Y" */ sprintf(ss, "%s %s %2d %.2d:%.2d:%.2d %d", tt->tm_wday < 7U ? abbrweek[tt->tm_wday] : "???", abbrmonth[tt->tm_mon], tt->tm_mday, tt->tm_hour, tt->tm_min, tt->tm_sec, tt->tm_year + 1900); break; case 'C': sprintf(ss, "%.2d", (tt->tm_year + 1900) / 100); break; case 'd': sprintf(ss, "%.2d", tt->tm_mday); break; case 'D': sprintf(ss, "%.2d/%.2d/%.2d", tt->tm_mon + 1, tt->tm_mday, tt->tm_year % 100); break; case 'e': sprintf(ss, "%2d", tt->tm_mday); break; case 'F': sprintf(ss, "%d-%.2d-%2.d", tt->tm_year + 1900, tt->tm_mon + 1, tt->tm_mday); break; case 'g': case 'G': case 'V': getiso8601week(ss, c, tt->tm_year + 1900, tt->tm_wday, tt->tm_yday); break; case 'H': sprintf(ss, "%.2d", tt->tm_hour); break; case 'I': sprintf(ss, "%.2d", (tt->tm_hour + 11)%12 + 1); break; case 'j': sprintf(ss, "%.3d", tt->tm_yday + 1); break; case 'm': sprintf(ss, "%.2d", tt->tm_mon + 1); break; case 'M': sprintf(ss, "%.2d", tt->tm_min); break; case 'n': strcpy(ss, "\n"); break; case 'p': /* I am worried here re 12.00 AM/PM and times near same. */ if (territory) ss = gettimeinfo(territory, tt, "%AM", buff); else ss = ampmname[tt->tm_hour >= 12]; break; case 'r': if (territory) ss = gettimeinfo(territory, tt, "%12:%MI:%SE %AM", buff); else sprintf(ss, "%.2d:%.2d:%.2d %s", (tt->tm_hour + 11) % 12 + 1, tt->tm_min, tt->tm_sec, ampmname[tt->tm_hour >= 12]); break; case 'R': sprintf(ss, "%.2d:%.2d", tt->tm_hour, tt->tm_min); break; case 'S': sprintf(ss, "%.2d", tt->tm_sec); break; case 't': strcpy(ss, "\t"); break; case 'T': sprintf(ss, "%.2d:%.2d:%.2d", tt->tm_hour, tt->tm_min, tt->tm_sec); break; case 'u': sprintf(ss, "%.1d", (tt->tm_wday + 6)%7 + 1); break; case 'U': sprintf(ss, "%.2d", findweek(tt->tm_yday, 0, tt->tm_wday)); break; case 'w': sprintf(ss, "%.1d", tt->tm_wday); break; case 'W': sprintf(ss, "%.2d", findweek(tt->tm_yday, 1, tt->tm_wday)); break; case 'x': if (territory) ss = gettimedate(territory, tt, ss, Territory_ConvertStandardDate); else /* Format for "C" locale changed as per C99 */ sprintf(ss, "%.2d/%.2d/%.2d", tt->tm_mon + 1, tt->tm_mday, tt->tm_year % 100); break; case 'X': if (territory) ss = gettimedate(territory, tt, ss, Territory_ConvertStandardTime); else sprintf(ss, "%.2d:%.2d:%.2d", tt->tm_hour, tt->tm_min, tt->tm_sec); break; case 'y': sprintf(ss, "%.2d", tt->tm_year % 100); break; case 'Y': sprintf(ss, "%d", tt->tm_year + 1900); break; case 'z': case 'Z': if (territory) ss = gettimezone(territory, tt, buff, c == 'z'); else ss = ""; break; case '%': push('%'); continue; } while ((c = *ss++) != 0) push(c); continue; } #undef push } } #define STATE_DEPENDENT_ENCODINGS 0 int mblen(const char *s, size_t n) { if (s == 0) return STATE_DEPENDENT_ENCODINGS; /* @@@ ANSI ambiguity: if n=0 and *s=0 then return 0 or -1? */ /* @@@ LDS: for consistency with mbtowc, return -1 */ if (n == 0) return -1; if (*s == 0) return 0; return 1; } int mbtowc(wchar_t *pwc, const char *s, size_t n) { if (s == 0) return STATE_DEPENDENT_ENCODINGS; /* @@@ ANSI ambiguity: if n=0 and *s=0 then return 0 or -1? */ /* @@@ LDS At most n chars of s are examined, ergo must return -1. */ if (n == 0) return -1; else { wchar_t wc = *(unsigned char *)s; if (pwc) *pwc = wc; return (wc != 0); } } int wctomb(char *s, wchar_t w) { if (s == 0) return STATE_DEPENDENT_ENCODINGS; /* @@@ ANSI ambiguity: what return (and setting for s) if w == 0? */ /* @@@ LDS The CVS suggests return #chars stored; I agree this is rational. */ if ((unsigned)w > (unsigned char)-1) return -1; *s = w; return 1; } size_t mbstowcs(wchar_t *pwcs, const char *s, size_t n) { /* @@@ ANSI ambiguity: if n=0 then is *s read? */ size_t r = 0; for (; n != 0; n--) { if ((pwcs[r] = ((unsigned char *)s)[r]) == 0) return r; r++; } return r; } size_t wcstombs(char *s, const wchar_t *pwcs, size_t n) { /* @@@ ANSI ambiguity: if n=0 then is *pwcs read? Also invalidity check? */ size_t r = 0; for (; n != 0; n--) { wchar_t w = pwcs[r]; if ((unsigned)w > (unsigned char)-1) return (size_t)-1; if ((s[r] = w) == 0) return r; r++; } return r; } /* end of locale.c */