1 | /* |
2 | * Copyright (C) 2011,2012 Google Inc. All rights reserved. |
3 | * |
4 | * Redistribution and use in source and binary forms, with or without |
5 | * modification, are permitted provided that the following conditions are |
6 | * met: |
7 | * |
8 | * * Redistributions of source code must retain the above copyright |
9 | * notice, this list of conditions and the following disclaimer. |
10 | * * Redistributions in binary form must reproduce the above |
11 | * copyright notice, this list of conditions and the following disclaimer |
12 | * in the documentation and/or other materials provided with the |
13 | * distribution. |
14 | * * Neither the name of Google Inc. nor the names of its |
15 | * contributors may be used to endorse or promote products derived from |
16 | * this software without specific prior written permission. |
17 | * |
18 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
19 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
20 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
21 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT |
22 | * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
23 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
24 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
25 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
26 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
27 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
28 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
29 | */ |
30 | |
31 | #include "config.h" |
32 | #include "PlatformLocale.h" |
33 | |
34 | #include "DateTimeFormat.h" |
35 | #include "LocalizedStrings.h" |
36 | #include <wtf/text/StringBuilder.h> |
37 | |
38 | namespace WebCore { |
39 | |
40 | #if ENABLE(DATE_AND_TIME_INPUT_TYPES) |
41 | |
42 | class DateTimeStringBuilder : private DateTimeFormat::TokenHandler { |
43 | WTF_MAKE_NONCOPYABLE(DateTimeStringBuilder); |
44 | |
45 | public: |
46 | // The argument objects must be alive until this object dies. |
47 | DateTimeStringBuilder(Locale&, const DateComponents&); |
48 | |
49 | bool build(const String&); |
50 | String toString(); |
51 | |
52 | private: |
53 | // DateTimeFormat::TokenHandler functions. |
54 | void visitField(DateTimeFormat::FieldType, int) final; |
55 | void visitLiteral(const String&) final; |
56 | |
57 | String zeroPadString(const String&, size_t width); |
58 | void appendNumber(int number, size_t width); |
59 | |
60 | StringBuilder m_builder; |
61 | Locale& m_localizer; |
62 | const DateComponents& m_date; |
63 | }; |
64 | |
65 | DateTimeStringBuilder::DateTimeStringBuilder(Locale& localizer, const DateComponents& date) |
66 | : m_localizer(localizer) |
67 | , m_date(date) |
68 | { |
69 | } |
70 | |
71 | bool DateTimeStringBuilder::build(const String& formatString) |
72 | { |
73 | m_builder.reserveCapacity(formatString.length()); |
74 | return DateTimeFormat::parse(formatString, *this); |
75 | } |
76 | |
77 | String DateTimeStringBuilder::zeroPadString(const String& string, size_t width) |
78 | { |
79 | if (string.length() >= width) |
80 | return string; |
81 | StringBuilder zeroPaddedStringBuilder; |
82 | zeroPaddedStringBuilder.reserveCapacity(width); |
83 | for (size_t i = string.length(); i < width; ++i) |
84 | zeroPaddedStringBuilder.append('0'); |
85 | zeroPaddedStringBuilder.append(string); |
86 | return zeroPaddedStringBuilder.toString(); |
87 | } |
88 | |
89 | void DateTimeStringBuilder::appendNumber(int number, size_t width) |
90 | { |
91 | String zeroPaddedNumberString = zeroPadString(String::number(number), width); |
92 | m_builder.append(m_localizer.convertToLocalizedNumber(zeroPaddedNumberString)); |
93 | } |
94 | |
95 | void DateTimeStringBuilder::visitField(DateTimeFormat::FieldType fieldType, int numberOfPatternCharacters) |
96 | { |
97 | switch (fieldType) { |
98 | case DateTimeFormat::FieldTypeYear: |
99 | // Always use padding width of 4 so it matches DateTimeEditElement. |
100 | appendNumber(m_date.fullYear(), 4); |
101 | return; |
102 | case DateTimeFormat::FieldTypeMonth: |
103 | if (numberOfPatternCharacters == 3) |
104 | m_builder.append(m_localizer.shortMonthLabels()[m_date.month()]); |
105 | else if (numberOfPatternCharacters == 4) |
106 | m_builder.append(m_localizer.monthLabels()[m_date.month()]); |
107 | else { |
108 | // Always use padding width of 2 so it matches DateTimeEditElement. |
109 | appendNumber(m_date.month() + 1, 2); |
110 | } |
111 | return; |
112 | case DateTimeFormat::FieldTypeMonthStandAlone: |
113 | if (numberOfPatternCharacters == 3) |
114 | m_builder.append(m_localizer.shortStandAloneMonthLabels()[m_date.month()]); |
115 | else if (numberOfPatternCharacters == 4) |
116 | m_builder.append(m_localizer.standAloneMonthLabels()[m_date.month()]); |
117 | else { |
118 | // Always use padding width of 2 so it matches DateTimeEditElement. |
119 | appendNumber(m_date.month() + 1, 2); |
120 | } |
121 | return; |
122 | case DateTimeFormat::FieldTypeDayOfMonth: |
123 | // Always use padding width of 2 so it matches DateTimeEditElement. |
124 | appendNumber(m_date.monthDay(), 2); |
125 | return; |
126 | case DateTimeFormat::FieldTypeWeekOfYear: |
127 | // Always use padding width of 2 so it matches DateTimeEditElement. |
128 | appendNumber(m_date.week(), 2); |
129 | return; |
130 | case DateTimeFormat::FieldTypePeriod: |
131 | m_builder.append(m_localizer.timeAMPMLabels()[(m_date.hour() >= 12 ? 1 : 0)]); |
132 | return; |
133 | case DateTimeFormat::FieldTypeHour12: { |
134 | int hour12 = m_date.hour() % 12; |
135 | if (!hour12) |
136 | hour12 = 12; |
137 | appendNumber(hour12, numberOfPatternCharacters); |
138 | return; |
139 | } |
140 | case DateTimeFormat::FieldTypeHour23: |
141 | appendNumber(m_date.hour(), numberOfPatternCharacters); |
142 | return; |
143 | case DateTimeFormat::FieldTypeHour11: |
144 | appendNumber(m_date.hour() % 12, numberOfPatternCharacters); |
145 | return; |
146 | case DateTimeFormat::FieldTypeHour24: { |
147 | int hour24 = m_date.hour(); |
148 | if (!hour24) |
149 | hour24 = 24; |
150 | appendNumber(hour24, numberOfPatternCharacters); |
151 | return; |
152 | } |
153 | case DateTimeFormat::FieldTypeMinute: |
154 | appendNumber(m_date.minute(), numberOfPatternCharacters); |
155 | return; |
156 | case DateTimeFormat::FieldTypeSecond: |
157 | if (!m_date.millisecond()) |
158 | appendNumber(m_date.second(), numberOfPatternCharacters); |
159 | else { |
160 | double second = m_date.second() + m_date.millisecond() / 1000.0; |
161 | String zeroPaddedSecondString = zeroPadString(String::numberToStringFixedWidth(second, 3), numberOfPatternCharacters + 4); |
162 | m_builder.append(m_localizer.convertToLocalizedNumber(zeroPaddedSecondString)); |
163 | } |
164 | return; |
165 | default: |
166 | return; |
167 | } |
168 | } |
169 | |
170 | void DateTimeStringBuilder::visitLiteral(const String& text) |
171 | { |
172 | ASSERT(text.length()); |
173 | m_builder.append(text); |
174 | } |
175 | |
176 | String DateTimeStringBuilder::toString() |
177 | { |
178 | return m_builder.toString(); |
179 | } |
180 | |
181 | #endif |
182 | |
183 | Locale::~Locale() = default; |
184 | |
185 | void Locale::setLocaleData(const Vector<String, DecimalSymbolsSize>& symbols, const String& positivePrefix, const String& positiveSuffix, const String& negativePrefix, const String& negativeSuffix) |
186 | { |
187 | for (size_t i = 0; i < symbols.size(); ++i) { |
188 | ASSERT(!symbols[i].isEmpty()); |
189 | m_decimalSymbols[i] = symbols[i]; |
190 | } |
191 | m_positivePrefix = positivePrefix; |
192 | m_positiveSuffix = positiveSuffix; |
193 | m_negativePrefix = negativePrefix; |
194 | m_negativeSuffix = negativeSuffix; |
195 | ASSERT(!m_positivePrefix.isEmpty() || !m_positiveSuffix.isEmpty() || !m_negativePrefix.isEmpty() || !m_negativeSuffix.isEmpty()); |
196 | m_hasLocaleData = true; |
197 | } |
198 | |
199 | String Locale::convertToLocalizedNumber(const String& input) |
200 | { |
201 | initializeLocaleData(); |
202 | if (!m_hasLocaleData || input.isEmpty()) |
203 | return input; |
204 | |
205 | unsigned i = 0; |
206 | bool isNegative = false; |
207 | StringBuilder builder; |
208 | builder.reserveCapacity(input.length()); |
209 | |
210 | if (input[0] == '-') { |
211 | ++i; |
212 | isNegative = true; |
213 | builder.append(m_negativePrefix); |
214 | } else |
215 | builder.append(m_positivePrefix); |
216 | |
217 | for (; i < input.length(); ++i) { |
218 | switch (input[i]) { |
219 | case '0': |
220 | case '1': |
221 | case '2': |
222 | case '3': |
223 | case '4': |
224 | case '5': |
225 | case '6': |
226 | case '7': |
227 | case '8': |
228 | case '9': |
229 | builder.append(m_decimalSymbols[input[i] - '0']); |
230 | break; |
231 | case '.': |
232 | builder.append(m_decimalSymbols[DecimalSeparatorIndex]); |
233 | break; |
234 | default: |
235 | ASSERT_NOT_REACHED(); |
236 | } |
237 | } |
238 | |
239 | builder.append(isNegative ? m_negativeSuffix : m_positiveSuffix); |
240 | |
241 | return builder.toString(); |
242 | } |
243 | |
244 | static bool matches(const String& text, unsigned position, const String& part) |
245 | { |
246 | if (part.isEmpty()) |
247 | return true; |
248 | if (position + part.length() > text.length()) |
249 | return false; |
250 | for (unsigned i = 0; i < part.length(); ++i) { |
251 | if (text[position + i] != part[i]) |
252 | return false; |
253 | } |
254 | return true; |
255 | } |
256 | |
257 | bool Locale::detectSignAndGetDigitRange(const String& input, bool& isNegative, unsigned& startIndex, unsigned& endIndex) |
258 | { |
259 | startIndex = 0; |
260 | endIndex = input.length(); |
261 | if (m_negativePrefix.isEmpty() && m_negativeSuffix.isEmpty()) { |
262 | if (input.startsWith(m_positivePrefix) && input.endsWith(m_positiveSuffix)) { |
263 | isNegative = false; |
264 | startIndex = m_positivePrefix.length(); |
265 | endIndex -= m_positiveSuffix.length(); |
266 | } else |
267 | isNegative = true; |
268 | } else { |
269 | if (input.startsWith(m_negativePrefix) && input.endsWith(m_negativeSuffix)) { |
270 | isNegative = true; |
271 | startIndex = m_negativePrefix.length(); |
272 | endIndex -= m_negativeSuffix.length(); |
273 | } else { |
274 | isNegative = false; |
275 | if (input.startsWith(m_positivePrefix) && input.endsWith(m_positiveSuffix)) { |
276 | startIndex = m_positivePrefix.length(); |
277 | endIndex -= m_positiveSuffix.length(); |
278 | } else |
279 | return false; |
280 | } |
281 | } |
282 | return true; |
283 | } |
284 | |
285 | unsigned Locale::matchedDecimalSymbolIndex(const String& input, unsigned& position) |
286 | { |
287 | for (unsigned symbolIndex = 0; symbolIndex < DecimalSymbolsSize; ++symbolIndex) { |
288 | if (m_decimalSymbols[symbolIndex].length() && matches(input, position, m_decimalSymbols[symbolIndex])) { |
289 | position += m_decimalSymbols[symbolIndex].length(); |
290 | return symbolIndex; |
291 | } |
292 | } |
293 | return DecimalSymbolsSize; |
294 | } |
295 | |
296 | String Locale::convertFromLocalizedNumber(const String& localized) |
297 | { |
298 | initializeLocaleData(); |
299 | String input = localized.stripWhiteSpace(); |
300 | if (!m_hasLocaleData || input.isEmpty()) |
301 | return input; |
302 | |
303 | bool isNegative; |
304 | unsigned startIndex; |
305 | unsigned endIndex; |
306 | if (!detectSignAndGetDigitRange(input, isNegative, startIndex, endIndex)) |
307 | return input; |
308 | |
309 | StringBuilder builder; |
310 | builder.reserveCapacity(input.length()); |
311 | if (isNegative) |
312 | builder.append('-'); |
313 | for (unsigned i = startIndex; i < endIndex;) { |
314 | unsigned symbolIndex = matchedDecimalSymbolIndex(input, i); |
315 | if (symbolIndex >= DecimalSymbolsSize) |
316 | return input; |
317 | if (symbolIndex == DecimalSeparatorIndex) |
318 | builder.append('.'); |
319 | else if (symbolIndex == GroupSeparatorIndex) |
320 | return input; |
321 | else |
322 | builder.append(static_cast<UChar>('0' + symbolIndex)); |
323 | } |
324 | return builder.toString(); |
325 | } |
326 | |
327 | #if ENABLE(DATE_AND_TIME_INPUT_TYPES) |
328 | |
329 | #if !PLATFORM(IOS_FAMILY) |
330 | String Locale::formatDateTime(const DateComponents& date, FormatType formatType) |
331 | { |
332 | if (date.type() == DateComponents::Invalid) |
333 | return String(); |
334 | #if !ENABLE(INPUT_TYPE_WEEK) |
335 | if (date.type() == DateComponents::Week) |
336 | return String(); |
337 | #endif |
338 | |
339 | DateTimeStringBuilder builder(*this, date); |
340 | switch (date.type()) { |
341 | case DateComponents::Time: |
342 | builder.build(formatType == FormatTypeShort ? shortTimeFormat() : timeFormat()); |
343 | break; |
344 | case DateComponents::Date: |
345 | builder.build(dateFormat()); |
346 | break; |
347 | case DateComponents::Month: |
348 | builder.build(formatType == FormatTypeShort ? shortMonthFormat() : monthFormat()); |
349 | break; |
350 | case DateComponents::Week: |
351 | #if ENABLE(INPUT_TYPE_WEEK) |
352 | builder.build(weekFormatInLDML()); |
353 | break; |
354 | #endif |
355 | case DateComponents::DateTime: |
356 | case DateComponents::DateTimeLocal: |
357 | builder.build(formatType == FormatTypeShort ? dateTimeFormatWithoutSeconds() : dateTimeFormatWithSeconds()); |
358 | break; |
359 | case DateComponents::Invalid: |
360 | ASSERT_NOT_REACHED(); |
361 | break; |
362 | } |
363 | return builder.toString(); |
364 | } |
365 | #endif // !PLATFORM(IOS_FAMILY) |
366 | |
367 | #endif |
368 | |
369 | } |
370 | |