1<?php 2/** 3 * Zend Framework 4 * 5 * LICENSE 6 * 7 * This source file is subject to the new BSD license that is bundled 8 * with this package in the file LICENSE.txt. 9 * It is also available through the world-wide-web at this URL: 10 * http://framework.zend.com/license/new-bsd 11 * If you did not receive a copy of the license and are unable to 12 * obtain it through the world-wide-web, please send an email 13 * to license@zend.com so we can send you a copy immediately. 14 * 15 * @category Zend 16 * @package Zend_Locale 17 * @subpackage Format 18 * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com) 19 * @version $Id$ 20 * @license http://framework.zend.com/license/new-bsd New BSD License 21 */ 22 23/** 24 * include needed classes 25 */ 26 27/** 28 * @category Zend 29 * @package Zend_Locale 30 * @subpackage Format 31 * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com) 32 * @license http://framework.zend.com/license/new-bsd New BSD License 33 */ 34class Zend_Locale_Format 35{ 36 const STANDARD = 'auto'; 37 38 private static $_options = array('date_format' => null, 39 'number_format' => null, 40 'format_type' => 'iso', 41 'fix_date' => false, 42 'locale' => null, 43 'cache' => null, 44 'disableCache' => null, 45 'precision' => null); 46 47 /** 48 * Sets class wide options, if no option was given, the actual set options will be returned 49 * The 'precision' option of a value is used to truncate or stretch extra digits. -1 means not to touch the extra digits. 50 * The 'locale' option helps when parsing numbers and dates using separators and month names. 51 * The date format 'format_type' option selects between CLDR/ISO date format specifier tokens and PHP's date() tokens. 52 * The 'fix_date' option enables or disables heuristics that attempt to correct invalid dates. 53 * The 'number_format' option can be used to specify a default number format string 54 * The 'date_format' option can be used to specify a default date format string, but beware of using getDate(), 55 * checkDateFormat() and getTime() after using setOptions() with a 'format'. To use these four methods 56 * with the default date format for a locale, use array('date_format' => null, 'locale' => $locale) for their options. 57 * 58 * @param array $options Array of options, keyed by option name: format_type = 'iso' | 'php', fix_date = true | false, 59 * locale = Zend_Locale | locale string, precision = whole number between -1 and 30 60 * @throws Zend_Locale_Exception 61 * @return array if no option was given 62 */ 63 public static function setOptions(array $options = array()) 64 { 65 self::$_options = self::_checkOptions($options) + self::$_options; 66 return self::$_options; 67 } 68 69 /** 70 * Internal function for checking the options array of proper input values 71 * See {@link setOptions()} for details. 72 * 73 * @param array $options Array of options, keyed by option name: format_type = 'iso' | 'php', fix_date = true | false, 74 * locale = Zend_Locale | locale string, precision = whole number between -1 and 30 75 * @throws Zend_Locale_Exception 76 * @return array if no option was given 77 */ 78 private static function _checkOptions(array $options = array()) 79 { 80 if (count($options) == 0) { 81 return self::$_options; 82 } 83 foreach ($options as $name => $value) { 84 $name = strtolower($name); 85 if ($name !== 'locale') { 86 if (gettype($value) === 'string') { 87 $value = strtolower($value); 88 } 89 } 90 91 switch($name) { 92 case 'number_format' : 93 if ($value == Zend_Locale_Format::STANDARD) { 94 $locale = self::$_options['locale']; 95 if (isset($options['locale'])) { 96 $locale = $options['locale']; 97 } 98 $options['number_format'] = Zend_Locale_Data::getContent($locale, 'decimalnumber'); 99 } else if ((gettype($value) !== 'string') and ($value !== NULL)) { 100 $stringValue = (string)(is_array($value) ? implode(' ', $value) : $value); 101 throw new Zend_Locale_Exception("Unknown number format type '" . gettype($value) . "'. " 102 . "Format '$stringValue' must be a valid number format string."); 103 } 104 break; 105 106 case 'date_format' : 107 if ($value == Zend_Locale_Format::STANDARD) { 108 $locale = self::$_options['locale']; 109 if (isset($options['locale'])) { 110 $locale = $options['locale']; 111 } 112 $options['date_format'] = Zend_Locale_Format::getDateFormat($locale); 113 } else if ((gettype($value) !== 'string') and ($value !== NULL)) { 114 $stringValue = (string)(is_array($value) ? implode(' ', $value) : $value); 115 throw new Zend_Locale_Exception("Unknown dateformat type '" . gettype($value) . "'. " 116 . "Format '$stringValue' must be a valid ISO or PHP date format string."); 117 } else { 118 if (((isset($options['format_type']) === true) and ($options['format_type'] == 'php')) or 119 ((isset($options['format_type']) === false) and (self::$_options['format_type'] == 'php'))) { 120 $options['date_format'] = Zend_Locale_Format::convertPhpToIsoFormat($value); 121 } 122 } 123 break; 124 125 case 'format_type' : 126 if (($value != 'php') && ($value != 'iso')) { 127 throw new Zend_Locale_Exception("Unknown date format type '$value'. Only 'iso' and 'php'" 128 . " are supported."); 129 } 130 break; 131 132 case 'fix_date' : 133 if (($value !== true) && ($value !== false)) { 134 throw new Zend_Locale_Exception("Enabling correction of dates must be either true or false" 135 . "(fix_date='$value')."); 136 } 137 break; 138 139 case 'locale' : 140 $options['locale'] = Zend_Locale::findLocale($value); 141 break; 142 143 case 'cache' : 144 if ($value instanceof Zend_Cache_Core) { 145 Zend_Locale_Data::setCache($value); 146 } 147 break; 148 149 case 'disablecache' : 150 if (null !== $value) { 151 Zend_Locale_Data::disableCache($value); 152 } 153 break; 154 155 case 'precision' : 156 if ($value === NULL) { 157 $value = -1; 158 } 159 160 if (($value < -1) || ($value > 30)) { 161 throw new Zend_Locale_Exception("'$value' precision is not a whole number less than 30."); 162 } 163 break; 164 165 default: 166 throw new Zend_Locale_Exception("Unknown option: '$name' = '$value'"); 167 break; 168 169 } 170 } 171 172 return $options; 173 } 174 175 /** 176 * Changes the numbers/digits within a given string from one script to another 177 * 'Decimal' representated the stardard numbers 0-9, if a script does not exist 178 * an exception will be thrown. 179 * 180 * Examples for conversion from Arabic to Latin numerals: 181 * convertNumerals('١١٠ Tests', 'Arab'); -> returns '100 Tests' 182 * Example for conversion from Latin to Arabic numerals: 183 * convertNumerals('100 Tests', 'Latn', 'Arab'); -> returns '١١٠ Tests' 184 * 185 * @param string $input String to convert 186 * @param string $from Script to parse, see {@link Zend_Locale::getScriptList()} for details. 187 * @param string $to OPTIONAL Script to convert to 188 * @return string Returns the converted input 189 * @throws Zend_Locale_Exception 190 */ 191 public static function convertNumerals($input, $from, $to = null) 192 { 193 if (!self::_getUniCodeSupport()) { 194 trigger_error("Sorry, your PCRE extension does not support UTF8 which is needed for the I18N core", E_USER_NOTICE); 195 } 196 197 $from = strtolower($from); 198 $source = Zend_Locale_Data::getContent('en', 'numberingsystem', $from); 199 if (empty($source)) { 200 throw new Zend_Locale_Exception("Unknown script '$from'. Use 'Latn' for digits 0,1,2,3,4,5,6,7,8,9."); 201 } 202 203 if ($to !== null) { 204 $to = strtolower($to); 205 $target = Zend_Locale_Data::getContent('en', 'numberingsystem', $to); 206 if (empty($target)) { 207 throw new Zend_Locale_Exception("Unknown script '$to'. Use 'Latn' for digits 0,1,2,3,4,5,6,7,8,9."); 208 } 209 } else { 210 $target = '0123456789'; 211 } 212 213 for ($x = 0; $x < 10; ++$x) { 214 $asource[$x] = "/" . iconv_substr($source, $x, 1, 'UTF-8') . "/u"; 215 $atarget[$x] = iconv_substr($target, $x, 1, 'UTF-8'); 216 } 217 218 return preg_replace($asource, $atarget, $input); 219 } 220 221 /** 222 * Returns the normalized number from a localized one 223 * Parsing depends on given locale (grouping and decimal) 224 * 225 * Examples for input: 226 * '2345.4356,1234' = 23455456.1234 227 * '+23,3452.123' = 233452.123 228 * '12343 ' = 12343 229 * '-9456' = -9456 230 * '0' = 0 231 * 232 * @param string $input Input string to parse for numbers 233 * @param array $options Options: locale, precision. See {@link setOptions()} for details. 234 * @return string Returns the extracted number 235 * @throws Zend_Locale_Exception 236 */ 237 public static function getNumber($input, array $options = array()) 238 { 239 $options = self::_checkOptions($options) + self::$_options; 240 if (!is_string($input)) { 241 return $input; 242 } 243 244 if (!self::isNumber($input, $options)) { 245 throw new Zend_Locale_Exception('No localized value in ' . $input . ' found, or the given number does not match the localized format'); 246 } 247 248 // Get correct signs for this locale 249 $symbols = Zend_Locale_Data::getList($options['locale'],'symbols'); 250 // Change locale input to be default number 251 if (($input[0] == $symbols['minus']) && ('-' != $input[0])) { 252 $input = '-' . substr($input, 1); 253 } 254 255 $input = str_replace($symbols['group'],'', $input); 256 if (strpos($input, $symbols['decimal']) !== false) { 257 if ($symbols['decimal'] != '.') { 258 $input = str_replace($symbols['decimal'], ".", $input); 259 } 260 261 $pre = substr($input, strpos($input, '.') + 1); 262 if ($options['precision'] === null) { 263 $options['precision'] = strlen($pre); 264 } 265 266 if (strlen($pre) >= $options['precision']) { 267 $input = substr($input, 0, strlen($input) - strlen($pre) + $options['precision']); 268 } 269 270 if (($options['precision'] == 0) && ($input[strlen($input) - 1] == '.')) { 271 $input = substr($input, 0, -1); 272 } 273 } 274 275 return $input; 276 } 277 278 /** 279 * Returns a locale formatted number depending on the given options. 280 * The seperation and fraction sign is used from the set locale. 281 * ##0.# -> 12345.12345 -> 12345.12345 282 * ##0.00 -> 12345.12345 -> 12345.12 283 * ##,##0.00 -> 12345.12345 -> 12,345.12 284 * 285 * @param string $value Localized number string 286 * @param array $options Options: number_format, locale, precision. See {@link setOptions()} for details. 287 * @return string locale formatted number 288 * @throws Zend_Locale_Exception 289 */ 290 public static function toNumber($value, array $options = array()) 291 { 292 // load class within method for speed 293 294 $value = Zend_Locale_Math::floatalize($value); 295 $value = Zend_Locale_Math::normalize($value); 296 $options = self::_checkOptions($options) + self::$_options; 297 $options['locale'] = (string) $options['locale']; 298 299 // Get correct signs for this locale 300 $symbols = Zend_Locale_Data::getList($options['locale'], 'symbols'); 301 $oenc = self::_getEncoding(); 302 self::_setEncoding('UTF-8'); 303 304 // Get format 305 $format = $options['number_format']; 306 if ($format === null) { 307 $format = Zend_Locale_Data::getContent($options['locale'], 'decimalnumber'); 308 $format = self::_seperateFormat($format, $value, $options['precision']); 309 310 if ($options['precision'] !== null) { 311 $value = Zend_Locale_Math::normalize(Zend_Locale_Math::round($value, $options['precision'])); 312 } 313 } else { 314 // seperate negative format pattern when available 315 $format = self::_seperateFormat($format, $value, $options['precision']); 316 if (strpos($format, '.')) { 317 if (is_numeric($options['precision'])) { 318 $value = Zend_Locale_Math::round($value, $options['precision']); 319 } else { 320 if (substr($format, iconv_strpos($format, '.') + 1, 3) == '###') { 321 $options['precision'] = null; 322 } else { 323 $options['precision'] = iconv_strlen(iconv_substr($format, iconv_strpos($format, '.') + 1, 324 iconv_strrpos($format, '0') - iconv_strpos($format, '.'))); 325 $format = iconv_substr($format, 0, iconv_strpos($format, '.') + 1) . '###' 326 . iconv_substr($format, iconv_strrpos($format, '0') + 1); 327 } 328 } 329 } else { 330 $value = Zend_Locale_Math::round($value, 0); 331 $options['precision'] = 0; 332 } 333 $value = Zend_Locale_Math::normalize($value); 334 } 335 336 if (iconv_strpos($format, '0') === false) { 337 self::_setEncoding($oenc); 338 throw new Zend_Locale_Exception('Wrong format... missing 0'); 339 } 340 341 // get number parts 342 $pos = iconv_strpos($value, '.'); 343 if ($pos !== false) { 344 if ($options['precision'] === null) { 345 $precstr = iconv_substr($value, $pos + 1); 346 } else { 347 $precstr = iconv_substr($value, $pos + 1, $options['precision']); 348 if (iconv_strlen($precstr) < $options['precision']) { 349 $precstr = $precstr . str_pad("0", ($options['precision'] - iconv_strlen($precstr)), "0"); 350 } 351 } 352 } else { 353 if ($options['precision'] > 0) { 354 $precstr = str_pad("0", ($options['precision']), "0"); 355 } 356 } 357 358 if ($options['precision'] === null) { 359 if (isset($precstr)) { 360 $options['precision'] = iconv_strlen($precstr); 361 } else { 362 $options['precision'] = 0; 363 } 364 } 365 366 // get fraction and format lengths 367 if (strpos($value, '.') !== false) { 368 $number = substr((string) $value, 0, strpos($value, '.')); 369 } else { 370 $number = $value; 371 } 372 373 $prec = call_user_func(Zend_Locale_Math::$sub, $value, $number, $options['precision']); 374 $prec = Zend_Locale_Math::floatalize($prec); 375 $prec = Zend_Locale_Math::normalize($prec); 376 if (iconv_strpos($prec, '-') !== false) { 377 $prec = iconv_substr($prec, 1); 378 } 379 380 if (($prec == 0) and ($options['precision'] > 0)) { 381 $prec = "0.0"; 382 } 383 384 if (($options['precision'] + 2) > iconv_strlen($prec)) { 385 $prec = str_pad((string) $prec, $options['precision'] + 2, "0", STR_PAD_RIGHT); 386 } 387 388 if (iconv_strpos($number, '-') !== false) { 389 $number = iconv_substr($number, 1); 390 } 391 $group = iconv_strrpos($format, ','); 392 $group2 = iconv_strpos ($format, ','); 393 $point = iconv_strpos ($format, '0'); 394 // Add fraction 395 $rest = ""; 396 if (iconv_strpos($format, '.')) { 397 $rest = iconv_substr($format, iconv_strpos($format, '.') + 1); 398 $length = iconv_strlen($rest); 399 for($x = 0; $x < $length; ++$x) { 400 if (($rest[0] == '0') || ($rest[0] == '#')) { 401 $rest = iconv_substr($rest, 1); 402 } 403 } 404 $format = iconv_substr($format, 0, iconv_strlen($format) - iconv_strlen($rest)); 405 } 406 407 if ($options['precision'] == '0') { 408 if (iconv_strrpos($format, '-') != 0) { 409 $format = iconv_substr($format, 0, $point) 410 . iconv_substr($format, iconv_strrpos($format, '#') + 2); 411 } else { 412 $format = iconv_substr($format, 0, $point); 413 } 414 } else { 415 $format = iconv_substr($format, 0, $point) . $symbols['decimal'] 416 . iconv_substr($prec, 2); 417 } 418 419 $format .= $rest; 420 // Add seperation 421 if ($group == 0) { 422 // no seperation 423 $format = $number . iconv_substr($format, $point); 424 } else if ($group == $group2) { 425 // only 1 seperation 426 $seperation = ($point - $group); 427 for ($x = iconv_strlen($number); $x > $seperation; $x -= $seperation) { 428 if (iconv_substr($number, 0, $x - $seperation) !== "") { 429 $number = iconv_substr($number, 0, $x - $seperation) . $symbols['group'] 430 . iconv_substr($number, $x - $seperation); 431 } 432 } 433 $format = iconv_substr($format, 0, iconv_strpos($format, '#')) . $number . iconv_substr($format, $point); 434 } else { 435 436 // 2 seperations 437 if (iconv_strlen($number) > ($point - $group)) { 438 $seperation = ($point - $group); 439 $number = iconv_substr($number, 0, iconv_strlen($number) - $seperation) . $symbols['group'] 440 . iconv_substr($number, iconv_strlen($number) - $seperation); 441 442 if ((iconv_strlen($number) - 1) > ($point - $group + 1)) { 443 $seperation2 = ($group - $group2 - 1); 444 for ($x = iconv_strlen($number) - $seperation2 - 2; $x > $seperation2; $x -= $seperation2) { 445 $number = iconv_substr($number, 0, $x - $seperation2) . $symbols['group'] 446 . iconv_substr($number, $x - $seperation2); 447 } 448 } 449 450 } 451 $format = iconv_substr($format, 0, iconv_strpos($format, '#')) . $number . iconv_substr($format, $point); 452 } 453 // set negative sign 454 if (call_user_func(Zend_Locale_Math::$comp, $value, 0, $options['precision']) < 0) { 455 if (iconv_strpos($format, '-') === false) { 456 $format = $symbols['minus'] . $format; 457 } else { 458 $format = str_replace('-', $symbols['minus'], $format); 459 } 460 } 461 462 self::_setEncoding($oenc); 463 return (string) $format; 464 } 465 466 /** 467 * @param string $format 468 * @param string $value 469 * @param int $precision 470 * @return string 471 */ 472 private static function _seperateFormat($format, $value, $precision) 473 { 474 if (iconv_strpos($format, ';') !== false) { 475 if (call_user_func(Zend_Locale_Math::$comp, $value, 0, $precision) < 0) { 476 $tmpformat = iconv_substr($format, iconv_strpos($format, ';') + 1); 477 if ($tmpformat[0] == '(') { 478 $format = iconv_substr($format, 0, iconv_strpos($format, ';')); 479 } else { 480 $format = $tmpformat; 481 } 482 } else { 483 $format = iconv_substr($format, 0, iconv_strpos($format, ';')); 484 } 485 } 486 487 return $format; 488 } 489 490 491 /** 492 * Checks if the input contains a normalized or localized number 493 * 494 * @param string $input Localized number string 495 * @param array $options Options: locale. See {@link setOptions()} for details. 496 * @return boolean Returns true if a number was found 497 */ 498 public static function isNumber($input, array $options = array()) 499 { 500 if (!self::_getUniCodeSupport()) { 501 trigger_error("Sorry, your PCRE extension does not support UTF8 which is needed for the I18N core", E_USER_NOTICE); 502 } 503 504 $options = self::_checkOptions($options) + self::$_options; 505 506 // Get correct signs for this locale 507 $symbols = Zend_Locale_Data::getList($options['locale'],'symbols'); 508 509 $regexs = Zend_Locale_Format::_getRegexForType('decimalnumber', $options); 510 $regexs = array_merge($regexs, Zend_Locale_Format::_getRegexForType('scientificnumber', $options)); 511 if (!empty($input) && ($input[0] == $symbols['decimal'])) { 512 $input = 0 . $input; 513 } 514 foreach ($regexs as $regex) { 515 preg_match($regex, $input, $found); 516 if (isset($found[0])) { 517 return true; 518 } 519 } 520 521 return false; 522 } 523 524 /** 525 * Internal method to convert cldr number syntax into regex 526 * 527 * @param string $type 528 * @param array $options Options: locale. See {@link setOptions()} for details. 529 * @return string 530 * @throws Zend_Locale_Exception 531 */ 532 private static function _getRegexForType($type, $options) 533 { 534 $decimal = Zend_Locale_Data::getContent($options['locale'], $type); 535 $decimal = preg_replace('/[^#0,;\.\-Ee]/u', '',$decimal); 536 $patterns = explode(';', $decimal); 537 538 if (count($patterns) == 1) { 539 $patterns[1] = '-' . $patterns[0]; 540 } 541 542 $symbols = Zend_Locale_Data::getList($options['locale'],'symbols'); 543 544 foreach($patterns as $pkey => $pattern) { 545 $regex[$pkey] = '/^'; 546 $rest = 0; 547 $end = null; 548 if (strpos($pattern, '.') !== false) { 549 $end = substr($pattern, strpos($pattern, '.') + 1); 550 $pattern = substr($pattern, 0, -strlen($end) - 1); 551 } 552 553 if (strpos($pattern, ',') !== false) { 554 $parts = explode(',', $pattern); 555 $count = count($parts); 556 foreach($parts as $key => $part) { 557 switch ($part) { 558 case '#': 559 case '-#': 560 if ($part[0] == '-') { 561 $regex[$pkey] .= '[' . $symbols['minus'] . '-]{0,1}'; 562 } else { 563 $regex[$pkey] .= '[' . $symbols['plus'] . '+]{0,1}'; 564 } 565 566 if (($parts[$key + 1]) == '##0') { 567 $regex[$pkey] .= '[0-9]{1,3}'; 568 } else if (($parts[$key + 1]) == '##') { 569 $regex[$pkey] .= '[0-9]{1,2}'; 570 } else { 571 throw new Zend_Locale_Exception('Unsupported token for numberformat (Pos 1):"' . $pattern . '"'); 572 } 573 break; 574 case '##': 575 if ($parts[$key + 1] == '##0') { 576 $regex[$pkey] .= '(\\' . $symbols['group'] . '{0,1}[0-9]{2})*'; 577 } else { 578 throw new Zend_Locale_Exception('Unsupported token for numberformat (Pos 2):"' . $pattern . '"'); 579 } 580 break; 581 case '##0': 582 if ($parts[$key - 1] == '##') { 583 $regex[$pkey] .= '[0-9]'; 584 } else if (($parts[$key - 1] == '#') || ($parts[$key - 1] == '-#')) { 585 $regex[$pkey] .= '(\\' . $symbols['group'] . '{0,1}[0-9]{3})*'; 586 } else { 587 throw new Zend_Locale_Exception('Unsupported token for numberformat (Pos 3):"' . $pattern . '"'); 588 } 589 break; 590 case '#0': 591 if ($key == 0) { 592 $regex[$pkey] .= '[0-9]*'; 593 } else { 594 throw new Zend_Locale_Exception('Unsupported token for numberformat (Pos 4):"' . $pattern . '"'); 595 } 596 break; 597 } 598 } 599 } 600 601 if (strpos($pattern, 'E') !== false) { 602 if (($pattern == '#E0') || ($pattern == '#E00')) { 603 $regex[$pkey] .= '[' . $symbols['plus']. '+]{0,1}[0-9]{1,}(\\' . $symbols['decimal'] . '[0-9]{1,})*[eE][' . $symbols['plus']. '+]{0,1}[0-9]{1,}'; 604 } else if (($pattern == '-#E0') || ($pattern == '-#E00')) { 605 $regex[$pkey] .= '[' . $symbols['minus']. '-]{0,1}[0-9]{1,}(\\' . $symbols['decimal'] . '[0-9]{1,})*[eE][' . $symbols['minus']. '-]{0,1}[0-9]{1,}'; 606 } else { 607 throw new Zend_Locale_Exception('Unsupported token for numberformat (Pos 5):"' . $pattern . '"'); 608 } 609 } 610 611 if (!empty($end)) { 612 if ($end == '###') { 613 $regex[$pkey] .= '(\\' . $symbols['decimal'] . '{1}[0-9]{1,}){0,1}'; 614 } else if ($end == '###-') { 615 $regex[$pkey] .= '(\\' . $symbols['decimal'] . '{1}[0-9]{1,}){0,1}[' . $symbols['minus']. '-]'; 616 } else { 617 throw new Zend_Locale_Exception('Unsupported token for numberformat (Pos 6):"' . $pattern . '"'); 618 } 619 } 620 621 $regex[$pkey] .= '$/u'; 622 } 623 624 return $regex; 625 } 626 627 /** 628 * Alias for getNumber 629 * 630 * @param string $input Number to localize 631 * @param array $options Options: locale, precision. See {@link setOptions()} for details. 632 * @return float 633 */ 634 public static function getFloat($input, array $options = array()) 635 { 636 return floatval(self::getNumber($input, $options)); 637 } 638 639 /** 640 * Returns a locale formatted integer number 641 * Alias for toNumber() 642 * 643 * @param string $value Number to normalize 644 * @param array $options Options: locale, precision. See {@link setOptions()} for details. 645 * @return string Locale formatted number 646 */ 647 public static function toFloat($value, array $options = array()) 648 { 649 $options['number_format'] = Zend_Locale_Format::STANDARD; 650 return self::toNumber($value, $options); 651 } 652 653 /** 654 * Returns if a float was found 655 * Alias for isNumber() 656 * 657 * @param string $value Localized number string 658 * @param array $options Options: locale. See {@link setOptions()} for details. 659 * @return boolean Returns true if a number was found 660 */ 661 public static function isFloat($value, array $options = array()) 662 { 663 return self::isNumber($value, $options); 664 } 665 666 /** 667 * Returns the first found integer from an string 668 * Parsing depends on given locale (grouping and decimal) 669 * 670 * Examples for input: 671 * ' 2345.4356,1234' = 23455456 672 * '+23,3452.123' = 233452 673 * ' 12343 ' = 12343 674 * '-9456km' = -9456 675 * '0' = 0 676 * '(-){0,1}(\d+(\.){0,1})*(\,){0,1})\d+' 677 * 678 * @param string $input Input string to parse for numbers 679 * @param array $options Options: locale. See {@link setOptions()} for details. 680 * @return integer Returns the extracted number 681 */ 682 public static function getInteger($input, array $options = array()) 683 { 684 $options['precision'] = 0; 685 return intval(self::getFloat($input, $options)); 686 } 687 688 /** 689 * Returns a localized number 690 * 691 * @param string $value Number to normalize 692 * @param array $options Options: locale. See {@link setOptions()} for details. 693 * @return string Locale formatted number 694 */ 695 public static function toInteger($value, array $options = array()) 696 { 697 $options['precision'] = 0; 698 $options['number_format'] = Zend_Locale_Format::STANDARD; 699 return self::toNumber($value, $options); 700 } 701 702 /** 703 * Returns if a integer was found 704 * 705 * @param string $value Localized number string 706 * @param array $options Options: locale. See {@link setOptions()} for details. 707 * @return boolean Returns true if a integer was found 708 */ 709 public static function isInteger($value, array $options = array()) 710 { 711 if (!self::isNumber($value, $options)) { 712 return false; 713 } 714 715 if (self::getInteger($value, $options) == self::getFloat($value, $options)) { 716 return true; 717 } 718 719 return false; 720 } 721 722 /** 723 * Converts a format string from PHP's date format to ISO format 724 * Remember that Zend Date always returns localized string, so a month name which returns the english 725 * month in php's date() will return the translated month name with this function... use 'en' as locale 726 * if you are in need of the original english names 727 * 728 * The conversion has the following restrictions: 729 * 'a', 'A' - Meridiem is not explicit upper/lowercase, you have to upper/lowercase the translated value yourself 730 * 731 * @param string $format Format string in PHP's date format 732 * @return string Format string in ISO format 733 */ 734 public static function convertPhpToIsoFormat($format) 735 { 736 if ($format === null) { 737 return null; 738 } 739 740 $convert = array( 741 'd' => 'dd' , 'D' => 'EE' , 'j' => 'd' , 'l' => 'EEEE', 742 'N' => 'eee' , 'S' => 'SS' , 'w' => 'e' , 'z' => 'D' , 743 'W' => 'ww' , 'F' => 'MMMM', 'm' => 'MM' , 'M' => 'MMM' , 744 'n' => 'M' , 't' => 'ddd' , 'L' => 'l' , 'o' => 'YYYY', 745 'Y' => 'yyyy', 'y' => 'yy' , 'a' => 'a' , 'A' => 'a' , 746 'B' => 'B' , 'g' => 'h' , 'G' => 'H' , 'h' => 'hh' , 747 'H' => 'HH' , 'i' => 'mm' , 's' => 'ss' , 'e' => 'zzzz', 748 'I' => 'I' , 'O' => 'Z' , 'P' => 'ZZZZ', 'T' => 'z' , 749 'Z' => 'X' , 'c' => 'yyyy-MM-ddTHH:mm:ssZZZZ', 'r' => 'r', 750 'U' => 'U', 751 ); 752 $escaped = false; 753 $inEscapedString = false; 754 $converted = array(); 755 foreach (str_split($format) as $char) { 756 if (!$escaped && $char == '\\') { 757 // Next char will be escaped: let's remember it 758 $escaped = true; 759 } elseif ($escaped) { 760 if (!$inEscapedString) { 761 // First escaped string: start the quoted chunk 762 $converted[] = "'"; 763 $inEscapedString = true; 764 } 765 // Since the previous char was a \ and we are in the quoted 766 // chunk, let's simply add $char as it is 767 $converted[] = $char; 768 $escaped = false; 769 } elseif ($char == "'") { 770 // Single quotes need to be escaped like this 771 $converted[] = "''"; 772 } else { 773 if ($inEscapedString) { 774 // Close the single-quoted chunk 775 $converted[] = "'"; 776 $inEscapedString = false; 777 } 778 // Convert the unescaped char if needed 779 if (isset($convert[$char])) { 780 $converted[] = $convert[$char]; 781 } else { 782 $converted[] = $char; 783 } 784 } 785 } 786 787 return implode($converted); 788 } 789 790 /** 791 * Parse date and split in named array fields 792 * 793 * @param string $date Date string to parse 794 * @param array $options Options: format_type, fix_date, locale, date_format. See {@link setOptions()} for details. 795 * @return array Possible array members: day, month, year, hour, minute, second, fixed, format 796 * @throws Zend_Locale_Exception 797 */ 798 private static function _parseDate($date, $options) 799 { 800 if (!self::_getUniCodeSupport()) { 801 trigger_error("Sorry, your PCRE extension does not support UTF8 which is needed for the I18N core", E_USER_NOTICE); 802 } 803 804 $options = self::_checkOptions($options) + self::$_options; 805 $test = array('h', 'H', 'm', 's', 'y', 'Y', 'M', 'd', 'D', 'E', 'S', 'l', 'B', 'I', 806 'X', 'r', 'U', 'G', 'w', 'e', 'a', 'A', 'Z', 'z', 'v'); 807 808 $format = $options['date_format']; 809 $number = $date; // working copy 810 $result['date_format'] = $format; // save the format used to normalize $number (convenience) 811 $result['locale'] = $options['locale']; // save the locale used to normalize $number (convenience) 812 813 $oenc = self::_getEncoding(); 814 self::_setEncoding('UTF-8'); 815 $day = iconv_strpos($format, 'd'); 816 $month = iconv_strpos($format, 'M'); 817 $year = iconv_strpos($format, 'y'); 818 $hour = iconv_strpos($format, 'H'); 819 $min = iconv_strpos($format, 'm'); 820 $sec = iconv_strpos($format, 's'); 821 $am = null; 822 if ($hour === false) { 823 $hour = iconv_strpos($format, 'h'); 824 } 825 if ($year === false) { 826 $year = iconv_strpos($format, 'Y'); 827 } 828 if ($day === false) { 829 $day = iconv_strpos($format, 'E'); 830 if ($day === false) { 831 $day = iconv_strpos($format, 'D'); 832 } 833 } 834 835 if ($day !== false) { 836 $parse[$day] = 'd'; 837 if (!empty($options['locale']) && ($options['locale'] !== 'root') && 838 (!is_object($options['locale']) || ((string) $options['locale'] !== 'root'))) { 839 // erase day string 840 $daylist = Zend_Locale_Data::getList($options['locale'], 'day'); 841 foreach($daylist as $key => $name) { 842 if (iconv_strpos($number, $name) !== false) { 843 $number = str_replace($name, "EEEE", $number); 844 break; 845 } 846 } 847 } 848 } 849 $position = false; 850 851 if ($month !== false) { 852 $parse[$month] = 'M'; 853 if (!empty($options['locale']) && ($options['locale'] !== 'root') && 854 (!is_object($options['locale']) || ((string) $options['locale'] !== 'root'))) { 855 // prepare to convert month name to their numeric equivalents, if requested, 856 // and we have a $options['locale'] 857 $position = self::_replaceMonth($number, Zend_Locale_Data::getList($options['locale'], 858 'month')); 859 if ($position === false) { 860 $position = self::_replaceMonth($number, Zend_Locale_Data::getList($options['locale'], 861 'month', array('gregorian', 'format', 'abbreviated'))); 862 } 863 } 864 } 865 if ($year !== false) { 866 $parse[$year] = 'y'; 867 } 868 if ($hour !== false) { 869 $parse[$hour] = 'H'; 870 } 871 if ($min !== false) { 872 $parse[$min] = 'm'; 873 } 874 if ($sec !== false) { 875 $parse[$sec] = 's'; 876 } 877 878 if (empty($parse)) { 879 self::_setEncoding($oenc); 880 throw new Zend_Locale_Exception("Unknown date format, neither date nor time in '" . $format . "' found"); 881 } 882 ksort($parse); 883 884 // get daytime 885 if (iconv_strpos($format, 'a') !== false) { 886 if (iconv_strpos(strtoupper($number), strtoupper(Zend_Locale_Data::getContent($options['locale'], 'am'))) !== false) { 887 $am = true; 888 } else if (iconv_strpos(strtoupper($number), strtoupper(Zend_Locale_Data::getContent($options['locale'], 'pm'))) !== false) { 889 $am = false; 890 } 891 } 892 893 // split number parts 894 $split = false; 895 preg_match_all('/\d+/u', $number, $splitted); 896 897 if (count($splitted[0]) == 0) { 898 self::_setEncoding($oenc); 899 throw new Zend_Locale_Exception("No date part in '$date' found."); 900 } 901 if (count($splitted[0]) == 1) { 902 $split = 0; 903 } 904 $cnt = 0; 905 foreach($parse as $key => $value) { 906 907 switch($value) { 908 case 'd': 909 if ($split === false) { 910 if (count($splitted[0]) > $cnt) { 911 $result['day'] = $splitted[0][$cnt]; 912 } 913 } else { 914 $result['day'] = iconv_substr($splitted[0][0], $split, 2); 915 $split += 2; 916 } 917 ++$cnt; 918 break; 919 case 'M': 920 if ($split === false) { 921 if (count($splitted[0]) > $cnt) { 922 $result['month'] = $splitted[0][$cnt]; 923 } 924 } else { 925 $result['month'] = iconv_substr($splitted[0][0], $split, 2); 926 $split += 2; 927 } 928 ++$cnt; 929 break; 930 case 'y': 931 $length = 2; 932 if ((iconv_substr($format, $year, 4) == 'yyyy') 933 || (iconv_substr($format, $year, 4) == 'YYYY')) { 934 $length = 4; 935 } 936 937 if ($split === false) { 938 if (count($splitted[0]) > $cnt) { 939 $result['year'] = $splitted[0][$cnt]; 940 } 941 } else { 942 $result['year'] = iconv_substr($splitted[0][0], $split, $length); 943 $split += $length; 944 } 945 946 ++$cnt; 947 break; 948 case 'H': 949 if ($split === false) { 950 if (count($splitted[0]) > $cnt) { 951 $result['hour'] = $splitted[0][$cnt]; 952 } 953 } else { 954 $result['hour'] = iconv_substr($splitted[0][0], $split, 2); 955 $split += 2; 956 } 957 ++$cnt; 958 break; 959 case 'm': 960 if ($split === false) { 961 if (count($splitted[0]) > $cnt) { 962 $result['minute'] = $splitted[0][$cnt]; 963 } 964 } else { 965 $result['minute'] = iconv_substr($splitted[0][0], $split, 2); 966 $split += 2; 967 } 968 ++$cnt; 969 break; 970 case 's': 971 if ($split === false) { 972 if (count($splitted[0]) > $cnt) { 973 $result['second'] = $splitted[0][$cnt]; 974 } 975 } else { 976 $result['second'] = iconv_substr($splitted[0][0], $split, 2); 977 $split += 2; 978 } 979 ++$cnt; 980 break; 981 } 982 } 983 984 // AM/PM correction 985 if ($hour !== false) { 986 if (($am === true) and ($result['hour'] == 12)){ 987 $result['hour'] = 0; 988 } else if (($am === false) and ($result['hour'] != 12)) { 989 $result['hour'] += 12; 990 } 991 } 992 993 if ($options['fix_date'] === true) { 994 $result['fixed'] = 0; // nothing has been "fixed" by swapping date parts around (yet) 995 } 996 997 if ($day !== false) { 998 // fix false month 999 if (isset($result['day']) and isset($result['month'])) { 1000 if (($position !== false) and ((iconv_strpos($date, $result['day']) === false) or 1001 (isset($result['year']) and (iconv_strpos($date, $result['year']) === false)))) { 1002 if ($options['fix_date'] !== true) { 1003 self::_setEncoding($oenc); 1004 throw new Zend_Locale_Exception("Unable to parse date '$date' using '" . $format 1005 . "' (false month, $position, $month)"); 1006 } 1007 $temp = $result['day']; 1008 $result['day'] = $result['month']; 1009 $result['month'] = $temp; 1010 $result['fixed'] = 1; 1011 } 1012 } 1013 1014 // fix switched values d <> y 1015 if (isset($result['day']) and isset($result['year'])) { 1016 if ($result['day'] > 31) { 1017 if ($options['fix_date'] !== true) { 1018 self::_setEncoding($oenc); 1019 throw new Zend_Locale_Exception("Unable to parse date '$date' using '" 1020 . $format . "' (d <> y)"); 1021 } 1022 $temp = $result['year']; 1023 $result['year'] = $result['day']; 1024 $result['day'] = $temp; 1025 $result['fixed'] = 2; 1026 } 1027 } 1028 1029 // fix switched values M <> y 1030 if (isset($result['month']) and isset($result['year'])) { 1031 if ($result['month'] > 31) { 1032 if ($options['fix_date'] !== true) { 1033 self::_setEncoding($oenc); 1034 throw new Zend_Locale_Exception("Unable to parse date '$date' using '" 1035 . $format . "' (M <> y)"); 1036 } 1037 $temp = $result['year']; 1038 $result['year'] = $result['month']; 1039 $result['month'] = $temp; 1040 $result['fixed'] = 3; 1041 } 1042 } 1043 1044 // fix switched values M <> d 1045 if (isset($result['month']) and isset($result['day'])) { 1046 if ($result['month'] > 12) { 1047 if ($options['fix_date'] !== true || $result['month'] > 31) { 1048 self::_setEncoding($oenc); 1049 throw new Zend_Locale_Exception("Unable to parse date '$date' using '" 1050 . $format . "' (M <> d)"); 1051 } 1052 $temp = $result['day']; 1053 $result['day'] = $result['month']; 1054 $result['month'] = $temp; 1055 $result['fixed'] = 4; 1056 } 1057 } 1058 } 1059 1060 if (isset($result['year'])) { 1061 if (((iconv_strlen($result['year']) == 2) && ($result['year'] < 10)) || 1062 (((iconv_strpos($format, 'yy') !== false) && (iconv_strpos($format, 'yyyy') === false)) || 1063 ((iconv_strpos($format, 'YY') !== false) && (iconv_strpos($format, 'YYYY') === false)))) { 1064 if (($result['year'] >= 0) && ($result['year'] < 100)) { 1065 if ($result['year'] < 70) { 1066 $result['year'] = (int) $result['year'] + 100; 1067 } 1068 1069 $result['year'] = (int) $result['year'] + 1900; 1070 } 1071 } 1072 } 1073 1074 self::_setEncoding($oenc); 1075 return $result; 1076 } 1077 1078 /** 1079 * Search $number for a month name found in $monthlist, and replace if found. 1080 * 1081 * @param string $number Date string (modified) 1082 * @param array $monthlist List of month names 1083 * 1084 * @return int|false Position of replaced string (false if nothing replaced) 1085 */ 1086 protected static function _replaceMonth(&$number, $monthlist) 1087 { 1088 // If $locale was invalid, $monthlist will default to a "root" identity 1089 // mapping for each month number from 1 to 12. 1090 // If no $locale was given, or $locale was invalid, do not use this identity mapping to normalize. 1091 // Otherwise, translate locale aware month names in $number to their numeric equivalents. 1092 $position = false; 1093 if ($monthlist && $monthlist[1] != 1) { 1094 foreach($monthlist as $key => $name) { 1095 if (($position = iconv_strpos($number, $name, 0, 'UTF-8')) !== false) { 1096 $number = str_ireplace($name, $key, $number); 1097 return $position; 1098 } 1099 } 1100 } 1101 1102 return false; 1103 } 1104 1105 /** 1106 * Returns the default date format for $locale. 1107 * 1108 * @param string|Zend_Locale $locale OPTIONAL Locale of $number, possibly in string form (e.g. 'de_AT') 1109 * @return string format 1110 * @throws Zend_Locale_Exception throws an exception when locale data is broken 1111 */ 1112 public static function getDateFormat($locale = null) 1113 { 1114 $format = Zend_Locale_Data::getContent($locale, 'date'); 1115 if (empty($format)) { 1116 throw new Zend_Locale_Exception("failed to receive data from locale $locale"); 1117 } 1118 1119 return $format; 1120 } 1121 1122 /** 1123 * Returns an array with the normalized date from an locale date 1124 * a input of 10.01.2006 without a $locale would return: 1125 * array ('day' => 10, 'month' => 1, 'year' => 2006) 1126 * The 'locale' option is only used to convert human readable day 1127 * and month names to their numeric equivalents. 1128 * The 'format' option allows specification of self-defined date formats, 1129 * when not using the default format for the 'locale'. 1130 * 1131 * @param string $date Date string 1132 * @param array $options Options: format_type, fix_date, locale, date_format. See {@link setOptions()} for details. 1133 * @return array Possible array members: day, month, year, hour, minute, second, fixed, format 1134 */ 1135 public static function getDate($date, array $options = array()) 1136 { 1137 $options = self::_checkOptions($options) + self::$_options; 1138 if (empty($options['date_format'])) { 1139 $options['format_type'] = 'iso'; 1140 $options['date_format'] = self::getDateFormat($options['locale']); 1141 } 1142 1143 return self::_parseDate($date, $options); 1144 } 1145 1146 /** 1147 * Returns if the given datestring contains all date parts from the given format. 1148 * If no format is given, the default date format from the locale is used 1149 * If you want to check if the date is a proper date you should use Zend_Date::isDate() 1150 * 1151 * @param string $date Date string 1152 * @param array $options Options: format_type, fix_date, locale, date_format. See {@link setOptions()} for details. 1153 * @return boolean 1154 */ 1155 public static function checkDateFormat($date, array $options = array()) 1156 { 1157 try { 1158 $date = self::getDate($date, $options); 1159 } catch (Exception $e) { 1160 return false; 1161 } 1162 1163 if (empty($options['date_format'])) { 1164 $options['format_type'] = 'iso'; 1165 $options['date_format'] = self::getDateFormat(isset($options['locale']) ? $options['locale'] : null); 1166 } 1167 $options = self::_checkOptions($options) + self::$_options; 1168 1169 // day expected but not parsed 1170 if ((iconv_strpos($options['date_format'], 'd', 0, 'UTF-8') !== false) and (!isset($date['day']) or ($date['day'] === ""))) { 1171 return false; 1172 } 1173 1174 // month expected but not parsed 1175 if ((iconv_strpos($options['date_format'], 'M', 0, 'UTF-8') !== false) and (!isset($date['month']) or ($date['month'] === ""))) { 1176 return false; 1177 } 1178 1179 // year expected but not parsed 1180 if (((iconv_strpos($options['date_format'], 'Y', 0, 'UTF-8') !== false) or 1181 (iconv_strpos($options['date_format'], 'y', 0, 'UTF-8') !== false)) and (!isset($date['year']) or ($date['year'] === ""))) { 1182 return false; 1183 } 1184 1185 // second expected but not parsed 1186 if ((iconv_strpos($options['date_format'], 's', 0, 'UTF-8') !== false) and (!isset($date['second']) or ($date['second'] === ""))) { 1187 return false; 1188 } 1189 1190 // minute expected but not parsed 1191 if ((iconv_strpos($options['date_format'], 'm', 0, 'UTF-8') !== false) and (!isset($date['minute']) or ($date['minute'] === ""))) { 1192 return false; 1193 } 1194 1195 // hour expected but not parsed 1196 if (((iconv_strpos($options['date_format'], 'H', 0, 'UTF-8') !== false) or 1197 (iconv_strpos($options['date_format'], 'h', 0, 'UTF-8') !== false)) and (!isset($date['hour']) or ($date['hour'] === ""))) { 1198 return false; 1199 } 1200 1201 return true; 1202 } 1203 1204 /** 1205 * Returns the default time format for $locale. 1206 * 1207 * @param string|Zend_Locale $locale OPTIONAL Locale of $number, possibly in string form (e.g. 'de_AT') 1208 * @return string format 1209 * @throws Zend_Locale_Exception 1210 */ 1211 public static function getTimeFormat($locale = null) 1212 { 1213 $format = Zend_Locale_Data::getContent($locale, 'time'); 1214 if (empty($format)) { 1215 throw new Zend_Locale_Exception("failed to receive data from locale $locale"); 1216 } 1217 return $format; 1218 } 1219 1220 /** 1221 * Returns an array with 'hour', 'minute', and 'second' elements extracted from $time 1222 * according to the order described in $format. For a format of 'H:i:s', and 1223 * an input of 11:20:55, getTime() would return: 1224 * array ('hour' => 11, 'minute' => 20, 'second' => 55) 1225 * The optional $locale parameter may be used to help extract times from strings 1226 * containing both a time and a day or month name. 1227 * 1228 * @param string $time Time string 1229 * @param array $options Options: format_type, fix_date, locale, date_format. See {@link setOptions()} for details. 1230 * @return array Possible array members: day, month, year, hour, minute, second, fixed, format 1231 */ 1232 public static function getTime($time, array $options = array()) 1233 { 1234 $options = self::_checkOptions($options) + self::$_options; 1235 if (empty($options['date_format'])) { 1236 $options['format_type'] = 'iso'; 1237 $options['date_format'] = self::getTimeFormat($options['locale']); 1238 } 1239 return self::_parseDate($time, $options); 1240 } 1241 1242 /** 1243 * Returns the default datetime format for $locale. 1244 * 1245 * @param string|Zend_Locale $locale OPTIONAL Locale of $number, possibly in string form (e.g. 'de_AT') 1246 * @return string format 1247 * @throws Zend_Locale_Exception 1248 */ 1249 public static function getDateTimeFormat($locale = null) 1250 { 1251 $format = Zend_Locale_Data::getContent($locale, 'datetime'); 1252 if (empty($format)) { 1253 throw new Zend_Locale_Exception("failed to receive data from locale $locale"); 1254 } 1255 return $format; 1256 } 1257 1258 /** 1259 * Returns an array with 'year', 'month', 'day', 'hour', 'minute', and 'second' elements 1260 * extracted from $datetime according to the order described in $format. For a format of 'd.M.y H:i:s', 1261 * and an input of 10.05.1985 11:20:55, getDateTime() would return: 1262 * array ('year' => 1985, 'month' => 5, 'day' => 10, 'hour' => 11, 'minute' => 20, 'second' => 55) 1263 * The optional $locale parameter may be used to help extract times from strings 1264 * containing both a time and a day or month name. 1265 * 1266 * @param string $datetime DateTime string 1267 * @param array $options Options: format_type, fix_date, locale, date_format. See {@link setOptions()} for details. 1268 * @return array Possible array members: day, month, year, hour, minute, second, fixed, format 1269 */ 1270 public static function getDateTime($datetime, array $options = array()) 1271 { 1272 $options = self::_checkOptions($options) + self::$_options; 1273 if (empty($options['date_format'])) { 1274 $options['format_type'] = 'iso'; 1275 $options['date_format'] = self::getDateTimeFormat($options['locale']); 1276 } 1277 return self::_parseDate($datetime, $options); 1278 } 1279 1280 /** 1281 * Internal method to detect of Unicode supports UTF8 1282 * which should be enabled within vanilla php installations 1283 * 1284 * @return boolean 1285 */ 1286 protected static function _getUniCodeSupport() 1287 { 1288 return (@preg_match('/\pL/u', 'a')) ? true : false; 1289 } 1290 1291 /** 1292 * Internal method to retrieve the current encoding via the ini setting 1293 * default_charset for PHP >= 5.6 or iconv_get_encoding otherwise. 1294 * 1295 * @return string 1296 */ 1297 protected static function _getEncoding() 1298 { 1299 $oenc = PHP_VERSION_ID < 50600 1300 ? iconv_get_encoding('internal_encoding') 1301 : ini_get('default_charset'); 1302 1303 return $oenc; 1304 } 1305 1306 /** 1307 * Internal method to set the encoding via the ini setting 1308 * default_charset for PHP >= 5.6 or iconv_set_encoding otherwise. 1309 * 1310 * @param string $encoding 1311 * @return void 1312 */ 1313 protected static function _setEncoding($encoding) 1314 { 1315 if (PHP_VERSION_ID < 50600) { 1316 iconv_set_encoding('internal_encoding', $encoding); 1317 } else { 1318 ini_set('default_charset', $encoding); 1319 } 1320 } 1321} 1322