view lib/strftime.c @ 582:3ec26150a8d6

(mon_week_ISO): New function to implement new %V format. (sun_week): Make TM parameter `const'. (mon_week): Likewise. (mon_week): Rewrite to correctly implement %W format..
author Jim Meyering <jim@meyering.net>
date Mon, 08 Apr 1996 04:53:54 +0000
parents 7be335c70443
children e54f6bf3d960
line wrap: on
line source

/* strftime - custom formatting of date and/or time
   Copyright (C) 1989, 1991, 1992 Free Software Foundation, Inc.

   This program is free software; you can redistribute it and/or modify
   it under the terms of the GNU General Public License as published by
   the Free Software Foundation; either version 2, or (at your option)
   any later version.

   This program is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   GNU General Public License for more details.

   You should have received a copy of the GNU General Public License
   along with this program; if not, write to the Free Software
   Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.  */

/* Note: this version of strftime lacks locale support,
   but it is standalone.

   Performs `%' substitutions similar to those in printf.  Except
   where noted, substituted fields have a fixed size; numeric fields are
   padded if necessary.  Padding is with zeros by default; for fields
   that display a single number, padding can be changed or inhibited by
   following the `%' with one of the modifiers described below.  Unknown
   field specifiers are copied as normal characters.  All other
   characters are copied to the output without change.

   Supports a superset of the ANSI C field specifiers.

   Literal character fields:
   %	%
   n	newline
   t	tab

   Numeric modifiers (a nonstandard extension):
   -	do not pad the field
   _	pad the field with spaces

   Time fields:
   %H	hour (00..23)
   %I	hour (01..12)
   %k	hour ( 0..23)
   %l	hour ( 1..12)
   %M	minute (00..59)
   %p	locale's AM or PM
   %r	time, 12-hour (hh:mm:ss [AP]M)
   %R	time, 24-hour (hh:mm)
   %s	time in seconds since 00:00:00, Jan 1, 1970 (a nonstandard extension)
   %S	second (00..61)
   %T	time, 24-hour (hh:mm:ss)
   %X	locale's time representation (%H:%M:%S)
   %z   RFC-822 style numeric timezone (-0500) (a nonstandard extension)
   %Z	time zone (EDT), or nothing if no time zone is determinable

   Date fields:
   %a	locale's abbreviated weekday name (Sun..Sat)
   %A	locale's full weekday name, variable length (Sunday..Saturday)
   %b	locale's abbreviated month name (Jan..Dec)
   %B	locale's full month name, variable length (January..December)
   %c	locale's date and time (Sat Nov 04 12:02:33 EST 1989)
   %C	century (00..99)
   %d	day of month (01..31)
   %e	day of month ( 1..31)
   %D	date (mm/dd/yy)
   %h	same as %b
   %j	day of year (001..366)
   %m	month (01..12)
   %U	week number of year with Sunday as first day of week (00..53)
   %V	FIXME
   %w	day of week (0..6)
   %W	week number of year with Monday as first day of week (00..53)
   %x	locale's date representation (mm/dd/yy)
   %y	last two digits of year (00..99)
   %Y	year (1970...)

   David MacKenzie <djm@gnu.ai.mit.edu> */

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

#include <stdio.h>
#include <sys/types.h>
#if defined(TM_IN_SYS_TIME) || (!defined(HAVE_TM_ZONE) && !defined(HAVE_TZNAME))
#include <sys/time.h>
#else
#include <time.h>
#endif

#ifndef STDC_HEADERS
time_t mktime ();
#endif

#if defined(HAVE_TZNAME)
extern char *tzname[2];
#endif

/* Types of padding for numbers in date and time. */
enum padding
{
  none, blank, zero
};

static char const* const days[] =
{
  "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"
};

static char const * const months[] =
{
  "January", "February", "March", "April", "May", "June",
  "July", "August", "September", "October", "November", "December"
};

/* Add character C to STRING and increment LENGTH,
   unless LENGTH would exceed MAX. */

#define add_char(c)							\
  do									\
    {									\
      if (length + 1 <= max)						\
	string[length++] = (c);						\
    }									\
  while (0)

/* Add a 2 digit number to STRING, padding if specified.
   Return the number of characters added, up to MAX. */

static int
add_num2 (string, num, max, pad)
     char *string;
     int num;
     int max;
     enum padding pad;
{
  int top = num / 10;
  int length = 0;

  if (top == 0 && pad == blank)
    add_char (' ');
  else if (top != 0 || pad == zero)
    add_char (top + '0');
  add_char (num % 10 + '0');
  return length;
}

/* Add a 3 digit number to STRING, padding if specified.
   Return the number of characters added, up to MAX. */

static int
add_num3 (string, num, max, pad)
     char *string;
     int num;
     int max;
     enum padding pad;
{
  int top = num / 100;
  int mid = (num - top * 100) / 10;
  int length = 0;

  if (top == 0 && pad == blank)
    add_char (' ');
  else if (top != 0 || pad == zero)
    add_char (top + '0');
  if (mid == 0 && top == 0 && pad == blank)
    add_char (' ');
  else if (mid != 0 || top != 0 || pad == zero)
    add_char (mid + '0');
  add_char (num % 10 + '0');
  return length;
}

/* Like strncpy except return the number of characters copied. */

static int
add_str (to, from, max)
     char *to;
     const char *from;
     int max;
{
  int i;

  for (i = 0; from[i] && i <= max; ++i)
    to[i] = from[i];
  return i;
}

static int
add_num_time_t (string, max, num)
     char *string;
     int max;
     time_t num;
{
  /* This buffer is large enough to hold the character representation
     (including the trailing NUL) of any unsigned decimal quantity
     whose binary representation fits in 128 bits.  */
  char buf[40];
  int length;

  if (sizeof (num) > 16)
    abort ();
  sprintf (buf, "%lu", (unsigned long) num);
  length = add_str (string, buf, max);
  return length;
}

/* Convert MINUTES_EAST into a string suitable for use as the RFC-822
   timezone indicator.  Write no more than MAX bytes into STRING.
    Return the number of bytes written into STRING.  */

static int
add_num_tz (string, max, minutes_east)
     char *string;
     int max;
     int minutes_east;
{
  int length;

  if (max < 1)
    return 0;

  if (minutes_east < 0)
    {
      *string = '-';
      minutes_east = -minutes_east;
    }
  else
    *string = '+';

  length = 1 + add_num2 (&string[1], (minutes_east / 60) % 24, max - 1, zero);
  length += add_num2 (&string[length], minutes_east % 60, max - length, zero);

  return length;
}

/* Implement %U.  Return the week in the year of the time in TM,
   with the weeks starting on Sundays.  */

static int
sun_week (tm)
     const struct tm *tm;
{
  int dl;

  /* %U Week of the year (Sunday as the first day of the week) as a decimal
     number [00-53].  All days in a new year preceding the first Sunday are
     considered to be in week 0.  */

  dl = tm->tm_yday - tm->tm_wday;
  return dl < 0 ? 0 : dl / 7 + 1;
}

/* Implement %V.  Similar to mon_week (%W), but there is no 0'th week --
   they're numbered [01-53].  And if the week containing January 1 has
   four or more days in the new year, then it is considered week 1;
   otherwise, it is week 53 of the previous year, and the next week is
   week 1. (See the ISO 8601: 1988 standard.)  */

static int
mon_week_ISO (tm)
     const struct tm *tm;
{
  int dl, n_days_before_first_monday;
  int week_num;

  n_days_before_first_monday = (tm->tm_yday + 7 - tm->tm_wday + 1) % 7;
  dl = tm->tm_yday - n_days_before_first_monday;
  week_num = dl < 0 ? 0 : dl / 7 + 1;
  if (n_days_before_first_monday >= 4)
    {
      week_num = (week_num + 1) % 54;
      if (week_num == 0)
	week_num = 1;
    }
  if (week_num == 0)
    week_num = 53;

  return week_num;
}

/* Implement %W.  Return the week in the year of the time in TM,
   with the weeks starting on Mondays.  */

static int
mon_week (tm)
     const struct tm *tm;
{
  int dl, n_days_before_first_monday;

  n_days_before_first_monday = (tm->tm_yday + 7 - tm->tm_wday + 1) % 7;
  dl = tm->tm_yday - n_days_before_first_monday;
  return dl < 0 ? 0 : dl / 7 + 1;
}

#if !defined(HAVE_TM_ZONE) && !defined(HAVE_TZNAME)
char *
zone_name (tp)
     struct tm *tp;
{
  char *timezone ();
  struct timeval tv;
  struct timezone tz;

  gettimeofday (&tv, &tz);
  return timezone (tz.tz_minuteswest, tp->tm_isdst);
}
#endif

/* Format the time given in TM according to FORMAT, and put the
   results in STRING.
   Return the number of characters (not including terminating null)
   that were put into STRING, or 0 if the length would have
   exceeded MAX. */

size_t
strftime (string, max, format, tm)
     char *string;
     size_t max;
     const char *format;
     const struct tm *tm;
{
  enum padding pad;		/* Type of padding to apply. */
  size_t length = 0;		/* Characters put in STRING so far. */

  for (; *format && length < max; ++format)
    {
      if (*format != '%')
	add_char (*format);
      else
	{
	  ++format;
	  /* Modifiers: */
	  if (*format == '-')
	    {
	      pad = none;
	      ++format;
	    }
	  else if (*format == '_')
	    {
	      pad = blank;
	      ++format;
	    }
	  else
	    pad = zero;

	  switch (*format)
	    {
	      /* Literal character fields: */
	    case 0:
	    case '%':
	      add_char ('%');
	      break;
	    case 'n':
	      add_char ('\n');
	      break;
	    case 't':
	      add_char ('\t');
	      break;
	    default:
	      add_char (*format);
	      break;

	      /* Time fields: */
	    case 'H':
	    case 'k':
	      length +=
		add_num2 (&string[length], tm->tm_hour, max - length,
			  *format == 'H' ? pad : blank);
	      break;
	    case 'I':
	    case 'l':
	      {
		int hour12;

		if (tm->tm_hour == 0)
		  hour12 = 12;
		else if (tm->tm_hour > 12)
		  hour12 = tm->tm_hour - 12;
		else
		  hour12 = tm->tm_hour;
		length +=
		  add_num2 (&string[length], hour12, max - length,
			    *format == 'I' ? pad : blank);
	      }
	      break;
	    case 'M':
	      length +=
		add_num2 (&string[length], tm->tm_min, max - length, pad);
	      break;
	    case 'p':
	      if (tm->tm_hour < 12)
		add_char ('A');
	      else
		add_char ('P');
	      add_char ('M');
	      break;
	    case 'r':
	      length +=
		strftime (&string[length], max - length, "%I:%M:%S %p", tm);
	      break;
	    case 'R':
	      length +=
		strftime (&string[length], max - length, "%H:%M", tm);
	      break;

	    case 's':
	      {
		struct tm writable_tm;
		writable_tm = *tm;
		length += add_num_time_t (&string[length], max - length,
					  mktime (&writable_tm));
	      }
	      break;

	    case 'S':
	      length +=
		add_num2 (&string[length], tm->tm_sec, max - length, pad);
	      break;
	    case 'T':
	      length +=
		strftime (&string[length], max - length, "%H:%M:%S", tm);
	      break;
	    case 'X':
	      length +=
		strftime (&string[length], max - length, "%H:%M:%S", tm);
	      break;
	    case 'z':
	      {
		time_t t;
		struct tm tml, tmg;
		int diff;

		tml = *tm;
		t = mktime (&tml);
		tml = *localtime (&t); /* Canonicalize the local time */
		tmg = *gmtime (&t);

		/* Compute the difference */

		diff = tml.tm_min - tmg.tm_min;
		diff += 60 * (tml.tm_hour - tmg.tm_hour);

		if (tml.tm_mon != tmg.tm_mon)
		  {
		    /* We assume no timezone differs from UTC by more than
		       +- 23 hours.  This should be safe. */
		    if (tmg.tm_mday == 1)
		      tml.tm_mday = 0;
		    else /* tml.tm_mday == 1 */
		      tmg.tm_mday = 0;
		  }

		diff += 1440 * (tml.tm_mday - tmg.tm_mday);

		length += add_num_tz (&string[length], max - length, diff);
	      }
	      break;
	    case 'Z':
#ifdef HAVE_TM_ZONE
	      length += add_str (&string[length], tm->tm_zone, max - length);
#else
#ifdef HAVE_TZNAME
	      if (tm->tm_isdst && tzname[1] && *tzname[1])
		length += add_str (&string[length], tzname[1], max - length);
	      else
		length += add_str (&string[length], tzname[0], max - length);
#else
	      length += add_str (&string[length], zone_name (tm), max - length);
#endif
#endif
	      break;

	      /* Date fields: */
	    case 'a':
	      add_char (days[tm->tm_wday][0]);
	      add_char (days[tm->tm_wday][1]);
	      add_char (days[tm->tm_wday][2]);
	      break;
	    case 'A':
	      length +=
		add_str (&string[length], days[tm->tm_wday], max - length);
	      break;
	    case 'b':
	    case 'h':
	      add_char (months[tm->tm_mon][0]);
	      add_char (months[tm->tm_mon][1]);
	      add_char (months[tm->tm_mon][2]);
	      break;
	    case 'B':
	      length +=
		add_str (&string[length], months[tm->tm_mon], max - length);
	      break;
	    case 'c':
	      length +=
		strftime (&string[length], max - length,
			  "%a %b %d %H:%M:%S %Z %Y", tm);
	      break;
	    case 'C':
	      length +=
		add_num2 (&string[length], (tm->tm_year + 1900) / 100,
			  max - length, pad);
	      break;
	    case 'd':
	      length +=
		add_num2 (&string[length], tm->tm_mday, max - length, pad);
	      break;
	    case 'e':
	      length +=
		add_num2 (&string[length], tm->tm_mday, max - length, blank);
	      break;
	    case 'D':
	      length +=
		strftime (&string[length], max - length, "%m/%d/%y", tm);
	      break;
	    case 'j':
	      length +=
		add_num3 (&string[length], tm->tm_yday + 1, max - length, pad);
	      break;
	    case 'm':
	      length +=
		add_num2 (&string[length], tm->tm_mon + 1, max - length, pad);
	      break;
	    case 'U':
	      length +=
		add_num2 (&string[length], sun_week (tm), max - length, pad);
	      break;
	    case 'V':
	      length += add_num2 (&string[length], mon_week_ISO (tm),
				  max - length, pad);
	      break;
	    case 'w':
	      add_char (tm->tm_wday + '0');
	      break;
	    case 'W':
	      length +=
		add_num2 (&string[length], mon_week (tm), max - length, pad);
	      break;
	    case 'x':
	      length +=
		strftime (&string[length], max - length, "%m/%d/%y", tm);
	      break;
	    case 'y':
	      length +=
		add_num2 (&string[length], tm->tm_year % 100,
			  max - length, pad);
	      break;
	    case 'Y':
	      add_char ((tm->tm_year + 1900) / 1000 + '0');
	      length +=
		add_num3 (&string[length],
			  (1900 + tm->tm_year) % 1000, max - length, zero);
	      break;
	    }
	}
    }
  add_char (0);
  return length - 1;
}