1/*
2 * This file is part of gitg
3 *
4 * Copyright (C) 2013 - Jesse van den Kieboom
5 *
6 * gitg is free software; you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation; either version 2 of the License, or
9 * (at your option) any later version.
10 *
11 * gitg is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14 * GNU General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License
17 * along with gitg. If not, see <http://www.gnu.org/licenses/>.
18 */
19
20namespace Gitg
21{
22
23public errordomain DateError
24{
25	INVALID_FORMAT
26}
27
28public class Date : Object, Initable
29{
30	private static Regex s_rfc2822;
31	private static Regex s_iso8601;
32	private static Regex s_internal;
33
34	private static Settings? s_gnome_interface_settings;
35	private static bool s_tried_gnome_interface_settings;
36
37	private static string?[] s_months = new string?[] {
38		null,
39		"Jan",
40		"Feb",
41		"Mar",
42		"Apr",
43		"May",
44		"Jun",
45		"Jul",
46		"Aug",
47		"Sep",
48		"Oct",
49		"Nov",
50		"Dec"
51	};
52
53	static construct
54	{
55		try
56		{
57
58		s_iso8601 = new Regex(@"^
59			(?<year>[0-9]{4})
60			(?:
61				[-.]?(?:
62					(?<month>[0-9]{2})
63					(?:
64						[-.]?(?<day>[0-9]{2})
65					)?
66				|
67					W(?<week>[0-9]{2})
68					(?:
69						[-.]?(?<weekday>[0-9])
70					)?
71				)
72				(?:
73					[T ](?<hour>[0-9]{2})
74					(?:
75						:?
76						(?<minute>[0-9]{2})
77						(?:
78							:?
79							(?<seconds>[0-9]{2})
80							(?<tz>
81								(?<tzutc>Z) |
82								[+-](?<tzhour>[0-9]{2})
83								(?:
84									:?
85									(?<tzminute>[0-9]{2})
86								)?
87							)?
88						)?
89					)?
90				)?
91			)?
92		$$", RegexCompileFlags.EXTENDED);
93
94		s_rfc2822 = new Regex(@"^
95			(?:
96				[\\s]*(?<dayofweek>Mon|Tue|Wed|Thu|Fri|Sat|Sun)
97				,
98			)?
99			[\\s]*(?<day>[0-9]{1,2})
100			[\\s]+
101				(?<month>Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)
102			[\\s]+
103				(?<year>[0-9]{4})
104			[\\s]+
105				(?<hour>[0-9]{2})
106				:
107				(?<minute>[0-9]{2})
108				(?:
109					:
110					(?<seconds>[0-9]{2})
111				)?
112			[\\s]+
113			(?<tz>
114				[+-]
115				(?<tzhour>[0-9]{2})
116				(?<tzminute>[0-9]{2})
117			)
118		$$", RegexCompileFlags.EXTENDED);
119
120		s_internal = new Regex(@"^
121			@?
122			(?<timestamp>[0-9]+)
123			[ ](?<tz>
124				[+-](?<tzhour>[0-9]{2})
125				(?:
126					:?
127					(?<tzminute>[0-9]{2})?
128				)
129			)
130		$$", RegexCompileFlags.EXTENDED);
131
132		}
133		catch (Error e)
134		{
135			warning(@"Failed to compile date regex: $(e.message)");
136		}
137	}
138
139	private static bool fetch_and_set_int(MatchInfo info, string name, ref int retval)
140	{
141		string? val = info.fetch_named(name);
142
143		if (val == null)
144		{
145			return false;
146		}
147
148		retval = int.parse(val);
149		return true;
150	}
151
152	private static bool fetch_and_set_double(MatchInfo info, string name, ref double retval)
153	{
154		string? val = info.fetch_named(name);
155
156		if (val == null)
157		{
158			return false;
159		}
160
161		retval = double.parse(val);
162		return true;
163	}
164
165	private static DateTime parse_internal(MatchInfo info) throws Error
166	{
167		string? timestamp = info.fetch_named("timestamp");
168		int64 unixt = int64.parse(timestamp);
169
170		string? tzs = info.fetch_named("tz");
171
172		if (tzs != null)
173		{
174			var ret = new DateTime.from_unix_utc(unixt);
175			return ret.to_timezone(new TimeZone(tzs));
176		}
177		else
178		{
179			return new DateTime.from_unix_local(unixt);
180		}
181	}
182
183	private static DateTime parse_iso8601(MatchInfo info) throws Error
184	{
185		TimeZone tz = new TimeZone.utc();
186
187		int year = 0;
188		int month = 1;
189		int day = 1;
190		int hour = 0;
191		int minute = 0;
192		double seconds = 0.0;
193
194		fetch_and_set_int(info, "year", ref year);
195		fetch_and_set_int(info, "month", ref month);
196		fetch_and_set_int(info, "day", ref day);
197		fetch_and_set_int(info, "hour", ref hour);
198		fetch_and_set_int(info, "minute", ref minute);
199		fetch_and_set_double(info, "seconds", ref seconds);
200
201		string? tzs = info.fetch_named("tz");
202
203		if (tzs != null)
204		{
205			tz = new TimeZone(tzs);
206		}
207		else
208		{
209			tz = new TimeZone.local();
210		}
211
212		return new DateTime(tz, year, month, day, hour, minute, seconds);
213	}
214
215	private static DateTime parse_rfc2822(MatchInfo info) throws Error
216	{
217		TimeZone tz;
218		int year = 0;
219		int month = 0;
220		int day = 1;
221		int hour = 0;
222		int minute = 0;
223		double seconds = 0;
224
225		fetch_and_set_int(info, "year", ref year);
226
227		string? monthstr = info.fetch_named("month");
228
229		for (int i = 0; i < s_months.length; ++i)
230		{
231			if (s_months[i] != null && s_months[i] == monthstr)
232			{
233				month = i;
234				break;
235			}
236		}
237
238		if (month == 0)
239		{
240			throw new DateError.INVALID_FORMAT("Invalid month specified");
241		}
242
243		fetch_and_set_int(info, "day", ref day);
244		fetch_and_set_int(info, "hour", ref hour);
245		fetch_and_set_int(info, "minute", ref minute);
246		fetch_and_set_double(info, "seconds", ref seconds);
247
248		string? tzs = info.fetch_named("tz");
249
250		if (tzs != null)
251		{
252			tz = new TimeZone(tzs);
253		}
254		else
255		{
256			tz = new TimeZone.local();
257		}
258
259		return new DateTime(tz, year, month, day, hour, minute, seconds);
260	}
261
262	private DateTime d_datetime;
263
264	public string date_string
265	{
266		get; construct set;
267	}
268
269	public DateTime date
270	{
271		get { return d_datetime; }
272	}
273
274	public bool init(Cancellable? cancellable = null) throws Error
275	{
276		MatchInfo info;
277
278		if (s_internal.match(date_string, 0, out info))
279		{
280			d_datetime = parse_internal(info);
281
282			return true;
283		}
284
285		if (s_iso8601.match(date_string, 0, out info))
286		{
287			d_datetime = parse_iso8601(info);
288
289			return true;
290		}
291
292		if (s_rfc2822.match(date_string, 0, out info))
293		{
294			d_datetime = parse_rfc2822(info);
295
296			return true;
297		}
298
299		throw new DateError.INVALID_FORMAT("Invalid date format");
300	}
301
302	public Date(string date) throws Error
303	{
304		Object(date_string: date);
305		((Initable)this).init(null);
306	}
307
308	private bool is_24h
309	{
310		get
311		{
312			if (s_gnome_interface_settings == null && !s_tried_gnome_interface_settings)
313			{
314				var source = SettingsSchemaSource.get_default();
315
316				s_tried_gnome_interface_settings = true;
317
318				var schema_id = "org.gnome.desktop.interface";
319
320				if (source != null && source.lookup(schema_id, true) != null)
321				{
322					s_gnome_interface_settings = new Settings(schema_id);
323				}
324			}
325
326			if (s_gnome_interface_settings == null)
327			{
328				return false;
329			}
330
331			return s_gnome_interface_settings.get_enum("clock-format") == GDesktop.ClockFormat.24H;
332		}
333	}
334
335	public string for_display()
336	{
337		var dt = d_datetime;
338		TimeSpan t = (new DateTime.now_local()).difference(dt);
339
340		if (t < TimeSpan.MINUTE * 29.5)
341		{
342			int rounded_minutes = (int) Math.round((float) t / TimeSpan.MINUTE);
343
344			if (rounded_minutes == 0)
345			{
346				return _("Now");
347			}
348			else
349			{
350				return ngettext("A minute ago", "%d minutes ago", rounded_minutes).printf(rounded_minutes);
351			}
352		}
353		else if (t < TimeSpan.MINUTE * 45)
354		{
355			return _("Half an hour ago");
356		}
357		else if (t < TimeSpan.HOUR * 23.5)
358		{
359			int rounded_hours = (int) Math.round((float) t / TimeSpan.HOUR);
360			return ngettext("An hour ago", "%d hours ago", rounded_hours).printf(rounded_hours);
361		}
362		else if (t < TimeSpan.DAY * 7)
363		{
364			int rounded_days = (int) Math.round((float) t / TimeSpan.DAY);
365			return ngettext("A day ago", "%d days ago", rounded_days).printf(rounded_days);
366		}
367		else if (dt.get_year() == new DateTime.now_local().get_year())
368		{
369			if (is_24h)
370			{
371				/* Translators: this is a strftime type date format which is
372				   used when the date is in the current year and uses a 24 hour
373				   clock.*/
374				return dt.format(_("%b %e, %H∶%M"));
375			}
376			else
377			{
378				/* Translators: this is a strftime type date format which is
379				   used when the date is in the current year and uses a 12 hour
380				   clock.*/
381				return dt.format(_("%b %e, %I∶%M %p"));
382			}
383		}
384		else
385		{
386			if (is_24h)
387			{
388				/* Translators: this is a strftime type date format which is
389				   used when the date is not in the current year and uses a 24
390				   hour clock.*/
391				return dt.format(_("%b %e %Y, %H∶%M"));
392			}
393			else
394			{
395				/* Translators: this is a strftime type date format which is
396				   used when the date is not in the current year and uses a 12
397				   hour clock.*/
398				return dt.format(_("%b %e %Y, %I∶%M %p"));
399			}
400		}
401	}
402
403	public Date.for_date_time(DateTime dt)
404	{
405		d_datetime = dt;
406	}
407
408	public static DateTime parse(string date) throws Error
409	{
410		return (new Date(date)).date;
411	}
412}
413
414}
415
416// ex: ts=4 noet
417