1 /*
2 * Copyright 2023 GNOME Foundation Inc.
3 *
4 * SPDX-License-Identifier: LGPL-2.1-or-later
5 *
6 * This library is free software; you can redistribute it and/or
7 * modify it under the terms of the GNU Lesser General Public
8 * License as published by the Free Software Foundation; either
9 * version 2.1 of the License, or (at your option) any later version.
10 *
11 * This library 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 GNU
14 * Lesser General Public License for more details.
15 *
16 * You should have received a copy of the GNU Lesser General Public
17 * License along with this library; if not, see <http://www.gnu.org/licenses/>.
18 *
19 * Authors:
20 * - Philip Withnall <pwithnall@gnome.org>
21 */
22
23 #include "glib.h"
24 #include "gdatetime-private.h"
25
26 /**
27 * _g_era_date_compare:
28 * @date1: first date
29 * @date2: second date
30 *
31 * Compare two #GEraDates for ordering, taking into account negative and
32 * positive infinity.
33 *
34 * Returns: strcmp()-style integer, `<0` indicates `date1 < date2`, `0`
35 * indicates `date1 == date2`, `>0` indicates `date1 > date2`
36 * Since: 2.80
37 */
38 int
39 _g_era_date_compare (const GEraDate *date1,
40 const GEraDate *date2)
41 {
42 if (date1->type == G_ERA_DATE_SET &&
43 date2->type == G_ERA_DATE_SET)
44 {
45 if (date1->year != date2->year)
46 return date1->year - date2->year;
47 if (date1->month != date2->month)
48 return date1->month - date2->month;
49 return date1->day - date2->day;
50 }
51
52 if (date1->type == date2->type)
53 return 0;
54
55 if (date1->type == G_ERA_DATE_MINUS_INFINITY || date2->type == G_ERA_DATE_PLUS_INFINITY)
56 return -1;
57 if (date1->type == G_ERA_DATE_PLUS_INFINITY || date2->type == G_ERA_DATE_MINUS_INFINITY)
58 return 1;
59
60 g_assert_not_reached ();
61 }
62
63 static gboolean
64 parse_era_date (const char *str,
65 const char *endptr,
66 GEraDate *out_date)
67 {
68 const char *str_endptr = NULL;
69 int year_multiplier;
70 guint64 year, month, day;
71
72 year_multiplier = (str[0] == '-') ? -1 : 1;
73 if (str[0] == '-' || str[0] == '+')
74 str++;
75
76 year = g_ascii_strtoull (str, (gchar **) &str_endptr, 10);
77 g_assert (str_endptr <= endptr);
78 if (str_endptr == endptr || *str_endptr != '/' || year > G_MAXINT)
79 return FALSE;
80 str = str_endptr + 1;
81
82 month = g_ascii_strtoull (str, (gchar **) &str_endptr, 10);
83 g_assert (str_endptr <= endptr);
84 if (str_endptr == endptr || *str_endptr != '/' || month < 1 || month > 12)
85 return FALSE;
86 str = str_endptr + 1;
87
88 day = g_ascii_strtoull (str, (gchar **) &str_endptr, 10);
89 g_assert (str_endptr <= endptr);
90 if (str_endptr != endptr || day < 1 || day > 31)
91 return FALSE;
92
93 /* Success */
94 out_date->type = G_ERA_DATE_SET;
95 out_date->year = year_multiplier * year;
96 out_date->month = month;
97 out_date->day = day;
98
99 return TRUE;
100 }
101
102 /**
103 * _g_era_description_segment_ref:
104 * @segment: a #GEraDescriptionSegment
105 *
106 * Increase the ref count of @segment.
107 *
108 * Returns: (transfer full): @segment
109 * Since: 2.80
110 */
111 GEraDescriptionSegment *
112 _g_era_description_segment_ref (GEraDescriptionSegment *segment)
113 {
114 g_atomic_ref_count_inc (&segment->ref_count);
115 return segment;
116 }
117
118 /**
119 * _g_era_description_segment_unref:
120 * @segment: (transfer full): a #GEraDescriptionSegment to unref
121 *
122 * Decreases the ref count of @segment.
123 *
124 * Since: 2.80
125 */
126 void
127 _g_era_description_segment_unref (GEraDescriptionSegment *segment)
128 {
129 if (g_atomic_ref_count_dec (&segment->ref_count))
130 {
131 g_free (segment->era_format);
132 g_free (segment->era_name);
133 g_free (segment);
134 }
135 }
136
137 /**
138 * _g_era_description_parse:
139 * @desc: an `ERA` description string from `nl_langinfo()`
140 *
141 * Parse an ERA description string. See [`nl_langinfo(3)`](man:nl_langinfo(3)).
142 *
143 * Example description string for th_TH.UTF-8:
144 * ```
145 * +:1:-543/01/01:+*:พ.ศ.:%EC %Ey
146 * ```
147 *
148 * @desc must be in UTF-8, so all conversion from the locale encoding must
149 * happen before this function is called. The resulting `era_name` and
150 * `era_format` in the returned segments will be in UTF-8.
151 *
152 * Returns: (transfer full) (nullable) (element-type GEraDescriptionSegment):
153 * array of one or more parsed era segments, or %NULL if parsing failed
154 * Since: 2.80
155 */
156 GPtrArray *
157 _g_era_description_parse (const char *desc)
158 {
159 GPtrArray *segments = g_ptr_array_new_with_free_func ((GDestroyNotify) _g_era_description_segment_unref);
160
161 for (const char *p = desc; *p != '\0';)
162 {
163 const char *next_colon, *endptr = NULL;
164 GEraDescriptionSegment *segment = NULL;
165 char direction;
166 guint64 offset;
167 GEraDate start_date, end_date;
168 char *era_name = NULL, *era_format = NULL;
169
170 /* direction */
171 direction = *p++;
172 if (direction != '+' && direction != '-')
173 goto error;
174
175 if (*p++ != ':')
176 goto error;
177
178 /* offset */
179 next_colon = strchr (p, ':');
180 if (next_colon == NULL)
181 goto error;
182
183 offset = g_ascii_strtoull (p, (gchar **) &endptr, 10);
184 if (endptr != next_colon)
185 goto error;
186 p = next_colon + 1;
187
188 /* start_date */
189 next_colon = strchr (p, ':');
190 if (next_colon == NULL)
191 goto error;
192
193 if (!parse_era_date (p, next_colon, &start_date))
194 goto error;
195 p = next_colon + 1;
196
197 /* end_date */
198 next_colon = strchr (p, ':');
199 if (next_colon == NULL)
200 goto error;
201
202 if (strncmp (p, "-*", 2) == 0)
203 end_date.type = G_ERA_DATE_MINUS_INFINITY;
204 else if (strncmp (p, "+*", 2) == 0)
205 end_date.type = G_ERA_DATE_PLUS_INFINITY;
206 else if (!parse_era_date (p, next_colon, &end_date))
207 goto error;
208 p = next_colon + 1;
209
210 /* era_name */
211 next_colon = strchr (p, ':');
212 if (next_colon == NULL)
213 goto error;
214
215 if (next_colon - p == 0)
216 goto error;
217 era_name = g_strndup (p, next_colon - p);
218 p = next_colon + 1;
219
220 /* era_format; either the final field in the segment (followed by a
221 * semicolon) or the description (followed by nul) */
222 next_colon = strchr (p, ';');
223 if (next_colon == NULL)
224 next_colon = p + strlen (p);
225
226 if (next_colon - p == 0)
227 {
228 g_free (era_name);
229 goto error;
230 }
231 era_format = g_strndup (p, next_colon - p);
232 if (*next_colon == ';')
233 p = next_colon + 1;
234 else
235 p = next_colon;
236
237 /* Successfully parsed that segment. */
238 segment = g_new0 (GEraDescriptionSegment, 1);
239 g_atomic_ref_count_init (&segment->ref_count);
240 segment->offset = offset;
241 segment->start_date = start_date;
242 segment->end_date = end_date;
243 segment->direction_multiplier =
244 ((_g_era_date_compare (&segment->start_date, &segment->end_date) <= 0) ? 1 : -1) *
245 ((direction == '-') ? -1 : 1);
246 segment->era_name = g_steal_pointer (&era_name);
247 segment->era_format = g_steal_pointer (&era_format);
248
249 g_ptr_array_add (segments, g_steal_pointer (&segment));
250 }
251
252 return g_steal_pointer (&segments);
253
254 error:
255 g_ptr_array_unref (segments);
256 return NULL;
257 }