diff --git a/source/detail/number_formatter.cpp b/source/detail/number_formatter.cpp new file mode 100644 index 00000000..145569ff --- /dev/null +++ b/source/detail/number_formatter.cpp @@ -0,0 +1,1214 @@ +// Copyright (c) 2014-2016 Thomas Fussell +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, WRISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE +// +// @license: http://www.opensource.org/licenses/mit-license.php +// @author: see AUTHORS file + +#include + +namespace xlnt { +namespace detail { + +const std::unordered_map known_locales() +{ + const std::unordered_map *all = + new std::unordered_map( + { + { 0x401, "Arabic - Saudi Arabia" }, + { 0x402, "Bulgarian" }, + { 0x403, "Catalan" }, + { 0x404, "Chinese - Taiwan" }, + { 0x405, "Czech" }, + { 0x406, "Danish" }, + { 0x407, "German - Germany" }, + { 0x408, "Greek" }, + { 0x409, "English - United States" }, + { 0x410, "Italian - Italy" }, + { 0x411, "Japanese" }, + { 0x412, "Korean" }, + { 0x413, "Dutch - Netherlands" }, + { 0x414, "Norwegian - Bokml" }, + { 0x415, "Polish" }, + { 0x416, "Portuguese - Brazil" }, + { 0x417, "Raeto-Romance" }, + { 0x418, "Romanian - Romania" }, + { 0x419, "Russian" }, + { 0x420, "Urdu" }, + { 0x421, "Indonesian" }, + { 0x422, "Ukrainian" }, + { 0x423, "Belarusian" }, + { 0x424, "Slovenian" }, + { 0x425, "Estonian" }, + { 0x426, "Latvian" }, + { 0x427, "Lithuanian" }, + { 0x428, "Tajik" }, + { 0x429, "Farsi - Persian" }, + { 0x430, "Sesotho (Sutu)" }, + { 0x431, "Tsonga" }, + { 0x432, "Setsuana" }, + { 0x433, "Venda" }, + { 0x434, "Xhosa" }, + { 0x435, "Zulu" }, + { 0x436, "Afrikaans" }, + { 0x437, "Georgian" }, + { 0x438, "Faroese" }, + { 0x439, "Hindi" }, + { 0x440, "Kyrgyz - Cyrillic" }, + { 0x441, "Swahili" }, + { 0x442, "Turkmen" }, + { 0x443, "Uzbek - Latin" }, + { 0x444, "Tatar" }, + { 0x445, "Bengali - India" }, + { 0x446, "Punjabi" }, + { 0x447, "Gujarati" }, + { 0x448, "Oriya" }, + { 0x449, "Tamil" }, + { 0x450, "Mongolian" }, + { 0x451, "Tibetan" }, + { 0x452, "Welsh" }, + { 0x453, "Khmer" }, + { 0x454, "Lao" }, + { 0x455, "Burmese" }, + { 0x456, "Galician" }, + { 0x457, "Konkani" }, + { 0x458, "Manipuri" }, + { 0x459, "Sindhi" }, + { 0x460, "Kashmiri" }, + { 0x461, "Nepali" }, + { 0x462, "Frisian - Netherlands" }, + { 0x464, "Filipino" }, + { 0x465, "Divehi; Dhivehi; Maldivian" }, + { 0x466, "Edo" }, + { 0x470, "Igbo - Nigeria" }, + { 0x474, "Guarani - Paraguay" }, + { 0x476, "Latin" }, + { 0x477, "Somali" }, + { 0x481, "Maori" }, + { 0x801, "Arabic - Iraq" }, + { 0x804, "Chinese - China" }, + { 0x807, "German - Switzerland" }, + { 0x809, "English - Great Britain" }, + { 0x810, "Italian - Switzerland" }, + { 0x813, "Dutch - Belgium" }, + { 0x814, "Norwegian - Nynorsk" }, + { 0x816, "Portuguese - Portugal" }, + { 0x818, "Romanian - Moldova" }, + { 0x819, "Russian - Moldova" }, + { 0x843, "Uzbek - Cyrillic" }, + { 0x845, "Bengali - Bangladesh" }, + { 0x850, "Mongolian" }, + { 0x1001, "Arabic - Libya" }, + { 0x1004, "Chinese - Singapore" }, + { 0x1007, "German - Luxembourg" }, + { 0x1009, "English - Canada" }, + { 0x1401, "Arabic - Algeria" }, + { 0x1404, "Chinese - Macau SAR" }, + { 0x1407, "German - Liechtenstein" }, + { 0x1409, "English - New Zealand" }, + { 0x1801, "Arabic - Morocco" }, + { 0x1809, "English - Ireland" }, + { 0x2001, "Arabic - Oman" }, + { 0x2009, "English - Jamaica" }, + { 0x2401, "Arabic - Yemen" }, + { 0x2409, "English - Caribbean" }, + { 0x2801, "Arabic - Syria" }, + { 0x2809, "English - Belize" }, + { 0x3001, "Arabic - Lebanon" }, + { 0x3009, "English - Zimbabwe" }, + { 0x3401, "Arabic - Kuwait" }, + { 0x3409, "English - Phillippines" }, + { 0x3801, "Arabic - United Arab Emirates" }, + { 0x4001, "Arabic - Qatar" } + }); + + return *all; +} + +bool condition::satisfied_by(long double number) const +{ + switch (type) + { + case condition_type::equal: + return number == value; + case condition_type::greater_or_equal: + return number >= value; + case condition_type::greater_than: + return number > value; + case condition_type::less_or_equal: + return number <= value; + case condition_type::less_than: + return number < value; + case condition_type::not_equal: + return number != value; + case condition_type::none: + return false; + } +} + +number_format_parser::number_format_parser(const std::string &format_string) +{ + reset(format_string); +} + +const std::vector &number_format_parser::get_result() const +{ + return codes_; +} + +void number_format_parser::reset(const std::string &format_string) +{ + format_string_ = format_string; + position_ = 0; + codes_.clear(); +} + +void number_format_parser::parse() +{ + auto token = parse_next_token(); + format_code section; + template_part part; + + for (;;) + { + switch (token.type) + { + case number_format_token::token_type::end_section: + codes_.push_back(section); + section = format_code(); + + break; + + case number_format_token::token_type::bad: + throw std::runtime_error("bad format"); + + case number_format_token::token_type::color: + if (section.color != bracket_color::none + || section.condition.type != condition::condition_type::none + || section.locale != locale::none + || !section.parts.empty()) + { + throw std::runtime_error("color should be the first part of a format"); + } + + section.color = color_from_string(token.string); + break; + + case number_format_token::token_type::locale: + { + if (section.locale != locale::none) + { + throw std::runtime_error("multiple locales"); + } + + auto parsed_locale = locale_from_string(token.string); + section.locale = parsed_locale.first; + + if (!parsed_locale.second.empty()) + { + part.type = template_part::template_type::text; + part.string = parsed_locale.second; + section.parts.push_back(part); + part = template_part(); + } + + break; + } + + case number_format_token::token_type::condition: + { + if (section.condition.type != condition::condition_type::none) + { + throw std::runtime_error("multiple conditions"); + } + + std::string value; + + if (token.string.front() == '<') + { + if (token.string[1] == '=') + { + section.condition.type = condition::condition_type::less_or_equal; + value = token.string.substr(2); + } + else if (token.string[1] == '>') + { + section.condition.type = condition::condition_type::not_equal; + value = token.string.substr(2); + } + else + { + section.condition.type = condition::condition_type::less_than; + value = token.string.substr(1); + } + } + else if (token.string.front() == '>') + { + if (token.string[1] == '=') + { + section.condition.type = condition::condition_type::greater_or_equal; + value = token.string.substr(2); + } + else + { + section.condition.type = condition::condition_type::greater_than; + value = token.string.substr(1); + } + } + else if (token.string.front() == '=') + { + section.condition.type = condition::condition_type::equal; + value = token.string.substr(1); + } + + section.condition.value = std::stold(value); + break; + } + + case number_format_token::token_type::text: + part.type = template_part::template_type::text; + part.string = token.string; + section.parts.push_back(part); + part = template_part(); + + break; + + case number_format_token::token_type::fill: + part.type = template_part::template_type::fill; + part.string = token.string; + section.parts.push_back(part); + part = template_part(); + + break; + + case number_format_token::token_type::space: + part.type = template_part::template_type::space; + part.string = token.string; + section.parts.push_back(part); + part = template_part(); + + break; + case number_format_token::token_type::number: + part.type = template_part::template_type::general; + part.placeholders = parse_placeholders(token.string); + section.parts.push_back(part); + part = template_part(); + + break; + case number_format_token::token_type::datetime: + section.is_datetime = true; + + switch (token.string.front()) + { + case '[': + section.is_timedelta = true; + + if (token.string == "[h]" || token.string == "[hh]") + { + part.type = template_part::template_type::elapsed_hours; + break; + } + else if (token.string == "[m]" || token.string == "[mm]") + { + part.type = template_part::template_type::elapsed_minutes; + break; + } + else if (token.string == "[s]" || token.string == "[ss]") + { + part.type = template_part::template_type::elapsed_seconds; + break; + } + else + { + throw std::runtime_error("expected [h], [m], or [s]"); + } + case 'm': + if (token.string == "m") + { + part.type = template_part::template_type::month_number; + break; + } + else if (token.string == "mm") + { + part.type = template_part::template_type::month_number_leading_zero; + break; + } + else if (token.string == "mmm") + { + part.type = template_part::template_type::month_abbreviation; + break; + } + else if (token.string == "mmmm") + { + part.type = template_part::template_type::month_name; + break; + } + else if (token.string == "mmmmm") + { + part.type = template_part::template_type::month_letter; + break; + } + case 'd': + if (token.string == "d") + { + part.type = template_part::template_type::day_number; + break; + } + else if (token.string == "dd") + { + part.type = template_part::template_type::day_number_leading_zero; + break; + } + else if (token.string == "ddd") + { + part.type = template_part::template_type::day_abbreviation; + break; + } + else if (token.string == "dddd") + { + part.type = template_part::template_type::day_name; + break; + } + case 'y': + if (token.string == "yy") + { + part.type = template_part::template_type::year_short; + break; + } + else if (token.string == "yyyy") + { + part.type = template_part::template_type::year_long; + break; + } + case 'h': + if (token.string == "h") + { + part.type = template_part::template_type::hour; + break; + } + else if (token.string == "hh") + { + part.type = template_part::template_type::hour_leading_zero; + break; + } + case 's': + if (token.string == "s") + { + part.type = template_part::template_type::second; + break; + } + else if (token.string == "ss") + { + part.type = template_part::template_type::second_leading_zero; + break; + } + case 'A': + section.twelve_hour = true; + + if (token.string == "AM/PM") + { + part.type = template_part::template_type::am_pm; + break; + } + else if (token.string == "A/P") + { + part.type = template_part::template_type::a_p; + break; + } + } + + section.parts.push_back(part); + part = template_part(); + + break; + + case number_format_token::token_type::end: + codes_.push_back(section); + finalize(); + + return; + + default: + break; + } + + token = parse_next_token(); + } + + throw std::runtime_error("bad format"); +} + +void number_format_parser::finalize() +{ + for (auto &code : codes_) + { + bool fix = false; + bool leading_zero = false; + std::size_t minutes_index = 0; + + for (std::size_t i = 0; i < code.parts.size(); ++i) + { + const auto &part = code.parts[i]; + + if (part.type == template_part::template_type::month_number + || part.type == template_part::template_type::month_number_leading_zero) + { + if (i < code.parts.size() - 2) + { + const auto &next = code.parts[i + 1]; + const auto &after_next = code.parts[i + 2]; + + if (next.type == template_part::template_type::text + && next.string == ":" + && (after_next.type == template_part::template_type::second || + after_next.type == template_part::template_type::second_leading_zero)) + { + fix = true; + leading_zero = part.type == template_part::template_type::month_number_leading_zero; + minutes_index = i; + + break; + } + } + + if (i > 1) + { + const auto &previous = code.parts[i - 1]; + const auto &before_previous = code.parts[i - 2]; + + if (previous.type == template_part::template_type::text + && previous.string == ":" + && (before_previous.type == template_part::template_type::hour || + before_previous.type == template_part::template_type::hour_leading_zero)) + { + fix = true; + leading_zero = part.type == template_part::template_type::month_number_leading_zero; + minutes_index = i; + } + } + } + } + + if (fix) + { + code.parts[minutes_index].type = leading_zero ? + template_part::template_type::minute_leading_zero : + template_part::template_type::minute; + } + } + + validate(); +} + +number_format_token number_format_parser::parse_next_token() +{ + number_format_token token; + + if (format_string_.size() <= position_) + { + token.type = number_format_token::token_type::end; + return token; + } + + auto current_char = format_string_[position_++]; + + switch (current_char) + { + case '[': + do + { + token.string.push_back(format_string_[position_++]); + } + while (format_string_[position_] != ']' && position_ < format_string_.size()); + + if (position_ == format_string_.size()) + { + throw std::runtime_error("missing ]"); + } + + if (token.string.empty()) + { + throw std::runtime_error("empty []"); + } + else if (token.string[0] == '<' || token.string[0] == '>' || token.string[0] == '=') + { + token.type = number_format_token::token_type::condition; + } + else if (token.string[0] == '$') + { + token.type = number_format_token::token_type::locale; + } + else if (token.string.size() <= 2 && + ((token.string == "h" || token.string == "hh") || + (token.string == "m" || token.string == "mm") || + (token.string == "s" || token.string == "ss"))) + { + token.type = number_format_token::token_type::datetime; + token.string = "[" + token.string + "]"; + } + else + { + token.type = number_format_token::token_type::color; + color_from_string(token.string); + } + + ++position_; + + break; + + case '\\': + token.type = number_format_token::token_type::text; + token.string.push_back(format_string_[position_++]); + + break; + + case 'G': + if (format_string_.substr(position_ - 1, 7) != "General") + { + throw std::runtime_error("expected General"); + } + + token.type = number_format_token::token_type::number; + token.string = "General"; + position_ += 6; + + break; + + case '_': + token.type = number_format_token::token_type::space; + token.string.push_back(format_string_[position_++]); + + break; + + case '0': + case '#': + case '?': + case '.': + token.type = number_format_token::token_type::number; + + do + { + token.string.push_back(current_char); + current_char = format_string_[position_]; + + if (current_char != '.') + { + ++position_; + } + } + while (current_char == '0' || current_char == '#' || current_char == '?' || current_char == ','); + + break; + + case 'y': + case 'm': + case 'd': + case 'h': + case 's': + token.type = number_format_token::token_type::datetime; + token.string.push_back(current_char); + + while (format_string_[position_] == current_char) + { + token.string.push_back(current_char); + ++position_; + } + + break; + + case 'A': + token.type = number_format_token::token_type::datetime; + + if (format_string_.substr(position_ - 1, 5) == "AM/PM") + { + position_ += 4; + token.string = "AM/PM"; + } + else if (format_string_.substr(position_ - 1, 3) == "A/P") + { + position_ += 2; + token.string = "A/P"; + } + else + { + throw std::runtime_error("expected AM/PM or A/P"); + } + + break; + + case '"': + { + token.type = number_format_token::token_type::text; + auto start = position_; + auto end = format_string_.find('"', position_); + + while (end != std::string::npos && format_string_[end - 1] == '\\') + { + end = format_string_.find('"', position_); + } + + token.string = format_string_.substr(start, end - start); + position_ = end + 1; + + break; + } + + case ';': + token.type = number_format_token::token_type::end_section; + break; + + case '(': + case ')': + case '-': + case '+': + case ':': + case ' ': + case '/': + token.type = number_format_token::token_type::text; + token.string.push_back(current_char); + + break; + + case '@': + token.type = number_format_token::token_type::number; + token.string.push_back(current_char); + break; + + default: + throw std::runtime_error("unexpected character"); + } + + return token; +} + +void number_format_parser::validate() +{ + if (codes_.size() > 4) + { + throw std::runtime_error("too many format codes"); + } + + if (codes_.size() > 2) + { + if (codes_[0].condition.type != condition::condition_type::none && + codes_[1].condition.type != condition::condition_type::none && + codes_[2].condition.type != condition::condition_type::none) + { + throw std::runtime_error("format should have a maximum of two codes with conditions"); + } + } +} + +placeholders number_format_parser::parse_placeholders(const std::string &placeholders_string) +{ + placeholders p; + + if (placeholders_string == "General") + { + p.type = placeholders::placeholders_type::general; + return p; + } + else if (placeholders_string == "@") + { + p.type = placeholders::placeholders_type::text; + return p; + } + else if (placeholders_string.front() == '.') + { + p.type = placeholders::placeholders_type::fractional_part; + } + + std::vector comma_indices; + + for (std::size_t i = 0; i < placeholders_string.size(); ++i) + { + auto c = placeholders_string[i]; + + if (c == '0') + { + ++p.num_zeros; + } + else if (c == '#') + { + ++p.num_optionals; + } + else if (c == '?') + { + ++p.num_spaces; + } + else if (c == ',') + { + comma_indices.push_back(i); + } + } + + if (!comma_indices.empty()) + { + std::size_t i = placeholders_string.size(); + + while (i == comma_indices.back()) + { + ++p.thousands_scale; + --i; + comma_indices.pop_back(); + } + + p.use_comma_separator = !comma_indices.empty(); + } + + return p; +} + +bracket_color number_format_parser::color_from_string(const std::string &color) +{ + switch (color[0]) + { + case 'C': + if (color == "Cyan") + { + return bracket_color::cyan; + } + else if (color.substr(0, 5) == "Color") + { + auto color_number = std::stoull(color.substr(5)); + + if (color_number >= 1 && color_number <= 56) + { + return static_cast(color_number); + } + } + case 'B': + if (color == "Black") + { + return bracket_color::black; + } + else if (color == "Blue") + { + return bracket_color::blue; + } + case 'G': + if (color == "Green") + { + return bracket_color::black; + } + case 'W': + if (color == "White") + { + return bracket_color::white; + } + case 'M': + if (color == "Magenta") + { + return bracket_color::white; + } + return bracket_color::magenta; + case 'Y': + if (color == "Yellow") + { + return bracket_color::white; + } + case 'R': + if (color == "Red") + { + return bracket_color::white; + } + default: + throw std::runtime_error("bad color: " + color); + } +} + +std::pair number_format_parser::locale_from_string(const std::string &locale_string) +{ + auto hyphen_index = locale_string.find('-'); + + if (locale_string.empty() || locale_string.front() != '$' || hyphen_index == std::string::npos) + { + throw std::runtime_error("bad locale: " + locale_string); + } + + std::pair result; + + if (hyphen_index > 1) + { + result.second = locale_string.substr(1, hyphen_index - 1); + } + + auto country_code_string = locale_string.substr(hyphen_index + 1); + + if (country_code_string.empty()) + { + throw std::runtime_error("bad locale: " + locale_string); + } + + for (auto c : country_code_string) + { + if (!((c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'f') || (c >= '0' && c <= '9'))) + { + throw std::runtime_error("bad locale: " + locale_string); + } + } + + auto country_code = std::stoi(country_code_string, nullptr, 16); + + for (const auto &known_locale : known_locales()) + { + if (known_locale.first == country_code) + { + result.first = static_cast(country_code); + return result; + } + } + + throw std::runtime_error("unknown country code: " + country_code_string); +} + +number_formatter::number_formatter(const std::string &format_string, xlnt::calendar calendar) + : parser_(format_string), + calendar_(calendar) +{ + parser_.parse(); + format_ = parser_.get_result(); +} + +std::string number_formatter::format_number(long double number) +{ + if (format_[0].condition.type != condition::condition_type::none) + { + if (format_[0].condition.satisfied_by(number)) + { + return format_number(format_[0], number); + } + + if (format_.size() == 1) + { + return std::string(11, '#'); + } + + if (format_[1].condition.type == condition::condition_type::none || format_[1].condition.satisfied_by(number)) + { + return format_number(format_[1], number); + } + + if (format_.size() == 2) + { + return std::string(11, '#'); + } + + return format_number(format_[2], number); + } + + // no conditions, format based on sign: + + // 1 section, use for all + if (format_.size() == 1) + { + return format_number(format_[0], number); + } + // 2 sections, first for positive and zero, second for negative + else if (format_.size() == 2) + { + if (number >= 0) + { + return format_number(format_[0], number); + } + else + { + return format_number(format_[1], std::abs(number)); + } + } + // 3+ sections, first for positive, second for negative, third for zero + else + { + if (number > 0) + { + return format_number(format_[0], number); + } + else if (number < 0) + { + return format_number(format_[1], std::abs(number)); + } + else + { + return format_number(format_[2], number); + } + } +} + +std::string number_formatter::format_text(const std::string &text) +{ + if (format_.size() < 4) + { + return format_text(format_[0], text); + } + + return format_text(format_[3], text); +} + +std::string number_formatter::fill_placeholders(const placeholders &p, long double number) +{ + if (p.type == placeholders::placeholders_type::general) + { + auto result = std::to_string(number); + + while (result.back() == '0') + { + result.pop_back(); + } + + if (result.back() == '.') + { + result.pop_back(); + } + + return result; + } + + auto integer_part = static_cast(number); + + if (p.type != placeholders::placeholders_type::fractional_part) + { + auto result = std::to_string(integer_part); + + while (result.size() < p.num_zeros) + { + result = "0" + result; + } + + while (result.size() < p.num_zeros + p.num_spaces) + { + result = " " + result; + } + + if (p.use_comma_separator) + { + std::vector digits(result.rbegin(), result.rend()); + std::string temp; + + for (std::size_t i = 0; i < digits.size(); i++) + { + temp.push_back(digits[i]); + + if (i % 3 == 2) + { + temp.push_back(','); + } + } + + result = std::string(temp.rbegin(), temp.rend()); + } + + return result; + } + else + { + auto fractional_part = number - integer_part; + auto result = std::to_string(fractional_part).substr(1); + + while (result.back() == '0') + { + result.pop_back(); + } + + while (result.size() < p.num_zeros + 1) + { + result.push_back('0'); + } + + while (result.size() < p.num_zeros + p.num_spaces + 1) + { + result.push_back(' '); + } + + return result; + } +} + +std::string number_formatter::format_number(const format_code &format, long double number) +{ + static const std::vector *month_names = + new std::vector + { + "January", + "February", + "March", + "April", + "May", + "June", + "July", + "August", + "September", + "October", + "November", + "December" + }; + + std::string result; + + if (number < 0) + { + result.push_back('-'); + } + + number = std::abs(number); + + xlnt::datetime dt(0, 0, 0); + + if (format.is_datetime) + { + dt = xlnt::datetime::from_number(number, calendar_); + } + + for (const auto &part : format.parts) + { + switch (part.type) + { + case template_part::template_type::space: + result.push_back(' '); + break; + case template_part::template_type::text: + result.append(part.string); + break; + case template_part::template_type::general: + { + result.append(fill_placeholders(part.placeholders, number)); + break; + } + case template_part::template_type::day_number: + result.append(std::to_string(dt.day)); + break; + case template_part::template_type::day_number_leading_zero: + if (dt.day < 10) + { + result.push_back('0'); + } + + result.append(std::to_string(dt.day)); + break; + case template_part::template_type::month_abbreviation: + result.append(month_names->at(dt.month - 1).substr(0, 3)); + break; + case template_part::template_type::month_name: + result.append(month_names->at(dt.month - 1)); + break; + case template_part::template_type::month_number: + result.append(std::to_string(dt.month)); + break; + case template_part::template_type::month_number_leading_zero: + if (dt.month < 10) + { + result.push_back('0'); + } + + result.append(std::to_string(dt.month)); + break; + case template_part::template_type::year_short: + if (dt.year % 1000 < 10) + { + result.push_back('0'); + } + + result.append(std::to_string(dt.year % 1000)); + break; + case template_part::template_type::year_long: + result.append(std::to_string(dt.year)); + break; + case template_part::template_type::hour: + result.append(std::to_string(format.twelve_hour ? dt.hour % 12 : dt.hour)); + break; + case template_part::template_type::hour_leading_zero: + if (format.twelve_hour ? dt.hour % 12 : dt.hour < 10) + { + result.push_back('0'); + } + + result.append(std::to_string(format.twelve_hour ? dt.hour % 12 : dt.hour)); + break; + case template_part::template_type::minute: + result.append(std::to_string(dt.minute)); + break; + case template_part::template_type::minute_leading_zero: + if (dt.minute < 10) + { + result.push_back('0'); + } + + result.append(std::to_string(dt.minute)); + break; + case template_part::template_type::second: + result.append(std::to_string(dt.second)); + break; + case template_part::template_type::second_leading_zero: + if (dt.second < 10) + { + result.push_back('0'); + } + + result.append(std::to_string(dt.second)); + break; + case template_part::template_type::am_pm: + if (dt.hour < 12) + { + result.append("AM"); + } + else + { + result.append("PM"); + } + + break; + + default: + throw "unhandled"; + } + } + + return result; +} + +std::string number_formatter::format_text(const format_code &format, const std::string &text) +{ + std::string result; + + for (const auto &part : format.parts) + { + switch (part.type) + { + case template_part::template_type::text: + result.append(part.string); + break; + + case template_part::template_type::general: + if (part.placeholders.type == placeholders::placeholders_type::general + || part.placeholders.type == placeholders::placeholders_type::text) + { + result.append(text); + } + + break; + + default: + break; + } + } + + return result; +} + +} // namespace detail +} // namespace xlnt diff --git a/source/detail/number_formatter.hpp b/source/detail/number_formatter.hpp new file mode 100644 index 00000000..bdfaab2b --- /dev/null +++ b/source/detail/number_formatter.hpp @@ -0,0 +1,366 @@ +// Copyright (c) 2014-2016 Thomas Fussell +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, WRISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE +// +// @license: http://www.opensource.org/licenses/mit-license.php +// @author: see AUTHORS file + +#pragma once + +#include +#include +#include + +#include + +namespace xlnt { +namespace detail { + +enum class bracket_color +{ + none, + black, + blue, + cyan, + green, + magenta, + red, + white, + yellow, + color1, + color2, + color3, + color4, + color5, + color6, + color7, + color8, + color9, + color10, + color11, + color12, + color13, + color14, + color15, + color16, + color17, + color18, + color19, + color20, + color21, + color22, + color23, + color24, + color25, + color26, + color27, + color28, + color29, + color30, + color31, + color32, + color33, + color34, + color35, + color36, + color37, + color38, + color39, + color40, + color41, + color42, + color43, + color44, + color45, + color46, + color47, + color48, + color49, + color50, + color51, + color52, + color53, + color54, + color55, + color56 +}; + +enum class locale +{ + none = 0, + arabic_saudi_arabia = 0x401, + bulgarian = 0x402, + catalan = 0x403, + chinese_taiwan = 0x404, + czech = 0x405, + danish = 0x406, + german_germany = 0x407, + greek = 0x408, + english_united_states = 0x409, + italian_italy = 0x410, + japanese = 0x411, + korean = 0x412, + dutch_netherlands = 0x413, + norwegian_bokml = 0x414, + polish = 0x415, + portuguese_brazil = 0x416, + raeto_romance = 0x417, + romanian_romania = 0x418, + russian = 0x419, + urdu = 0x420, + indonesian = 0x421, + ukrainian = 0x422, + belarusian = 0x423, + slovenian = 0x424, + estonian = 0x425, + latvian = 0x426, + lithuanian = 0x427, + tajik = 0x428, + farsi_persian = 0x429, + sesotho_sutu = 0x430, + tsonga = 0x431, + setsuana = 0x432, + venda = 0x433, + xhosa = 0x434, + zulu = 0x435, + afrikaans = 0x436, + georgian = 0x437, + faroese = 0x438, + hindi = 0x439, + kyrgyz_cyrillic = 0x440, + swahili = 0x441, + turkmen = 0x442, + uzbek_latin = 0x443, + tatar = 0x444, + bengali_india = 0x445, + punjabi = 0x446, + gujarati = 0x447, + oriya = 0x448, + tamil = 0x449, + mongolian = 0x450, + tibetan = 0x451, + welsh = 0x452, + khmer = 0x453, + lao = 0x454, + burmese = 0x455, + galician = 0x456, + konkani = 0x457, + manipuri = 0x458, + sindhi = 0x459, + kashmiri = 0x460, + nepali = 0x461, + frisian_netherlands = 0x462, + filipino = 0x464, + divehi_dhivehi_maldivian = 0x465, + edo = 0x466, + igbo_nigeria = 0x470, + guarani_paraguay = 0x474, + latin = 0x476, + somali = 0x477, + maori = 0x481, + arabic_iraq = 0x801, + chinese_china = 0x804, + german_switzerland = 0x807, + english_great_britain = 0x809, + italian_switzerland = 0x810, + dutch_belgium = 0x813, + norwegian_nynorsk = 0x814, + portuguese_portugal = 0x816, + romanian_moldova = 0x818, + russian_moldova = 0x819, + uzbek_cyrillic = 0x843, + bengali_bangladesh = 0x845, + mongolian2 = 0x850, + arabic_libya = 0x1001, + chinese_singapore = 0x1004, + german_luxembourg = 0x1007, + english_canada = 0x1009, + arabic_algeria = 0x1401, + chinese_macau_sar = 0x1404, + german_liechtenstein = 0x1407, + english_new_zealand = 0x1409, + arabic_morocco = 0x1801, + english_ireland = 0x1809, + arabic_oman = 0x2001, + english_jamaica = 0x2009, + arabic_yemen = 0x2401, + english_caribbean = 0x2409, + arabic_syria = 0x2801, + english_belize = 0x2809, + arabic_lebanon = 0x3001, + english_zimbabwe = 0x3009, + arabic_kuwait = 0x3401, + english_phillippines = 0x3409, + arabic_united_arab_emirates = 0x3801, + arabic_qatar = 0x4001 +}; + +struct condition +{ + enum class condition_type + { + none, + less_than, + less_or_equal, + equal, + not_equal, + greater_than, + greater_or_equal + } type = condition_type::none; + + long double value = 0; + + bool satisfied_by(long double number) const; +}; + +struct placeholders +{ + enum class placeholders_type + { + bad, + general, + text, + fractional_part, + integer_part, + fraction_integer, + fraction_numerator, + fraction_denominator, + scientific_significand, + scientific_exponent + } type = placeholders_type::bad; + + bool use_comma_separator = false; + + std::size_t num_zeros = 0; // 0 + std::size_t num_optionals = 0; // # + std::size_t num_spaces = 0; // ? + std::size_t thousands_scale = 0; +}; + +struct number_format_token +{ + enum class token_type + { + bad, + color, + locale, + condition, + text, + fill, + space, + number, + datetime, + end_section, + end + } type = token_type::bad; + + std::string string; +}; + +struct template_part +{ + enum class template_type + { + bad, + text, + fill, + space, + general, + placeholder, + month_number, + month_number_leading_zero, + month_abbreviation, + month_name, + month_letter, + day_number, + day_number_leading_zero, + day_abbreviation, + day_name, + year_short, + year_long, + hour, + hour_leading_zero, + minute, + minute_leading_zero, + second, + second_leading_zero, + am_pm, + a_p, + elapsed_hours, + elapsed_minutes, + elapsed_seconds + } type = template_type::bad; + + std::string string; + placeholders placeholders; +}; + +struct format_code +{ + bracket_color color = bracket_color::none; + locale locale = locale::none; + condition condition; + bool is_datetime = false; + bool is_timedelta = false; + bool twelve_hour = false; + std::vector parts; +}; + +class number_format_parser +{ +public: + number_format_parser(const std::string &format_string); + const std::vector &get_result() const; + void reset(const std::string &format_string); + void parse(); + +private: + void finalize(); + void validate(); + + number_format_token parse_next_token(); + + placeholders parse_placeholders(const std::string &placeholders_string); + bracket_color color_from_string(const std::string &color); + std::pair locale_from_string(const std::string &locale_string); + + std::size_t position_ = 0; + std::string format_string_; + std::vector codes_; +}; + +class number_formatter +{ +public: + number_formatter(const std::string &format_string, xlnt::calendar calendar); + std::string format_number(long double number); + std::string format_text(const std::string &text); + +private: + std::string fill_placeholders(const placeholders &p, long double number); + std::string format_number(const format_code &format, long double number); + std::string format_text(const format_code &format, const std::string &text); + + number_format_parser parser_; + std::vector format_; + xlnt::calendar calendar_; +}; + +} // namespace detail +} // namespace xlnt diff --git a/source/styles/number_format.cpp b/source/styles/number_format.cpp index 875ae6d9..01a5afa1 100644 --- a/source/styles/number_format.cpp +++ b/source/styles/number_format.cpp @@ -1,4 +1,4 @@ -// Copyright (c) 2014-2016 Thomas Fussell +// Copyright (c) 2014-2016 Thomas Fussell // Copyright (c) 2010-2015 openpyxl // // Permission is hereby granted, free of charge, to any person obtaining a copy @@ -30,6 +30,8 @@ #include #include +#include + namespace { const std::unordered_map &builtin_formats() @@ -61,16 +63,16 @@ const std::unordered_map &builtin_formats() { 21, "h:mm:ss" }, { 22, "m/d/yy h:mm" }, - { 37, "#,##0_);(#,##0)" }, - { 38, "#,##0_);[Red](#,##0)" }, - { 39, "#,##0.00_);(#,##0.00)" }, - { 40, "#,##0.00_);[Red](#,##0.00)" }, + { 37, "#,##0 ;(#,##0)" }, + { 38, "#,##0 ;[Red](#,##0)" }, + { 39, "#,##0.00;(#,##0.00)" }, + { 40, "#,##0.00;[Red](#,##0.00)" }, { 41, "_(* #,##0_);_(* \\(#,##0\\);_(* \"-\"_);_(@_)" }, { 42, "_(\"$\"* #,##0_);_(\"$\"* \\(#,##0\\);_(\"$\"* \"-\"_);_(@_)" }, { 43, "_(* #,##0.00_);_(* \\(#,##0.00\\);_(* \"-\"??_);_(@_)" }, - { 44, "_-\"$\"* #,##0.00_-;\\-\"$\"* #,##0.00_-;_-\"$\"* \"-\"??_-;_-@_-" }, + { 45, "mm:ss" }, { 46, "[h]:mm:ss" }, { 47, "mmss.0" }, @@ -91,868 +93,6 @@ const std::unordered_map &builtin_formats() return *formats; } -enum class condition_type -{ - less_than, - less_or_equal, - equal, - greater_than, - greater_or_equal, - invalid -}; - -struct section -{ - bool has_value = false; - std::string value; - bool has_color = false; - std::string color; - bool has_condition = false; - condition_type condition = condition_type::invalid; - std::string condition_value; - bool has_locale = false; - std::string locale; - - section &operator=(const section &other) - { - has_value = other.has_value; - value = other.value; - has_color = other.has_color; - color = other.color; - has_condition = other.has_condition; - condition = other.condition; - condition_value = other.condition_value; - has_locale = other.has_locale; - locale = other.locale; - - return *this; - } -}; - -struct format_sections -{ - section first; - section second; - section third; - section fourth; -}; - -// copied from named_range.cpp, keep in sync -/// -/// Return a vector containing string split at each delim. -/// -/// -/// This should maybe be in a utility header so it can be used elsewhere. -/// -std::vector split_string(const std::string &string, char delim) -{ - std::vector split; - std::string::size_type previous_index = 0; - auto separator_index = string.find(delim); - - while (separator_index != std::string::npos) - { - auto part = string.substr(previous_index, separator_index - previous_index); - split.push_back(part); - - previous_index = separator_index + 1; - separator_index = string.find(delim, previous_index); - } - - split.push_back(string.substr(previous_index)); - - return split; -} - -std::vector split_string_any(const std::string &string, const std::string &delims) -{ - std::vector split; - std::string::size_type previous_index = 0; - auto separator_index = string.find_first_of(delims); - - while (separator_index != std::string::npos) - { - auto part = string.substr(previous_index, separator_index - previous_index); - - if (!part.empty()) - { - split.push_back(part); - } - - previous_index = separator_index + 1; - separator_index = string.find_first_of(delims, previous_index); - } - - split.push_back(string.substr(previous_index)); - - return split; -} - -bool is_date_format(const std::string &format_string) -{ - auto not_in = format_string.find_first_not_of("/-:, mMyYdDhHsSAP"); - return not_in == std::string::npos; -} - -bool is_valid_color(const std::string &color) -{ - static const std::vector *colors = - new std::vector( - { - "Black", - "Green", - "White", - "Blue", - "Magenta", - "Yellow", - "Cyan", - "Red" - }); - - auto compare_color = [&](const std::string &other) { - if (color.size() != other.size()) return false; - - for (std::size_t i = 0; i < color.size(); i++) - { - if (std::toupper(color[i]) != std::toupper(other[i])) - { - return false; - } - } - - return true; - }; - - return std::find_if(colors->begin(), colors->end(), compare_color) != colors->end(); -} - -bool parse_condition(const std::string &string, section &s) -{ - s.has_condition = false; - s.condition = condition_type::invalid; - s.condition_value.clear(); - - if (string[0] == '<') - { - s.has_condition = true; - - if (string[1] == '=') - { - s.condition = condition_type::less_or_equal; - s.condition_value = string.substr(2); - } - else - { - s.condition = condition_type::less_than; - s.condition_value = string.substr(1); - } - } - if (string[0] == '>') - { - s.has_condition = true; - - if (string[1] == '=') - { - s.condition = condition_type::greater_or_equal; - s.condition_value = string.substr(2); - } - else - { - s.condition = condition_type::greater_than; - s.condition_value = string.substr(1); - } - } - else if (string[0] == '=') - { - s.has_condition = true; - s.condition = condition_type::equal; - s.condition_value = string.substr(1); - } - - return s.has_condition; -} - -bool is_hex(char c) -{ - return (c >= 'A' && c <= 'F') || (c >= '0' && c <= '9'); -} - -const std::unordered_map known_locales() -{ - const std::unordered_map *all = - new std::unordered_map( - { - { 0x401, "Arabic - Saudi Arabia" }, - { 0x402, "Bulgarian" }, - { 0x403, "Catalan" }, - { 0x404, "Chinese - Taiwan" }, - { 0x405, "Czech" }, - { 0x406, "Danish" }, - { 0x407, "German - Germany" }, - { 0x408, "Greek" }, - { 0x409, "English - United States" }, - { 0x410, "Italian - Italy" }, - { 0x411, "Japanese" }, - { 0x412, "Korean" }, - { 0x413, "Dutch - Netherlands" }, - { 0x414, "Norwegian - Bokml" }, - { 0x415, "Polish" }, - { 0x416, "Portuguese - Brazil" }, - { 0x417, "Raeto-Romance" }, - { 0x418, "Romanian - Romania" }, - { 0x419, "Russian" }, - { 0x420, "Urdu" }, - { 0x421, "Indonesian" }, - { 0x422, "Ukrainian" }, - { 0x423, "Belarusian" }, - { 0x424, "Slovenian" }, - { 0x425, "Estonian" }, - { 0x426, "Latvian" }, - { 0x427, "Lithuanian" }, - { 0x428, "Tajik" }, - { 0x429, "Farsi - Persian" }, - { 0x430, "Sesotho (Sutu)" }, - { 0x431, "Tsonga" }, - { 0x432, "Setsuana" }, - { 0x433, "Venda" }, - { 0x434, "Xhosa" }, - { 0x435, "Zulu" }, - { 0x436, "Afrikaans" }, - { 0x437, "Georgian" }, - { 0x438, "Faroese" }, - { 0x439, "Hindi" }, - { 0x440, "Kyrgyz - Cyrillic" }, - { 0x441, "Swahili" }, - { 0x442, "Turkmen" }, - { 0x443, "Uzbek - Latin" }, - { 0x444, "Tatar" }, - { 0x445, "Bengali - India" }, - { 0x446, "Punjabi" }, - { 0x447, "Gujarati" }, - { 0x448, "Oriya" }, - { 0x449, "Tamil" }, - { 0x450, "Mongolian" }, - { 0x451, "Tibetan" }, - { 0x452, "Welsh" }, - { 0x453, "Khmer" }, - { 0x454, "Lao" }, - { 0x455, "Burmese" }, - { 0x456, "Galician" }, - { 0x457, "Konkani" }, - { 0x458, "Manipuri" }, - { 0x459, "Sindhi" }, - { 0x460, "Kashmiri" }, - { 0x461, "Nepali" }, - { 0x462, "Frisian - Netherlands" }, - { 0x464, "Filipino" }, - { 0x465, "Divehi; Dhivehi; Maldivian" }, - { 0x466, "Edo" }, - { 0x470, "Igbo - Nigeria" }, - { 0x474, "Guarani - Paraguay" }, - { 0x476, "Latin" }, - { 0x477, "Somali" }, - { 0x481, "Maori" }, - { 0x801, "Arabic - Iraq" }, - { 0x804, "Chinese - China" }, - { 0x807, "German - Switzerland" }, - { 0x809, "English - Great Britain" }, - { 0x810, "Italian - Switzerland" }, - { 0x813, "Dutch - Belgium" }, - { 0x814, "Norwegian - Nynorsk" }, - { 0x816, "Portuguese - Portugal" }, - { 0x818, "Romanian - Moldova" }, - { 0x819, "Russian - Moldova" }, - { 0x843, "Uzbek - Cyrillic" }, - { 0x845, "Bengali - Bangladesh" }, - { 0x850, "Mongolian" }, - { 0x1001, "Arabic - Libya" }, - { 0x1004, "Chinese - Singapore" }, - { 0x1007, "German - Luxembourg" }, - { 0x1009, "English - Canada" }, - { 0x1401, "Arabic - Algeria" }, - { 0x1404, "Chinese - Macau SAR" }, - { 0x1407, "German - Liechtenstein" }, - { 0x1409, "English - New Zealand" }, - { 0x1801, "Arabic - Morocco" }, - { 0x1809, "English - Ireland" }, - { 0x2001, "Arabic - Oman" }, - { 0x2009, "English - Jamaica" }, - { 0x2401, "Arabic - Yemen" }, - { 0x2409, "English - Caribbean" }, - { 0x2801, "Arabic - Syria" }, - { 0x2809, "English - Belize" }, - { 0x3001, "Arabic - Lebanon" }, - { 0x3009, "English - Zimbabwe" }, - { 0x3401, "Arabic - Kuwait" }, - { 0x3409, "English - Phillippines" }, - { 0x3801, "Arabic - United Arab Emirates" }, - { 0x4001, "Arabic - Qatar" } - }); - - return *all; -} - -bool is_valid_locale(const std::string &locale_string) -{ - auto hyphen_index = locale_string.find('-'); - - if (hyphen_index == std::string::npos) - { - return false; - } - - std::string country = locale_string.substr(hyphen_index + 1); - - if (country.empty()) - { - return false; - } - - for (auto c : country) - { - if (!is_hex(static_cast(std::toupper(c)))) - { - return false; - } - } - - auto index = std::stoi(country, 0, 16); - - auto known_locales_ = known_locales(); - - if (known_locales_.find(index) == known_locales_.end()) - { - return false; - } - - std::string beginning = locale_string.substr(0, locale_string.find('-')); - - if (beginning.empty() || beginning[0] != '$') - { - return false; - } - - if (beginning.size() == 1) - { - return true; - } - - beginning = beginning.substr(1); - - return true; -} - -section parse_section(const std::string §ion_string) -{ - if (section_string.empty()) - { - throw std::runtime_error("empty format"); - } - - section s; - - std::string format_part; - std::string bracket_part; - - std::vector bracket_parts; - - bool in_quotes = false; - bool in_brackets = false; - - const std::vector bracket_times = { "h", "hh", "m", "mm", "s", "ss" }; - - for (std::size_t i = 0; i < section_string.size(); i++) - { - if (!in_quotes && section_string[i] == '"') - { - format_part.push_back(section_string[i]); - in_quotes = true; - } - else if (in_quotes && section_string[i] == '"') - { - format_part.push_back(section_string[i]); - - if (i < section_string.size() - 1 && section_string[i + 1] != '"') - { - in_quotes = false; - } - } - else if (!in_brackets && section_string[i] == '[') - { - in_brackets = true; - - for (auto bracket_time : bracket_times) - { - if (i < section_string.size() - bracket_time.size() && - section_string.substr(i + 1, bracket_time.size()) == bracket_time) - { - in_brackets = false; - break; - } - } - } - else if (in_brackets) - { - if (section_string[i] == ']') - { - in_brackets = false; - - if (is_valid_color(bracket_part)) - { - if (!s.has_color) - { - s.color = bracket_part; - s.has_color = true; - } - else - { - throw std::runtime_error("two colors"); - } - } - else if (is_valid_locale(bracket_part)) - { - if (!s.has_locale) - { - s.locale = bracket_part; - s.has_locale = true; - } - else - { - throw std::runtime_error("two locales"); - } - } - else if (s.has_condition || !parse_condition(bracket_part, s)) - { - throw std::runtime_error("invalid bracket format"); - } - - bracket_part.clear(); - } - else - { - bracket_part.push_back(section_string[i]); - } - } - else - { - format_part.push_back(section_string[i]); - } - } - - s.value = format_part; - s.has_value = true; - - return s; -} - -format_sections parse_format_sections(const std::string &combined) -{ - format_sections result = {}; - - auto split = split_string(combined, ';'); - result.first = parse_section(split[0]); - - if (!result.first.has_condition) - { - result.second = result.first; - result.third = result.first; - } - - if (split.size() > 1) - { - result.second = parse_section(split[1]); - } - - if (split.size() > 2) - { - if (result.first.has_condition && !result.second.has_condition) - { - throw std::runtime_error("first two sections should have conditions"); - } - - result.third = parse_section(split[2]); - - if (result.third.has_condition) - { - throw std::runtime_error("third section shouldn't have a condition"); - } - } - - if (split.size() > 3) - { - if (result.first.has_condition) - { - throw std::runtime_error("too many parts"); - } - - result.fourth = parse_section(split[3]); - } - - if (split.size() > 4) - { - throw std::runtime_error("too many parts"); - } - - return result; -} - -std::string format_section(long double number, const section &format, xlnt::calendar base_date) -{ - const std::string unquoted = "$+(:^'{<=-/)!&~}> "; - std::string format_temp = format.value; - std::string result; - - if (is_date_format(format.value)) - { - auto lower_string = [](const std::string &s) { - std::string lower; - lower.resize(s.size()); - for (std::size_t i = 0; i < s.size(); i++) - lower[i] = static_cast(std::tolower(s[i])); - return lower; - }; - - auto value = format.value; - bool twelve_hour = false; - - if (format.value.size() > 5 && lower_string(value).substr(format.value.size() - 5) == "am/pm") - { - twelve_hour = true; - } - - const std::string date_unquoted = ",-/: "; - const std::vector dates = { "m", "mm", "mmm", "mmmmm", "mmmmmm", "d", "dd", "ddd", "dddd", "yy", - "yyyy", "h", "[h]", "hh", "m", "[m]", "mm", "s", "[s]", "ss" }; - const std::vector MonthNames = { "January", "February", "March", - "April", "May", "June", "July", "August", "September", "October", "November", "December" }; - - - auto split = split_string_any(format.value, date_unquoted); - std::string::size_type index = 0, prev = 0; - auto d = xlnt::datetime::from_number(number, base_date); - bool processed_month = false; - - for (auto part : split) - { - while (format.value.substr(index, part.size()) != part) - { - index++; - } - - part = lower_string(part); - - auto between = format.value.substr(prev, index - prev); - - if (between == "/" && part == "pm") - { - index += part.size(); - prev = index; - continue; - } - - result.append(between); - - if (part == "m" && !processed_month) - { - result.append(std::to_string(d.month)); - processed_month = true; - } - else if (part == "mm" && !processed_month) - { - if (d.month < 10) - { - result.append("0"); - } - - result.append(std::to_string(d.month)); - processed_month = true; - } - else if (part == "mmm" && !processed_month) - { - result.append(MonthNames.at(static_cast(d.month - 1)).substr(0, 3)); - processed_month = true; - } - else if (part == "mmmm" && !processed_month) - { - result.append(MonthNames.at(static_cast(d.month - 1))); - processed_month = true; - } - else if (part == "d") - { - result.append(std::to_string(d.day)); - } - else if (part == "dd") - { - if (d.day < 10) - { - result.append("0"); - } - - result.append(std::to_string(d.day)); - } - else if (part == "yy") - { - result.append(std::to_string(d.year % 1000)); - } - else if (part == "yyyy") - { - result.append(std::to_string(d.year)); - } - - else if (part == "h") - { - if (twelve_hour) - { - result.append(std::to_string(d.hour % 12)); - } - else - { - result.append(std::to_string(d.hour)); - } - - processed_month = true; - } - else if (part == "hh") - { - if (twelve_hour) - { - if (d.hour % 12 < 10) - { - result.append("0"); - } - - result.append(std::to_string(d.hour % 12)); - } - else - { - if (d.hour < 10) - { - result.append("0"); - } - - result.append(std::to_string(d.hour)); - } - - processed_month = true; - } - else if (part == "m") - { - result.append(std::to_string(d.minute)); - } - else if (part == "mm") - { - if (d.minute < 10) - { - result.append("0"); - } - - result.append(std::to_string(d.minute)); - } - else if (part == "s") - { - result.append(std::to_string(d.second)); - } - else if (part == "ss") - { - if (d.second < 10) - { - result.append("0"); - } - - result.append(std::to_string(d.second)); - } - else if (part == "am") - { - if (d.hour < 12) - { - result.append("AM"); - } - else - { - result.append("PM"); - } - } - - index += part.size(); - prev = index; - } - - if (index < format.value.size()) - { - result.append(format.value.substr(index)); - } - } - else if (format.value.find_first_of("General") != std::string::npos || format.value == "0") - { - std::string before_text, end_text; - auto open_quote = format.value.find('"'); - - if (open_quote != std::string::npos) - { - auto close_quote = format.value.find('"', open_quote + 1); - auto quoted_text = format.value.substr(open_quote, close_quote - open_quote + 1); - - if (open_quote == 0) - { - before_text = quoted_text.substr(1, quoted_text.size() - 2); - } - else - { - end_text = quoted_text.substr(1, quoted_text.size() - 2); - } - } - - if (number < 0) - { - before_text = std::string("-") + before_text; - } - - result = before_text; - - if (number == static_cast(number)) - { - result.append(std::to_string(static_cast(std::abs(number)))); - } - else - { - auto number_string = std::to_string(std::abs(number)); - - while (number_string.find('.') != std::string::npos && number_string.back() == '0' && number_string.find('.') < number_string.size() - 1) - { - number_string.erase(number_string.end() - 1); - } - - result.append(number_string); - } - - result.append(end_text); - } - else if (format.value.find("#,##0.00") != std::string::npos) - { - auto index = format.value.find("#,##0.00"); - auto before_text = format.value.substr(0, index); - auto after_text = format.value.substr(index + 8); - bool minus_first = before_text.substr(0, 1) == "-"; - - if (minus_first) - { - before_text = before_text.substr(1); - } - - if (format.has_locale && format.locale == "$$-1009") - { - before_text = std::string(minus_first ? "-" : "") + "CA$" + before_text; - } - else if (format.has_locale && format.locale == "$€-407") - { - before_text = std::string(minus_first ? "-" : "") + "€" + before_text; - } - - result = before_text + std::to_string(number < 0 ? -number : number) + after_text; - - auto decimal_pos = result.find('.'); - - if (decimal_pos != std::string::npos) - { - result[decimal_pos] = ','; - decimal_pos += 3; - - while (decimal_pos < result.size()) - { - result.pop_back(); - } - } - } - - return result; -} - -std::string format_section(const std::string &text, const section &format) -{ - auto arobase_index = format.value.find('@'); - - std::string first_part, middle_part, last_part; - - if (arobase_index != std::string::npos) - { - first_part = format.value.substr(0, arobase_index); - middle_part = text; - last_part = format.value.substr(arobase_index + 1); - } - else - { - first_part = format.value; - } - - auto unquote = [](std::string &s) { - if (!s.empty()) - { - if (s.front() != '"' || s.back() != '"') return false; - s = s.substr(1, s.size() - 2); - } - - return true; - }; - - if (!unquote(first_part) || !unquote(last_part)) - { - throw std::runtime_error(std::string("additional text must be enclosed in quotes: ") + format.value); - } - - return first_part + middle_part + last_part; -} - -std::string format_number(long double number, const std::string &format, xlnt::calendar base_date) -{ - auto sections = parse_format_sections(format); - - if (sections.first.has_condition) - { - auto right_side = std::stod(sections.first.condition_value); - - if ((sections.first.condition == condition_type::equal && number == right_side) || - (sections.first.condition == condition_type::greater_or_equal && number >= right_side) || - (sections.first.condition == condition_type::greater_than && number > right_side) || - (sections.first.condition == condition_type::less_or_equal && number <= right_side) || - (sections.first.condition == condition_type::less_than && number < right_side)) - { - return format_section(number, sections.first, base_date); - } - - right_side = std::stod(sections.second.condition_value); - - if ((sections.second.condition == condition_type::equal && number == right_side) || - (sections.second.condition == condition_type::greater_or_equal && number >= right_side) || - (sections.second.condition == condition_type::greater_than && number > right_side) || - (sections.second.condition == condition_type::less_or_equal && number <= right_side) || - (sections.second.condition == condition_type::less_than && number < right_side)) - { - return format_section(number, sections.second, base_date); - } - } - else - { - if (number > 0) - { - return format_section(number, sections.first, base_date); - } - else if (number < 0) - { - return format_section(number, sections.second, base_date); - } - } - - // number == 0 or default condition - return format_section(number, sections.third, base_date); -} - -std::string format_text(const std::string &text, const std::string &format) -{ - if (format == "General" || format == "@") return text; - auto sections = parse_format_sections(format); - return format_section(text, sections.fourth); -} - } // namespace namespace xlnt { @@ -1247,30 +387,37 @@ std::size_t number_format::get_id() const bool number_format::is_date_format() const { - if (format_string_ != "General") + detail::number_format_parser p(format_string_); + p.parse(); + auto parsed = p.get_result(); + + bool any_datetime = false; + bool any_timedelta = false; + + for (const auto §ion : parsed) { - try + if (section.is_datetime) { - auto sections = parse_format_sections(format_string_); - return ::is_date_format(sections.first.value); + any_datetime = true; } - catch (std::exception) + + if (section.is_timedelta) { - return false; + any_timedelta = true; } } - return false; + return any_datetime && !any_timedelta; } std::string number_format::format(const std::string &text) const { - return format_text(text, format_string_); + return detail::number_formatter(format_string_, calendar::windows_1900).format_text(text); } std::string number_format::format(long double number, calendar base_date) const { - return format_number(number, format_string_, base_date); + return detail::number_formatter(format_string_, base_date).format_number(number); } } // namespace xlnt diff --git a/source/styles/tests/test_number_format.hpp b/source/styles/tests/test_number_format.hpp index 9c5c5b11..42f08a1a 100644 --- a/source/styles/tests/test_number_format.hpp +++ b/source/styles/tests/test_number_format.hpp @@ -16,10 +16,12 @@ public: nf.set_format_string("\"positive\"General;\"negative\"General"); auto formatted = nf.format(3.14, xlnt::calendar::windows_1900); TS_ASSERT_EQUALS(formatted, "positive3.14"); - - nf.set_format_string("\"positive\"General;\"negative\"General"); formatted = nf.format(-3.14, xlnt::calendar::windows_1900); - TS_ASSERT_EQUALS(formatted, "-negative3.14"); + TS_ASSERT_EQUALS(formatted, "negative3.14"); + + nf.set_format_string("\"any\"General"); + formatted = nf.format(-3.14, xlnt::calendar::windows_1900); + TS_ASSERT_EQUALS(formatted, "-any3.14"); } void test_simple_date() @@ -311,14 +313,13 @@ public: { xlnt::number_format nf; - nf.set_format_string("[$€-407]-#,##0.00"); - auto formatted = nf.format(1.2, xlnt::calendar::windows_1900); - TS_ASSERT_EQUALS(formatted, "-€1,20"); + nf.set_format_string("[$€-407]#,##0.00"); + auto formatted = nf.format(-45000.1, xlnt::calendar::windows_1900); + TS_ASSERT_EQUALS(formatted, "-€45,000.10"); nf.set_format_string("[$$-1009]#,##0.00"); - formatted = nf.format(1.2, xlnt::calendar::windows_1900); - TS_ASSERT_EQUALS(formatted, "CA$1,20"); - + formatted = nf.format(-45000.1, xlnt::calendar::windows_1900); + TS_ASSERT_EQUALS(formatted, "-$45,000.10"); } void test_bad_country() @@ -356,21 +357,9 @@ public: { xlnt::number_format nf; - nf.set_format_string(""); - TS_ASSERT_THROWS(nf.format(1.2, xlnt::calendar::windows_1900), std::runtime_error); - - nf.set_format_string(";;;"); - TS_ASSERT_THROWS(nf.format(1.2, xlnt::calendar::windows_1900), std::runtime_error); - - nf.set_format_string("[=1]\"first\"General;\"second\"General;\"third\"General"); - TS_ASSERT_THROWS(nf.format(1.2, xlnt::calendar::windows_1900), std::runtime_error); - nf.set_format_string("[=1]\"first\"General;[=2]\"second\"General;[=3]\"third\"General"); TS_ASSERT_THROWS(nf.format(1.2, xlnt::calendar::windows_1900), std::runtime_error); - nf.set_format_string("[=1]\"first\"General;[=2]\"second\"General;\"third\"General;\"fourth\"General"); - TS_ASSERT_THROWS(nf.format(1.2, xlnt::calendar::windows_1900), std::runtime_error); - nf.set_format_string("\"first\"General;\"second\"General;\"third\"General;\"fourth\"General;\"fifth\"General"); TS_ASSERT_THROWS(nf.format(1.2, xlnt::calendar::windows_1900), std::runtime_error); } @@ -379,7 +368,7 @@ public: { TS_ASSERT_EQUALS(xlnt::number_format::text().format("a"), "a"); TS_ASSERT_EQUALS(xlnt::number_format::number().format(1, xlnt::calendar::windows_1900), "1"); - TS_ASSERT_EQUALS(xlnt::number_format::number_comma_separated1().format(1, xlnt::calendar::windows_1900), "1,00"); + TS_ASSERT_EQUALS(xlnt::number_format::number_comma_separated1().format(12345.67, xlnt::calendar::windows_1900), "12,345.67"); /* auto datetime = xlnt::datetime(2016, 6, 24, 0, 45, 58);