Some more use of enum class.
[libdcp.git] / src / language_tag.cc
1 /*
2     Copyright (C) 2020-2021 Carl Hetherington <cth@carlh.net>
3
4     This file is part of libdcp.
5
6     libdcp 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     libdcp 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 libdcp.  If not, see <http://www.gnu.org/licenses/>.
18
19     In addition, as a special exception, the copyright holders give
20     permission to link the code of portions of this program with the
21     OpenSSL library under certain conditions as described in each
22     individual source file, and distribute linked combinations
23     including the two.
24
25     You must obey the GNU General Public License in all respects
26     for all of the code used other than OpenSSL.  If you modify
27     file(s) with this exception, you may extend this exception to your
28     version of the file(s), but you are not obligated to do so.  If you
29     do not wish to do so, delete this exception statement from your
30     version.  If you delete this exception statement from all source
31     files in the program, then also delete it here.
32 */
33
34
35 #include "compose.hpp"
36 #include "dcp_assert.h"
37 #include "exceptions.h"
38 #include "language_tag.h"
39 #include <boost/algorithm/string.hpp>
40 #include <boost/foreach.hpp>
41 #include <string>
42
43
44 using std::make_pair;
45 using std::ostream;
46 using std::pair;
47 using std::string;
48 using std::vector;
49 using boost::optional;
50 using boost::algorithm::trim;
51 using namespace dcp;
52
53
54 static vector<LanguageTag::SubtagData> language_list;
55 static vector<LanguageTag::SubtagData> variant_list;
56 static vector<LanguageTag::SubtagData> region_list;
57 static vector<LanguageTag::SubtagData> script_list;
58 static vector<LanguageTag::SubtagData> extlang_list;
59
60
61 static
62 optional<LanguageTag::SubtagData>
63 find_in_list (vector<LanguageTag::SubtagData> const& list, string subtag)
64 {
65         BOOST_FOREACH (LanguageTag::SubtagData const& i, list) {
66                 if (boost::iequals(i.subtag, subtag)) {
67                         return i;
68                 }
69         }
70
71         return optional<LanguageTag::SubtagData>();
72 }
73
74
75 LanguageTag::Subtag::Subtag (string subtag, SubtagType type)
76         : _subtag (subtag)
77 {
78         if (!get_subtag_data(type, subtag)) {
79                 throw LanguageTagError(String::compose("Unknown %1 string %2", subtag_type_name(type), subtag));
80         }
81 }
82
83
84 LanguageTag::LanguageTag (string tag)
85 {
86         vector<string> parts;
87         boost::split (parts, tag, boost::is_any_of("-"));
88         if (parts.empty()) {
89                 throw LanguageTagError (String::compose("Could not parse language tag %1", tag));
90         }
91
92         vector<string>::size_type p = 0;
93         _language = LanguageSubtag (parts[p]);
94         ++p;
95
96         if (p == parts.size()) {
97                 return;
98         }
99
100         try {
101                 _script = ScriptSubtag (parts[p]);
102                 ++p;
103         } catch (...) {}
104
105         if (p == parts.size()) {
106                 return;
107         }
108
109         try {
110                 _region = RegionSubtag (parts[p]);
111                 ++p;
112         } catch (...) {}
113
114         if (p == parts.size()) {
115                 return;
116         }
117
118         try {
119                 while (true) {
120                         _variants.push_back (VariantSubtag(parts[p]));
121                         ++p;
122                         if (p == parts.size()) {
123                                 return;
124                         }
125                 }
126         } catch (...) {}
127
128         try {
129                 while (true) {
130                         _extlangs.push_back (ExtlangSubtag(parts[p]));
131                         ++p;
132                         if (p == parts.size()) {
133                                 return;
134                         }
135                 }
136         } catch (...) {}
137
138         if (p < parts.size()) {
139                 throw LanguageTagError (String::compose("Unrecognised subtag %1", parts[p]));
140         }
141 }
142
143
144 string
145 LanguageTag::to_string () const
146 {
147         if (!_language) {
148                 throw LanguageTagError("No language set up");
149         }
150
151         string s = _language->subtag();
152
153         if (_script) {
154                 s += "-" + _script->subtag();
155         }
156
157         if (_region) {
158                 s += "-" + _region->subtag();
159         }
160
161         BOOST_FOREACH (VariantSubtag i, _variants) {
162                 s += "-" + i.subtag();
163         }
164
165         BOOST_FOREACH (ExtlangSubtag i, _extlangs) {
166                 s += "-" + i.subtag();
167         }
168
169         return s;
170 }
171
172
173 void
174 LanguageTag::set_language (LanguageSubtag language)
175 {
176         _language = language;
177 }
178
179
180 void
181 LanguageTag::set_script (ScriptSubtag script)
182 {
183         _script = script;
184 }
185
186
187 void
188 LanguageTag::set_region (RegionSubtag region)
189 {
190         _region = region;
191 }
192
193
194 void
195 LanguageTag::add_variant (VariantSubtag variant)
196 {
197         if (find(_variants.begin(), _variants.end(), variant) != _variants.end()) {
198                 throw LanguageTagError (String::compose("Duplicate Variant subtag %1", variant.subtag()));
199         }
200
201         _variants.push_back (variant);
202 }
203
204
205 template <class T>
206 void
207 check_for_duplicates (vector<T> const& subtags, dcp::LanguageTag::SubtagType type)
208 {
209         vector<T> sorted = subtags;
210         sort (sorted.begin(), sorted.end());
211         optional<T> last;
212         for (auto const& i: sorted) {
213                 if (last && i == *last) {
214                         throw LanguageTagError (String::compose("Duplicate %1 subtag %2", dcp::LanguageTag::subtag_type_name(type), i.subtag()));
215                 }
216                 last = i;
217         }
218 }
219
220
221 void
222 LanguageTag::set_variants (vector<VariantSubtag> variants)
223 {
224         check_for_duplicates (variants, SubtagType::VARIANT);
225         _variants = variants;
226 }
227
228
229 void
230 LanguageTag::add_extlang (ExtlangSubtag extlang)
231 {
232         if (find(_extlangs.begin(), _extlangs.end(), extlang) != _extlangs.end()) {
233                 throw LanguageTagError (String::compose("Duplicate Extlang subtag %1", extlang.subtag()));
234         }
235
236         _extlangs.push_back (extlang);
237 }
238
239
240 void
241 LanguageTag::set_extlangs (vector<ExtlangSubtag> extlangs)
242 {
243         check_for_duplicates (extlangs, SubtagType::EXTLANG);
244         _extlangs = extlangs;
245 }
246
247
248 string
249 LanguageTag::description () const
250 {
251         if (!_language) {
252                 throw LanguageTagError("No language set up");
253         }
254
255         string d;
256
257         BOOST_FOREACH (VariantSubtag const& i, _variants) {
258                 optional<SubtagData> variant = get_subtag_data (SubtagType::VARIANT, i.subtag());
259                 DCP_ASSERT (variant);
260                 d += variant->description + " dialect of ";
261         }
262
263         optional<SubtagData> language = get_subtag_data (SubtagType::LANGUAGE, _language->subtag());
264         DCP_ASSERT (language);
265         d += language->description;
266
267         if (_script) {
268                 optional<SubtagData> script = get_subtag_data (SubtagType::SCRIPT, _script->subtag());
269                 DCP_ASSERT (script);
270                 d += " written using the " + script->description + " script";
271         }
272
273         if (_region) {
274                 optional<SubtagData> region = get_subtag_data (SubtagType::REGION, _region->subtag());
275                 DCP_ASSERT (region);
276                 d += " for " + region->description;
277         }
278
279         BOOST_FOREACH (ExtlangSubtag const& i, _extlangs) {
280                 optional<SubtagData> extlang = get_subtag_data (SubtagType::EXTLANG, i.subtag());
281                 DCP_ASSERT (extlang);
282                 d += ", " + extlang->description;
283         }
284
285         return d;
286 }
287
288
289 vector<LanguageTag::SubtagData> const &
290 LanguageTag::get_all (SubtagType type)
291 {
292         switch (type) {
293         case SubtagType::LANGUAGE:
294                 return language_list;
295         case SubtagType::SCRIPT:
296                 return script_list;
297         case SubtagType::REGION:
298                 return region_list;
299         case SubtagType::VARIANT:
300                 return variant_list;
301         case SubtagType::EXTLANG:
302                 return extlang_list;
303         }
304
305         return language_list;
306 }
307
308
309 string
310 LanguageTag::subtag_type_name (SubtagType type)
311 {
312         switch (type) {
313                 case SubtagType::LANGUAGE:
314                         return "Language";
315                 case SubtagType::SCRIPT:
316                         return "Script";
317                 case SubtagType::REGION:
318                         return "Region";
319                 case SubtagType::VARIANT:
320                         return "Variant";
321                 case SubtagType::EXTLANG:
322                         return "Extended";
323         }
324
325         return "";
326 }
327
328 bool
329 dcp::LanguageTag::VariantSubtag::operator== (VariantSubtag const & other) const
330 {
331         return subtag() == other.subtag();
332 }
333
334
335 bool
336 dcp::LanguageTag::VariantSubtag::operator< (VariantSubtag const & other) const
337 {
338         return subtag() < other.subtag();
339 }
340
341
342 bool
343 dcp::LanguageTag::ExtlangSubtag::operator== (ExtlangSubtag const & other) const
344 {
345         return subtag() == other.subtag();
346 }
347
348
349 bool
350 dcp::LanguageTag::ExtlangSubtag::operator< (ExtlangSubtag const & other) const
351 {
352         return subtag() < other.subtag();
353 }
354
355
356 bool
357 dcp::operator== (dcp::LanguageTag const& a, dcp::LanguageTag const& b)
358 {
359         return a.to_string() == b.to_string();
360 }
361
362
363 ostream&
364 dcp::operator<< (ostream& os, dcp::LanguageTag const& tag)
365 {
366         os << tag.to_string();
367         return os;
368 }
369
370
371 vector<pair<LanguageTag::SubtagType, LanguageTag::SubtagData> >
372 LanguageTag::subtags () const
373 {
374         vector<pair<SubtagType, SubtagData> > s;
375
376         if (_language) {
377                 s.push_back (make_pair(SubtagType::LANGUAGE, *get_subtag_data(SubtagType::LANGUAGE, _language->subtag())));
378         }
379
380         if (_script) {
381                 s.push_back (make_pair(SubtagType::SCRIPT, *get_subtag_data(SubtagType::SCRIPT, _script->subtag())));
382         }
383
384         if (_region) {
385                 s.push_back (make_pair(SubtagType::REGION, *get_subtag_data(SubtagType::REGION, _region->subtag())));
386         }
387
388         BOOST_FOREACH (VariantSubtag const& i, _variants) {
389                 s.push_back (make_pair(SubtagType::VARIANT, *get_subtag_data(SubtagType::VARIANT, i.subtag())));
390         }
391
392         BOOST_FOREACH (ExtlangSubtag const& i, _extlangs) {
393                 s.push_back (make_pair(SubtagType::EXTLANG, *get_subtag_data(SubtagType::EXTLANG, i.subtag())));
394         }
395
396         return s;
397 }
398
399
400 optional<LanguageTag::SubtagData>
401 LanguageTag::get_subtag_data (LanguageTag::SubtagType type, string subtag)
402 {
403         switch (type) {
404         case SubtagType::LANGUAGE:
405                 return find_in_list(language_list, subtag);
406         case SubtagType::SCRIPT:
407                 return find_in_list(script_list, subtag);
408         case SubtagType::REGION:
409                 return find_in_list(region_list, subtag);
410         case SubtagType::VARIANT:
411                 return find_in_list(variant_list, subtag);
412         case SubtagType::EXTLANG:
413                 return find_in_list(extlang_list, subtag);
414         }
415
416         return optional<LanguageTag::SubtagData>();
417 }
418
419
420 optional<string>
421 LanguageTag::get_subtag_description (LanguageTag::SubtagType type, string subtag)
422 {
423         optional<SubtagData> data = get_subtag_data (type, subtag);
424         if (!data) {
425                 return optional<string>();
426         }
427
428         return data->description;
429 }
430
431
432 void
433 load_language_tag_list (boost::filesystem::path tags_directory, string name, vector<LanguageTag::SubtagData>& list)
434 {
435         FILE* f = fopen_boost (tags_directory / name, "r");
436         if (!f) {
437                 throw FileError ("Could not open tags file", tags_directory / name, errno);
438         }
439         char buffer[512];
440
441         int i = 0;
442         while (!feof(f)) {
443                 char* r = fgets (buffer, sizeof(buffer), f);
444                 if (r == 0) {
445                         break;
446                 }
447                 string a = buffer;
448                 trim (a);
449                 r = fgets (buffer, sizeof(buffer), f);
450                 if (r == 0) {
451                         fclose (f);
452                         throw FileError ("Bad tags file", tags_directory / name, -1);
453                 }
454                 string b = buffer;
455                 trim (b);
456                 list.push_back (LanguageTag::SubtagData(a, b));
457                 ++i;
458         }
459
460         fclose (f);
461 }
462
463
464 void
465 dcp::load_language_tag_lists (boost::filesystem::path tags_directory)
466 {
467         load_language_tag_list (tags_directory, "language", language_list);
468         load_language_tag_list (tags_directory, "variant", variant_list);
469         load_language_tag_list (tags_directory, "region", region_list);
470         load_language_tag_list (tags_directory, "script", script_list);
471         load_language_tag_list (tags_directory, "extlang", extlang_list);
472 }
473
474