1 /*******************************************************************************
2 
3     Functions to format time strings.
4 
5     Copyright:
6         Copyright (c) 2009-2016 dunnhumby Germany GmbH.
7         All rights reserved.
8 
9     License:
10         Boost Software License Version 1.0. See LICENSE_BOOST.txt for details.
11         Alternatively, this file may be distributed under the terms of the Tango
12         3-Clause BSD License (see LICENSE_BSD.txt for details).
13 
14 *******************************************************************************/
15 
16 module ocean.text.util.Time;
17 
18 
19 
20 
21 import core.sys.posix.time : gmtime_r;
22 
23 import ocean.meta.types.Qualifiers;
24 
25 import ocean.core.Enforce;
26 
27 import ocean.core.Array : copy;
28 
29 import ocean.core.Verify;
30 
31 import core.stdc.time : strftime, time_t, tm;
32 
33 import ocean.text.convert.Formatter;
34 
35 
36 
37 /*******************************************************************************
38 
39     Formats the given UNIX timestamp into the form specified by the format
40     string. If no format string is given, and the length of the destination
41     output buffer has been set to at least 20 characters, then the timestamp
42     will be formatted into the form `1982-09-15 10:37:29`.
43 
44     Refer to the manual page of `strftime` for the various conversion
45     specifications that can be used to construct the format string.
46 
47     This function should be preferred when formatting the time into a static
48     array.
49 
50     Params:
51         timestamp = timestamp to format
52         output = slice to destination string buffer, must be long enough to
53             contain the resulting string post-conversion
54         format_string = format string to define how the timestamp should be
55             formatted, must be null-terminated (defaults to "%F %T\0")
56             note that the format string requires an explicit '\0' even if string
57             literals are being passed
58 
59     Returns:
60         slice to the string formatted in 'output', may be an empty slice if the
61         provided buffer is too small or if an error occurred
62 
63 *******************************************************************************/
64 
65 public mstring formatTime ( time_t timestamp, mstring output,
66     cstring format_string = "%F %T\0" )
67 {
68     verify(!format_string[$ - 1], "Format string must be null-terminated");
69 
70     tm time;
71     size_t len;
72 
73     if ( gmtime_r(&timestamp, &time) )
74     {
75         len = strftime(output.ptr, output.length, format_string.ptr, &time);
76     }
77 
78     return output[0 .. len];
79 }
80 
81 
82 /*******************************************************************************
83 
84     Formats the given UNIX timestamp into the form specified by the format
85     string. If no format string is given, then the timestamp will be formatted
86     into the form `1982-09-15 10:37:29`.
87 
88     Refer to the manual page of `strftime` for the various conversion
89     specifications that can be used to construct the format string.
90 
91     This function should be preferred when formatting the time into a dynamic
92     array.
93 
94     Params:
95         timestamp = timestamp to format
96         output = slice to destination string buffer
97         format_string = format string to define how the timestamp should be
98             formatted, must be null-terminated (defaults to "%F %T\0")
99             note that the format string requires an explicit '\0' even if string
100             literals are being passed
101         max_output_len = maximum length of the resulting string post-conversion
102             (defaults to 50)
103 
104     Returns:
105         the formatted string, may be an empty slice if the result exceeds the
106         maximum output length specified or if an error occurred
107 
108 *******************************************************************************/
109 
110 public mstring formatTimeRef ( time_t timestamp, ref mstring output,
111     cstring format_string = "%F %T\0", uint max_output_len = 50 )
112 {
113     output.length = max_output_len;
114     assumeSafeAppend(output);
115 
116     output.length = formatTime(timestamp, output, format_string).length;
117     assumeSafeAppend(output);
118 
119     return output;
120 }
121 
122 
123 /*******************************************************************************
124 
125     Formats a string with the number of years, days, hours, minutes & seconds
126     specified.
127 
128     Params:
129         s = number of seconds elapsed
130         output = destination string buffer
131 
132     Returns:
133         formatted string
134 
135 *******************************************************************************/
136 
137 public mstring formatDuration ( ulong s, ref mstring output )
138 {
139     output.length = 0;
140     assumeSafeAppend(output);
141 
142     bool comma = false;
143 
144     /***************************************************************************
145 
146         Appends the count of the specified value to the output string, if the
147         value is > 0. Also appends a comma first, if this is not the first value
148         to be appended to the output string. In this way, a comma-separated list
149         of values is built up over multiple calls to this function.
150 
151         Params:
152             number = value to append
153             name = name of quantity
154 
155     ***************************************************************************/
156 
157     void append ( ulong number, cstring name )
158     {
159         if ( number > 0 )
160         {
161             if ( comma ) output ~= ", ";
162             sformat(output, "{} {}{}", number, name,
163                 number > 1 ? "s" : "");
164             comma = true;
165         }
166     }
167 
168     if ( s == 0 )
169     {
170         output.copy("0 seconds");
171     }
172     else
173     {
174         uint years, days, hours, minutes, seconds;
175         extractTimePeriods(s, years, days, hours, minutes, seconds);
176 
177         append(years,   "year");
178         append(days,    "day");
179         append(hours,   "hour");
180         append(minutes, "minute");
181         append(seconds, "second");
182     }
183 
184     return output;
185 }
186 
187 
188 /*******************************************************************************
189 
190     Formats a string with the number of years, days, hours, minutes & seconds
191     specified. The string is formatted with short names for the time periods
192     (e.g. 's' instead of 'seconds').
193 
194     Params:
195         s = number of seconds elapsed
196         output = destination string buffer
197 
198     Returns:
199         formatted string
200 
201 *******************************************************************************/
202 
203 public mstring formatDurationShort ( ulong s, ref mstring output )
204 {
205     output.length = 0;
206     assumeSafeAppend(output);
207 
208     /***************************************************************************
209 
210         Appends the count of the specified value to the output string, if the
211         value is > 0. Also appends a comma first, if this is not the first value
212         to be appended to the output string. In this way, a comma-separated list
213         of values is built up over multiple calls to this function.
214 
215         Params:
216             number = value to append
217             name = name of quantity
218 
219     ***************************************************************************/
220 
221     void append ( ulong number, cstring name )
222     {
223         if ( number > 0 )
224         {
225             sformat(output, "{}{}", number, name);
226         }
227     }
228 
229     if ( s == 0 )
230     {
231         output.copy("0s");
232     }
233     else
234     {
235         uint years, days, hours, minutes, seconds;
236         extractTimePeriods(s, years, days, hours, minutes, seconds);
237 
238         append(years,   "y");
239         append(days,    "d");
240         append(hours,   "h");
241         append(minutes, "m");
242         append(seconds, "s");
243     }
244 
245     return output;
246 }
247 
248 
249 /*******************************************************************************
250 
251     Works out the number of multiples of various timespans (years, days, hours,
252     minutes, seconds) in the provided total count of seconds, breaking the
253     seconds count down into constituent parts.
254 
255     Params:
256         s = total seconds count to extract timespans from
257         years = receives the extracted count of years in s
258         days = receives the extracted count of days in s
259         hours = receives the extracted count of hours in s
260         minutes  = receives the extracted count of minutes in s
261         seconds = receives the remaining seconds after all other timespans have
262             been extracted from s
263 
264 *******************************************************************************/
265 
266 public void extractTimePeriods ( ulong s, out uint years, out uint days,
267     out uint hours, out uint minutes, out uint seconds )
268 {
269     /***************************************************************************
270 
271         Works out the number of multiples of the specified timespan in the total
272         count of seconds, and reduces the seconds count by these multiples. In
273         this way, when this function is called multiple times with decreasingly
274         large timespans, the seconds count can be broken down into constituent
275         parts.
276 
277         Params:
278             timespan = number of seconds in timespan to extract
279 
280         Returns:
281             number of timespans in seconds
282 
283     ***************************************************************************/
284 
285     uint extract ( ulong timespan )
286     {
287         auto extracted = seconds / timespan;
288         seconds -= extracted * timespan;
289         return cast(uint) extracted;
290     }
291 
292     static immutable minute_timespan   = 60;
293     static immutable hour_timespan     = minute_timespan * 60;
294     static immutable day_timespan      = hour_timespan * 24;
295     static immutable year_timespan     = day_timespan * 365;
296 
297     enforce(s <= uint.max);
298     seconds = cast(uint) s;
299 
300     years      = extract(year_timespan);
301     days       = extract(day_timespan);
302     hours      = extract(hour_timespan);
303     minutes    = extract(minute_timespan);
304 }
305 
306 
307 version (unittest)
308 {
309     import ocean.core.Test;
310 }
311 
312 ///
313 unittest
314 {
315     time_t timestamp = 400934249;
316     char[20] static_str;
317     char[50] big_static_str;
318 
319     test!("==")(formatTime(timestamp, static_str), "1982-09-15 10:37:29");
320 
321     // An empty string is returned if the buffer is not large enough
322     test!("==")(formatTime(timestamp, static_str, "%A, %d %B %Y %T\0"), "");
323 
324     test!("==")(formatTime(timestamp, big_static_str, "%A, %d %B %Y %T\0"),
325         "Wednesday, 15 September 1982 10:37:29");
326 
327     mstring buf;
328 
329     formatTimeRef(timestamp, buf);
330     test!("==")(buf, "1982-09-15 10:37:29");
331 
332     formatTimeRef(timestamp, buf, "%A, %d %B %Y %T\0");
333     test!("==")(buf, "Wednesday, 15 September 1982 10:37:29");
334 
335     // An empty string is returned if the resulting string is longer than the
336     // maximum length
337     formatTimeRef(timestamp, buf,
338         "%d/%m/%y, but Americans would write that as %D\0");
339     test!("==")(buf, "");
340 
341     // A larger maximum length can be set if necessary
342     formatTimeRef(timestamp, buf,
343         "%d/%m/%y, but Americans would write that as %D\0", 100);
344     test!("==")(buf, "15/09/82, but Americans would write that as 09/15/82");
345 
346     mstring str;
347     uint seconds = 94523;
348 
349     formatDuration(seconds, str);
350     test!("==")(str, "1 day, 2 hours, 15 minutes, 23 seconds");
351 
352     formatDurationShort(seconds, str);
353     test!("==")(str, "1d2h15m23s");
354 
355     uint years, days, hours, minutes;
356 
357     extractTimePeriods(100000000, years, days, hours, minutes, seconds);
358     test!("==")(years, 3);
359     test!("==")(days, 62);
360     test!("==")(hours, 9);
361     test!("==")(minutes, 46);
362     test!("==")(seconds, 40);
363 }