1 | // |
2 | // Copyright (c) 2009-2011 Artyom Beilis (Tonkikh) |
3 | // Copyright (c) 2021-2023 Alexander Grund |
4 | // |
5 | // Distributed under the Boost Software License, Version 1.0. |
6 | // https://www.boost.org/LICENSE_1_0.txt |
7 | |
8 | #include <boost/locale.hpp> |
9 | #include <boost/locale/conversion.hpp> |
10 | #include "../src/boost/locale/win32/lcid.hpp" |
11 | #include "boostLocale/test/tools.hpp" |
12 | #include "boostLocale/test/unit_test.hpp" |
13 | #include <boost/assert.hpp> |
14 | #include <boost/core/ignore_unused.hpp> |
15 | #include <algorithm> |
16 | #include <iomanip> |
17 | #include <locale> |
18 | #include <sstream> |
19 | #include <string> |
20 | #include <vector> |
21 | #ifdef BOOST_LOCALE_WITH_ICU |
22 | # include <unicode/uversion.h> |
23 | # define BOOST_LOCALE_ICU_VERSION (U_ICU_VERSION_MAJOR_NUM * 100 + U_ICU_VERSION_MINOR_NUM) |
24 | #else |
25 | # define BOOST_LOCALE_ICU_VERSION 0 |
26 | #endif |
27 | |
28 | namespace boost { namespace locale { namespace test { |
29 | template<class Facet> |
30 | BOOST_NOINLINE bool is_facet(const std::locale::facet* facet) |
31 | { |
32 | return dynamic_cast<const Facet*>(facet) != nullptr; |
33 | } |
34 | |
35 | template<class Facet> |
36 | bool has_facet(const std::locale& l) |
37 | { |
38 | return std::has_facet<Facet>(l) && is_facet<Facet>(&std::use_facet<Facet>(l)); |
39 | } |
40 | |
41 | template<class Facet> |
42 | bool has_not_facet(const std::locale& l) |
43 | { |
44 | const Facet* f; |
45 | try { |
46 | f = &std::use_facet<Facet>(l); |
47 | } catch(const std::bad_cast&) { |
48 | return !std::has_facet<Facet>(l); |
49 | } |
50 | // This mustn't be reached, checks for debugging |
51 | TEST(is_facet<Facet>(f)); // LCOV_EXCL_LINE |
52 | TEST(!std::has_facet<Facet>(l)); // LCOV_EXCL_LINE |
53 | return false; // LCOV_EXCL_LINE |
54 | } |
55 | }}} // namespace boost::locale::test |
56 | namespace blt = boost::locale::test; |
57 | |
58 | bool has_message(const std::locale& l) |
59 | { |
60 | return blt::has_facet<boost::locale::message_format<char>>(l); |
61 | } |
62 | |
63 | struct test_facet : public std::locale::facet { |
64 | test_facet() : std::locale::facet(0) {} |
65 | static std::locale::id id; |
66 | }; |
67 | |
68 | std::locale::id test_facet::id; |
69 | |
70 | template<typename CharType> |
71 | using codecvt_by_char_type = std::codecvt<CharType, char, std::mbstate_t>; |
72 | |
73 | namespace bl = boost::locale; |
74 | |
75 | bool hasLocaleForBackend(const std::string& locale_name, const std::string& backendName) |
76 | { |
77 | if(backendName == "winapi" ) |
78 | return has_win_locale(locale_name); |
79 | else if(backendName == "std" ) |
80 | return has_std_locale(name: locale_name.c_str()); |
81 | else if(backendName == "posix" ) |
82 | return has_posix_locale(name: locale_name); |
83 | else { |
84 | BOOST_ASSERT(backendName == "icu" ); |
85 | return BOOST_LOCALE_ICU_VERSION >= 5901; // First version to use (correct) CLDR data |
86 | } |
87 | } |
88 | |
89 | void test_special_locales() |
90 | { |
91 | bl::localization_backend_manager backend = bl::localization_backend_manager::global(); |
92 | for(const std::string& backendName : backend.get_all_backends()) { |
93 | std::cout << "Backend: " << backendName << std::endl; |
94 | backend.select(backend_name: backendName); |
95 | bl::localization_backend_manager::global(backend); |
96 | |
97 | { |
98 | const auto utf8LocaleName = bl::util::get_system_locale(use_utf8_on_windows: true); |
99 | // The WinAPI backend only supports UTF-8 encoding and hence always returns the UTF-8 locale |
100 | const auto ansiLocaleName = (backendName == "winapi" ) ? utf8LocaleName : bl::util::get_system_locale(use_utf8_on_windows: false); |
101 | bl::generator g; |
102 | g.use_ansi_encoding(enc: true); |
103 | std::locale l = g("" ); |
104 | TEST_EQ(std::use_facet<bl::info>(l).name(), ansiLocaleName); |
105 | g.use_ansi_encoding(enc: false); |
106 | l = g("" ); |
107 | TEST_EQ(std::use_facet<bl::info>(l).name(), utf8LocaleName); |
108 | g.use_ansi_encoding(enc: true); |
109 | l = g("" ); |
110 | TEST_EQ(std::use_facet<bl::info>(l).name(), ansiLocaleName); |
111 | } |
112 | |
113 | bl::generator g; |
114 | |
115 | namespace as = bl::as; |
116 | constexpr time_t datetime = 60 * 60 * 24 * (31 + 4) // Feb 5th |
117 | + (15 * 60 + 42) * 60; // 15:42 |
118 | |
119 | const std::string enWorldName = "en_001.UTF-8" ; |
120 | if(!hasLocaleForBackend(locale_name: enWorldName, backendName)) |
121 | std::cout << "\tSkipping due to missing locale " << enWorldName << std::endl; |
122 | else { |
123 | auto l = g(enWorldName); |
124 | const auto& info = std::use_facet<bl::info>(loc: l); |
125 | TEST_EQ(info.language(), "en" ); |
126 | TEST_EQ(info.country(), "001" ); |
127 | TEST(info.utf8()); |
128 | TEST_EQ(info.encoding(), "UTF-8" ); |
129 | |
130 | std::ostringstream os; |
131 | os.imbue(loc: l); |
132 | os << as::time << as::gmt << as::time_short; |
133 | os << datetime; |
134 | TEST_EQ(os.str().substr(0, 4), "3:42" ); // 3:42 pm |
135 | } |
136 | const std::string enEuropeName = "en_150.UTF-8" ; |
137 | if(!hasLocaleForBackend(locale_name: enEuropeName, backendName)) |
138 | std::cout << "\tSkipping due to missing locale " << enEuropeName << std::endl; |
139 | else { |
140 | auto l = g(enEuropeName); |
141 | const auto& info = std::use_facet<bl::info>(loc: l); |
142 | TEST_EQ(info.language(), "en" ); |
143 | TEST_EQ(info.country(), "150" ); |
144 | TEST(info.utf8()); |
145 | TEST_EQ(info.encoding(), "UTF-8" ); |
146 | |
147 | std::ostringstream os; |
148 | |
149 | std::string expectedTimeFormat = "15:42" ; |
150 | // The std locale may not fully support the 150 region and use a different format |
151 | if(backendName == "std" ) { |
152 | os.imbue(loc: std::locale(os.getloc(), new std::time_put_byname<char>(enEuropeName))); |
153 | empty_stream(s&: os) << std::put_time(tmb: gmtime_wrap(time: &datetime), fmt: "%X" ); |
154 | expectedTimeFormat = os.str(); |
155 | } |
156 | |
157 | os.imbue(loc: l); |
158 | empty_stream(s&: os) << as::time << as::gmt << as::time_short; |
159 | os << datetime; |
160 | TEST_EQ(os.str().substr(0, expectedTimeFormat.size()), expectedTimeFormat); |
161 | } |
162 | } |
163 | } |
164 | |
165 | bool has_unicode_classic_locale() |
166 | { |
167 | std::locale l = std::locale::classic(); |
168 | for(const auto name : {"C.UTF-8" , "C.utf8" }) { |
169 | try { |
170 | l = std::locale(name); |
171 | break; |
172 | } catch(...) { |
173 | } |
174 | } |
175 | const wchar_t s = L'\u03B4'; |
176 | // Check that that the Unicode character is handled |
177 | return std::use_facet<std::ctype<wchar_t>>(loc: l).toupper(c: s) != s; |
178 | } |
179 | |
180 | // For a non-existing locale the C locale will be used as a fallback |
181 | // If UTF-8 is requested/reported then UTF-8 will still be used as much as possible |
182 | void test_invalid_locale() |
183 | { |
184 | std::ostringstream classicStream; |
185 | classicStream.imbue(loc: std::locale::classic()); |
186 | const boost::locale::util::locale_data systemLocale(boost::locale::util::get_system_locale()); |
187 | |
188 | bl::localization_backend_manager tmp_backend = bl::localization_backend_manager::global(); |
189 | tmp_backend.select(backend_name: "std" ); |
190 | bl::localization_backend_manager::global(tmp_backend); |
191 | bl::generator g; |
192 | std::locale nonExistingLocale = g("noLang_noCountry." + systemLocale.encoding()); |
193 | const auto& info = std::use_facet<bl::info>(loc: nonExistingLocale); |
194 | TEST_EQ(info.language(), "nolang" ); |
195 | TEST_EQ(info.country(), "NOCOUNTRY" ); |
196 | std::ostringstream os; |
197 | os.imbue(loc: nonExistingLocale); |
198 | os << boost::locale::as::number << 123456789 << " " << 1234567.89; |
199 | classicStream << 123456789 << " " << 1234567.89; |
200 | TEST_EQ(os.str(), classicStream.str()); |
201 | |
202 | // Request UTF-8 explicitly and check that it is used even when the locale doesn't exist |
203 | if(!info.utf8()) |
204 | nonExistingLocale = g("noLang_noCountry.UTF-8" ); |
205 | if(has_unicode_classic_locale()) { |
206 | // Case conversion works only if the backend supports classic locale with Unicode |
207 | TEST_EQ(boost::locale::to_upper("δ" , nonExistingLocale), "Δ" ); |
208 | } |
209 | // The codecvt facet always supports UTF-8 |
210 | { |
211 | auto& cvt = std::use_facet<std::codecvt<wchar_t, char, std::mbstate_t>>(loc: nonExistingLocale); |
212 | // String with Unicode chars from different cultures |
213 | const std::wstring wide_str = L"\U0001D49E-\u043F\u0440\u0438\u0432\u0435\u0442-\u3084\u3042" ; |
214 | |
215 | std::mbstate_t state{}; |
216 | const wchar_t* from_next = wide_str.c_str(); |
217 | const wchar_t* from_end = from_next + wide_str.size(); |
218 | char out_str[32]{}; |
219 | char* to_next = out_str; |
220 | TEST_EQ(cvt.out(state, from_next, from_end, from_next, out_str, out_str + sizeof(out_str), to_next), cvt.ok); |
221 | const std::string utf8_str = boost::locale::conv::utf_to_utf<char>(str: wide_str); |
222 | TEST_EQ(out_str, utf8_str); |
223 | } |
224 | } |
225 | |
226 | void test_install_chartype(const std::string& backendName) |
227 | { |
228 | // Use ASCII and UTF-8 encoding |
229 | for(const std::string localeName : {"C" , "en_US.UTF-8" }) { |
230 | std::cout << "--- Locale: " << localeName << std::endl; |
231 | const std::locale origLocale = bl::generator{}(localeName); |
232 | const auto backend = bl::localization_backend_manager::global().create(); |
233 | backend->set_option(name: "locale" , value: localeName); |
234 | for(auto category = bl::per_character_facet_first; category <= bl::per_character_facet_last; ++category) { |
235 | std::cout << "---- Testing category " << static_cast<unsigned>(category) << '\n'; |
236 | // This should modify the locale |
237 | const std::locale newLocale_char = backend->install(base: origLocale, category, type: bl::char_facet_t::char_f); |
238 | // This should not |
239 | const std::locale newLocale_nochar = backend->install(base: origLocale, category, type: bl::char_facet_t::nochar); |
240 | // But the boundary facet is only implemented in ICU, so for all else the locale is still unchanged |
241 | if(category != bl::category_t::boundary || backendName == "icu" ) |
242 | TEST(origLocale != newLocale_char); |
243 | else |
244 | TEST(origLocale == newLocale_char); |
245 | TEST(origLocale == newLocale_nochar); |
246 | } |
247 | } |
248 | } |
249 | |
250 | template<typename Char> |
251 | struct dummy_collate : std::collate<Char> {}; |
252 | |
253 | template<typename Char> |
254 | bool has_dummy_collate(const std::locale& l) |
255 | { |
256 | const auto& col = std::use_facet<std::collate<Char>>(l); // Implicitely require existance of std::collate |
257 | return blt::is_facet<dummy_collate<Char>>(&col); |
258 | } |
259 | |
260 | void test_std_collate_replaced(const std::string& /*backendName*/) |
261 | { |
262 | std::locale origLocale = std::locale::classic(); |
263 | origLocale = std::locale(origLocale, new dummy_collate<char>); |
264 | origLocale = std::locale(origLocale, new dummy_collate<wchar_t>); |
265 | #ifdef BOOST_LOCALE_ENABLE_CHAR16_T |
266 | origLocale = std::locale(origLocale, new dummy_collate<char16_t>); |
267 | #endif |
268 | #ifdef BOOST_LOCALE_ENABLE_CHAR32_T |
269 | origLocale = std::locale(origLocale, new dummy_collate<char32_t>); |
270 | #endif |
271 | |
272 | // Use ASCII and UTF-8 encoding |
273 | for(const std::string localeName : {"C" , "en_US.UTF-8" }) { |
274 | std::cout << "--- Locale: " << localeName << std::endl; |
275 | bl::generator g; |
276 | g.categories(cats: boost::locale::category_t::collation); |
277 | const std::locale l = g.generate(base: origLocale, id: localeName); |
278 | TEST(has_dummy_collate<char>(origLocale)); |
279 | TEST(!has_dummy_collate<char>(l)); |
280 | TEST(has_dummy_collate<wchar_t>(origLocale)); |
281 | TEST(!has_dummy_collate<wchar_t>(l)); |
282 | #ifdef BOOST_LOCALE_ENABLE_CHAR16_T |
283 | TEST(has_dummy_collate<char16_t>(origLocale)); |
284 | TEST(!has_dummy_collate<char16_t>(l)); |
285 | #endif |
286 | #ifdef BOOST_LOCALE_ENABLE_CHAR32_T |
287 | TEST(has_dummy_collate<char32_t>(origLocale)); |
288 | TEST(!has_dummy_collate<char32_t>(l)); |
289 | #endif |
290 | } |
291 | } |
292 | |
293 | void test_main(int /*argc*/, char** /*argv*/) |
294 | { |
295 | { |
296 | std::vector<std::string> backends; |
297 | #ifdef BOOST_LOCALE_WITH_ICU |
298 | backends.push_back(x: "icu" ); |
299 | #endif |
300 | #ifndef BOOST_LOCALE_NO_STD_BACKEND |
301 | backends.push_back(x: "std" ); |
302 | #endif |
303 | #ifndef BOOST_LOCALE_NO_WINAPI_BACKEND |
304 | backends.push_back("winapi" ); |
305 | #endif |
306 | #ifndef BOOST_LOCALE_NO_POSIX_BACKEND |
307 | backends.push_back(x: "posix" ); |
308 | #endif |
309 | std::sort(first: backends.begin(), last: backends.end()); |
310 | |
311 | std::vector<std::string> all_backends = bl::localization_backend_manager::global().get_all_backends(); |
312 | std::sort(first: all_backends.begin(), last: all_backends.end()); |
313 | TEST_EQ(all_backends, backends); |
314 | } |
315 | |
316 | const bl::localization_backend_manager orig_backend = bl::localization_backend_manager::global(); |
317 | for(const std::string& backendName : orig_backend.get_all_backends()) { |
318 | std::cout << "Backend: " << backendName << std::endl; |
319 | bl::localization_backend_manager tmp_backend = bl::localization_backend_manager::global(); |
320 | tmp_backend.select(backend_name: backendName); |
321 | bl::localization_backend_manager::global(tmp_backend); |
322 | bl::generator g; |
323 | for(const std::string localeName : {"" , "C" , "en_US.UTF-8" , "en_US.ISO8859-1" , "tr_TR.windows1254" }) { |
324 | std::cout << "-- Locale: " << localeName << std::endl; |
325 | const std::locale l = g(localeName); |
326 | #ifdef __cpp_char8_t |
327 | # define TEST_FOR_CHAR8(check) TEST(check) |
328 | #else |
329 | # define TEST_FOR_CHAR8(check) (void)0 |
330 | #endif |
331 | #ifndef BOOST_LOCALE_NO_CXX20_STRING8 |
332 | # define TEST_FOR_STRING8(check) TEST(check) |
333 | #else |
334 | # define TEST_FOR_STRING8(check) (void)0 |
335 | #endif |
336 | #ifdef BOOST_LOCALE_ENABLE_CHAR16_T |
337 | # define TEST_FOR_CHAR16(check) TEST(check) |
338 | #else |
339 | # define TEST_FOR_CHAR16(check) (void)0 |
340 | #endif |
341 | #ifdef BOOST_LOCALE_ENABLE_CHAR32_T |
342 | # define TEST_FOR_CHAR32(check) TEST(check) |
343 | #else |
344 | # define TEST_FOR_CHAR32(check) (void)0 |
345 | #endif |
346 | #define TEST_HAS_FACET_CHAR8(facet, l) TEST_FOR_CHAR8(blt::has_facet<facet<char8_t>>(l)) |
347 | #define TEST_HAS_FACET_CHAR16(facet, l) TEST_FOR_CHAR16(blt::has_facet<facet<char16_t>>(l)) |
348 | #define TEST_HAS_FACET_CHAR32(facet, l) TEST_FOR_CHAR32(blt::has_facet<facet<char32_t>>(l)) |
349 | #define TEST_HAS_FACET_STRING8(facet, l) TEST_FOR_STRING8(blt::has_facet<facet<char8_t>>(l)) |
350 | |
351 | #define TEST_HAS_FACETS(facet, l) \ |
352 | do { \ |
353 | TEST(blt::has_facet<facet<char>>(l)); \ |
354 | TEST(blt::has_facet<facet<wchar_t>>(l)); \ |
355 | TEST_HAS_FACET_CHAR16(facet, l); \ |
356 | TEST_HAS_FACET_CHAR32(facet, l); \ |
357 | } while(false) |
358 | |
359 | // Convert |
360 | TEST_HAS_FACETS(bl::converter, l); |
361 | TEST_HAS_FACET_STRING8(bl::converter, l); |
362 | // Collator |
363 | TEST_HAS_FACETS(std::collate, l); |
364 | if(backendName == "icu" || (backendName == "winapi" && std::use_facet<bl::info>(loc: l).utf8())) { |
365 | TEST_HAS_FACETS(bl::collator, l); |
366 | TEST_HAS_FACET_STRING8(bl::collator, l); |
367 | } else { |
368 | TEST(blt::has_not_facet<bl::collator<char>>(l)); |
369 | TEST(blt::has_not_facet<bl::collator<wchar_t>>(l)); |
370 | TEST_FOR_STRING8(blt::has_not_facet<bl::collator<char8_t>>(l)); |
371 | TEST_FOR_CHAR16(blt::has_not_facet<bl::collator<char16_t>>(l)); |
372 | TEST_FOR_CHAR32(blt::has_not_facet<bl::collator<char32_t>>(l)); |
373 | } |
374 | // Formatting |
375 | TEST_HAS_FACETS(std::num_put, l); |
376 | TEST_HAS_FACETS(std::time_put, l); |
377 | TEST_HAS_FACETS(std::numpunct, l); |
378 | TEST_HAS_FACETS(std::moneypunct, l); |
379 | // Parsing |
380 | TEST_HAS_FACETS(std::num_get, l); |
381 | // Message |
382 | TEST_HAS_FACETS(bl::message_format, l); |
383 | TEST_HAS_FACET_STRING8(bl::message_format, l); |
384 | // Codepage |
385 | TEST_HAS_FACETS(codecvt_by_char_type, l); |
386 | // Boundary |
387 | if(backendName == "icu" ) { |
388 | TEST_HAS_FACETS(bl::boundary::boundary_indexing, l); |
389 | TEST_HAS_FACET_CHAR8(bl::boundary::boundary_indexing, l); |
390 | } |
391 | // calendar |
392 | TEST(blt::has_facet<bl::calendar_facet>(l)); |
393 | // information |
394 | TEST(blt::has_facet<bl::info>(l)); |
395 | } |
396 | |
397 | std::locale l = g("en_US.UTF-8" ); |
398 | TEST(has_message(l)); |
399 | g.categories(cats: g.categories() ^ bl::category_t::message); |
400 | g.locale_cache_enabled(on: true); |
401 | g("en_US.UTF-8" ); |
402 | g.categories(cats: g.categories() | bl::category_t::message); |
403 | l = g("en_US.UTF-8" ); |
404 | TEST(!has_message(l)); |
405 | g.clear_cache(); |
406 | g.locale_cache_enabled(on: false); |
407 | l = g("en_US.UTF-8" ); |
408 | TEST(has_message(l)); |
409 | g.characters(chars: g.characters() ^ bl::char_facet_t::char_f); |
410 | l = g("en_US.UTF-8" ); |
411 | TEST(!has_message(l)); |
412 | g.characters(chars: g.characters() | bl::char_facet_t::char_f); |
413 | l = g("en_US.UTF-8" ); |
414 | TEST(has_message(l)); |
415 | |
416 | l = g("en_US.ISO8859-1" ); |
417 | { |
418 | const auto& info = std::use_facet<bl::info>(loc: l); |
419 | TEST_EQ(info.language(), "en" ); |
420 | TEST_EQ(info.country(), "US" ); |
421 | TEST(!info.utf8()); |
422 | TEST_EQ(info.variant(), "" ); |
423 | TEST_EQ(info.encoding(), "ISO8859-1" ); |
424 | } |
425 | l = g("en_US.UTF-8" ); |
426 | { |
427 | const auto& info = std::use_facet<bl::info>(loc: l); |
428 | TEST_EQ(info.language(), "en" ); |
429 | TEST_EQ(info.country(), "US" ); |
430 | TEST(info.utf8()); |
431 | TEST_EQ(info.variant(), "" ); |
432 | TEST_EQ(info.encoding(), "UTF-8" ); |
433 | } |
434 | l = g("da_DK.ISO8859-15@euro" ); |
435 | { |
436 | const auto& info = std::use_facet<bl::info>(loc: l); |
437 | TEST_EQ(info.language(), "da" ); |
438 | TEST_EQ(info.country(), "DK" ); |
439 | TEST(!info.utf8()); |
440 | TEST_EQ(info.variant(), "euro" ); |
441 | TEST_EQ(info.encoding(), "ISO8859-15" ); |
442 | } |
443 | l = g("en_US.ISO8859-1" ); |
444 | { |
445 | const auto& info = std::use_facet<bl::info>(loc: l); |
446 | TEST_EQ(info.language(), "en" ); |
447 | TEST_EQ(info.country(), "US" ); |
448 | TEST(!info.utf8()); |
449 | TEST_EQ(info.variant(), "" ); |
450 | TEST_EQ(info.encoding(), "ISO8859-1" ); |
451 | } |
452 | |
453 | // Check that generate() extends the given locale, not replaces it |
454 | std::locale l_wt(std::locale::classic(), new test_facet); |
455 | TEST(blt::has_facet<test_facet>(g.generate(l_wt, "en_US.UTF-8" ))); |
456 | TEST(!blt::has_facet<test_facet>(g.generate("en_US.UTF-8" ))); |
457 | TEST(blt::has_facet<test_facet>(g.generate(l_wt, "en_US.ISO8859-1" ))); |
458 | TEST(!blt::has_facet<test_facet>(g.generate("en_US.ISO8859-1" ))); |
459 | |
460 | // Check caching works |
461 | g.locale_cache_enabled(on: true); |
462 | // Generate a locale with a specific facet which is then cached |
463 | g.generate(base: l_wt, id: "en_US.UTF-8" ); |
464 | g.generate(base: l_wt, id: "en_US.ISO8859-1" ); |
465 | // Cached locale is returned -> facet is still there |
466 | TEST(blt::has_facet<test_facet>(g("en_US.UTF-8" ))); |
467 | TEST(blt::has_facet<test_facet>(g("en_US.ISO8859-1" ))); |
468 | // Check a property to verify it doesn't simply return the same locale for each call |
469 | TEST(std::use_facet<bl::info>(g("en_US.UTF-8" )).utf8()); |
470 | TEST(!std::use_facet<bl::info>(g("en_US.ISO8859-1" )).utf8()); |
471 | |
472 | test_install_chartype(backendName); |
473 | test_std_collate_replaced(backendName); |
474 | } |
475 | std::cout << "Test special locales" << std::endl; |
476 | test_special_locales(); |
477 | test_invalid_locale(); |
478 | } |
479 | |