summaryrefslogtreecommitdiff
path: root/libshouldbeinlibc/timefmt.c
blob: cef72e0264ef06256f6829344a31882edd5a7e12 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
/* Routines for formatting time

   Copyright (C) 1995, 1996 Free Software Foundation, Inc.

   Written by Miles Bader <miles@gnu.ai.mit.edu>

   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. */

#include <stdio.h>
#include <string.h>
#include <sys/time.h>
#include <time.h>

#include "timefmt.h"

#define SECOND 	1
#define MINUTE	60
#define HOUR	(60*MINUTE)
#define DAY	(24*HOUR)
#define WEEK 	(7*DAY)
#define MONTH   (31*DAY)	/* Not strictly accurate, but oh well.  */
#define YEAR    (365*DAY)	/* ditto */

/* Returns the number of digits in the integer N.  */
static unsigned
int_len (unsigned n)
{
  unsigned len = 1;
  while (n >= 10)
    {
      n /= 10;
      len++;
    }
  return len;
}

/* Returns TV1 divided by TV2.  */
static unsigned
tv_div (struct timeval *tv1, struct timeval *tv2)
{
  return 
    tv2->tv_sec
      ? tv1->tv_sec / tv2->tv_sec
	: (tv1->tv_usec / tv2->tv_usec
	   + (tv1->tv_sec ? tv1->tv_sec * 1000000 / tv2->tv_usec : 0));
}

/* Returns true if TV is zero.  */
static inline int
tv_is_zero (struct timeval *tv)
{
  return tv->tv_sec == 0 && tv->tv_usec == 0;
}

/* Returns true if TV1 >= TV2.  */
static inline int
tv_is_ge (struct timeval *tv1, struct timeval *tv2)
{
  return
    tv1->tv_sec > tv2->tv_sec
      || (tv1->tv_sec == tv2->tv_sec && tv1->tv_usec >= tv2->tv_usec);
}

/* Format into BUF & BUF_LEN the time interval represented by TV, trying to
   make the result less than WIDTH characters wide.  The number of characters
   used is returned.  */
size_t
fmt_named_interval (struct timeval *tv, size_t width,
		    char *buf, size_t buf_len)
{
  struct tscale
    {
      struct timeval thresh;	/* Minimum time to use this scale. */
      struct timeval unit;	/* Unit this scale is based on.  */
      struct timeval frac_thresh; /* If a emitting a single digit of precision
				     will cause at least this much error, also
				     emit a single fraction digit.  */
      char *sfxs[5];		/* Names to use, in descending length. */
    }
  time_scales[] =
  {
    {{2*YEAR, 0},  {YEAR, 0},  {MONTH, 0},{" years", "years", "yrs", "y", 0 }},
    {{3*MONTH, 0}, {MONTH, 0}, {WEEK, 0}, {" months","months","mo",       0 }},
    {{2*WEEK, 0},  {WEEK, 0},  {DAY, 0},  {" weeks", "weeks", "wks", "w", 0 }},
    {{2*DAY, 0},   {DAY, 0},   {HOUR, 0}, {" days",  "days",  "dys", "d", 0 }},
    {{2*HOUR, 0},  {HOUR, 0},  {MINUTE, 0},{" hours","hours", "hrs", "h", 0 }},
    {{2*MINUTE, 0},{MINUTE, 0},{1, 0},    {" minutes","min",  "mi",  "m", 0 }},
    {{1, 100000},  {1, 0},     {0, 100000},{" seconds", "sec", "s", 0 }},
    {{1, 0},       {1, 0},     {0, 0},    {" second", "sec", "s", 0 }},
    {{0, 1100},    {0, 1000},  {0, 100},  {" milliseconds", "ms", 0 }},
    {{0, 1000},    {0, 1000},  {0, 0},    {" millisecond", "ms", 0 }},
    {{0, 2},       {0, 1},     {0, 0},    {" microseconds", "us", 0 }},
    {{0, 1},       {0, 1},     {0, 0},    {" microsecond", "us", 0 }},
    {{0, 0} }
  };
  struct tscale *ts;

  if (width <= 0 || width >= buf_len)
    width = buf_len - 1;

  for (ts = time_scales; !tv_is_zero (&ts->thresh); ts++)
    if (tv_is_ge (tv, &ts->thresh))
      {
	char **sfx;
	struct timeval *u = &ts->unit;
	unsigned num = tv_div (tv, u);
	unsigned frac = 0;
	unsigned num_len = int_len (num);

	if (num < 10
	    && !tv_is_zero (&ts->frac_thresh)
	    && tv_is_ge (tv, &ts->frac_thresh))
	  /* Calculate another place of prec, but only for low numbers.  */
	  {
	    /* TV times 10.  */
	    struct timeval tv10 = 
	      { tv->tv_sec * 10 + tv->tv_usec / 100000,
		  (tv->tv_usec % 100000) * 10 };
	    frac = tv_div (&tv10, u) - num * 10;
	    if (frac)
	      num_len += 2;	/* Account for the extra `.' + DIGIT.  */
	  }

	/* While we have a choice, find a suffix that fits in WIDTH.  */
	for (sfx = ts->sfxs; sfx[1]; sfx++)
	  if (num_len + strlen (*sfx) <= width)
	    break;

	if (!sfx[1] && frac)
	  /* We couldn't find a suffix that fits, and we're printing a
	     fraction digit.  Sacrifice the fraction to make it fit.  */
	  {
	    num_len -= 2;
	    frac = 0;
	    for (sfx = ts->sfxs; sfx[1]; sfx++)
	      if (num_len + strlen (*sfx) <= width)
		break;
	  }

	if (!sfx[1])
	  /* Still couldn't find a suffix that fits.  Oh well, use the best. */
	  sfx--;

	if (frac)
	  return snprintf (buf, buf_len, "%d.%d%s", num, frac, *sfx);
	else
	  return snprintf (buf, buf_len, "%d%s", num, *sfx);
      }

  return sprintf (buf, "0");	/* Whatever */
}

/* Prints the number of units of size UNIT in *SECS, subtracting them from
   *SECS, to BUF (the result *must* fit!), followed by SUFFIX; if the number
   of units is zero, however, and *LEADING_ZEROS is false, print nothing (and
   if something *is* printed, set *LEADING_ZEROS to true).  MIN_WIDTH is the
   minimum *total width* (including other fields) needed to print these
   units.  WIDTH is the amount of (total) space available.  The number of
   characters printed is returned.  */
static size_t
add_field (int *secs, int unit, int *leading_zeros,
	   size_t min_width, char *suffix,
	   size_t width, char *buf)
{
  int units = *secs / unit;
  if (units || (width >= min_width && *leading_zeros))
    {
      *secs -= units * unit;
      *leading_zeros = 1;
      return
	sprintf (buf,
		  (width == min_width ? "%d%s"
		   : width == min_width + 1 ? "%2d%s"
		   : "%02d%s"),
		  units, suffix);
    }
  else
    return 0;
}

/* Format into BUF & BUF_LEN the time interval represented by TV, using
   HH:MM:SS notation where possible, with FRAC_PLACES digits after the
   decimal point, and trying to make the result less than WIDTH characters
   wide.  If LEADING_ZEROS is true, then any fields that are zero-valued, but
   would fit in the given width are printed.  If FRAC_PLACES is negative,
   then any space remaining after printing the time, up to WIDTH, is used for
   the fraction.  The number of characters used is returned.  */
size_t
fmt_seconds (struct timeval *tv, int leading_zeros, int frac_places,
	     size_t width, char *buf, size_t buf_len)
{
  char *p = buf;
  int secs = tv->tv_sec;

  if (width <= 0 || width >= buf_len)
    width = buf_len - 1;

  if (tv->tv_sec > DAY)
    return fmt_named_interval (tv, width, buf, buf_len);

  if (frac_places > 0)
    width -= frac_places + 1;

  /* See if this time won't fit at all in fixed format.  */
  if ((secs > 10*HOUR && width < 8)
      || (secs > HOUR && width < 7)
      || (secs > 10*MINUTE && width < 5)
      || (secs > MINUTE && width < 4)
      || (secs > 10 && width < 2)
      || width < 1)
    return fmt_named_interval (tv, width, buf, buf_len);

  p += add_field (&secs, HOUR, &leading_zeros, 7, ":", width, p);
  p += add_field (&secs, MINUTE, &leading_zeros, 4, ":", width, p);
  p += add_field (&secs, SECOND, &leading_zeros, 1, "", width, p);

  if (frac_places < 0 && (p - buf) < (int) width - 2)
    /* If FRAC_PLACES is < 0, then use any space remaining before WIDTH.  */
    frac_places = width - (p - buf) - 1;

  if (frac_places > 0)
    /* Print fractions of a second.  */
    {
      int frac = tv->tv_usec, i;
      for (i = 6; i > frac_places; i--)
	frac /= 10;
      return (p - buf) + sprintf (p, ".%0*d", frac_places, frac);
    }
  else
    return (p - buf);
}

/* Format into BUF & BUF_LEN the time interval represented by TV, using HH:MM
   notation where possible, and trying to make the result less than WIDTH
   characters wide.  If LEADING_ZEROS is true, then any fields that are
   zero-valued, but would fit in the given width are printed.  The number of
   characters used is returned.  */
size_t
fmt_minutes (struct timeval *tv, int leading_zeros,
	     size_t width, char *buf, size_t buf_len)
{
  char *p = buf;
  int secs = tv->tv_sec;

  if (width <= 0 || width >= buf_len)
    width = buf_len - 1;

  if (secs > DAY)
    return fmt_named_interval (tv, width, buf, buf_len);

  /* See if this time won't fit at all in fixed format.  */
  if ((secs > 10*HOUR && width < 5)
      || (secs > HOUR && width < 4)
      || (secs > 10*MINUTE && width < 2)
      || width < 1)
    return fmt_named_interval (tv, width, buf, buf_len);

  p += add_field (&secs, HOUR, &leading_zeros, 4, ":", width, p);
  p += add_field (&secs, MINUTE, &leading_zeros, 1, "", width, p);

  return p - buf;
}

/* Format into BUF & BUF_LEN the absolute time represented by TV.  An attempt
   is made to fit the result in less than WIDTH characters, by omitting
   fields implied by the current time, NOW (if NOW is 0, then no assumption
   is made, so the resulting times will be somewhat long).  The number of
   characters used is returned.  */
size_t
fmt_past_time (struct timeval *tv, struct timeval *now,
	       size_t width, char *buf, size_t buf_len)
{
  static char *time_fmts[] = { "%-r", "%-l:%M%p", "%-l%p", 0 };
  static char *week_fmts[] = { "%A", "%a", 0 };
  static char *month_fmts[] = { "%A %-d", "%a %-d", "%a%-d", 0 };
  static char *date_fmts[] =
    { "%A, %-d %B", "%a, %-d %b", "%-d %B", "%-d %b", "%-d%b", 0 };
  static char *year_fmts[] =
    { "%A, %-d %B %Y", "%a, %-d %b %Y", "%a, %-d %b %y", "%-d %b %y", "%-d%b%y", 0 };
  struct tm tm;
  int used = 0;			/* Number of characters generated.  */
  long diff = now ? (now->tv_sec - tv->tv_sec) : tv->tv_sec;

  if (diff < 0)
    diff = -diff;		/* XXX */

  bcopy (localtime ((time_t *) &tv->tv_sec), &tm, sizeof tm);

  if (width <= 0 || width >= buf_len)
    width = buf_len - 1;

  if (diff < DAY)
    {
      char **fmt;
      for (fmt = time_fmts; *fmt && !used; fmt++)
	used = strftime (buf, width + 1, *fmt, &tm);
      if (! used)
	/* Nothing worked, perhaps WIDTH is too small, but BUF_LEN will work.
	   We know FMT is one past the end the array, so FMT[-1] should be
	   the shortest possible option.  */
	used = strftime (buf, buf_len, fmt[-1], &tm);
    }
  else
    {
      static char *seps[] = { ", ", " ", "", 0 };
      char **fmt, **dfmt, **dfmts, **sep;

      if (diff < WEEK)
	dfmts = week_fmts;
      else if (diff < MONTH)
	dfmts = month_fmts;
      else if (diff < YEAR)
	dfmts = date_fmts;
      else
	dfmts = year_fmts;

      /* This ordering (date varying most quickly, then the separator, then
	 the time) preserves time detail as long as possible, and seems to
	 produce a graceful degradation of the result with decreasing widths. */
      for (fmt = time_fmts; *fmt && !used; fmt++)
	for (sep = seps; *sep && !used; sep++)
	  for (dfmt = dfmts; *dfmt && !used; dfmt++)
	    {
	      char whole_fmt[strlen (*dfmt) + strlen (*sep) + strlen (*fmt) + 1];
	      char *end = whole_fmt;

	      end = stpcpy (end, *dfmt);
	      end = stpcpy (end, *sep);
	      stpcpy (end, *fmt);

	      used = strftime (buf, width + 1, whole_fmt, &tm);
	    }

      if (! used)
	/* No concatenated formats worked, try just date formats.  */
	for (dfmt = dfmts; *dfmt && !used; dfmt++)
	  used = strftime (buf, width + 1, *dfmt, &tm);

      if (! used)
	/* Absolutely nothing has worked, perhaps WIDTH is too small, but
	   BUF_LEN will work.  We know DFMT is one past the end the array, so
	   DFMT[-1] should be the shortest possible option.  */
	used = strftime (buf, buf_len, dfmt[-1], &tm);
    }

  return used;
}