Format.php 46 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087
  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-2008 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. * include needed classes
  24. */
  25. require_once 'Zend/Locale/Data.php';
  26. /**
  27. * @category Zend
  28. * @package Zend_Locale
  29. * @subpackage Format
  30. * @copyright Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
  31. * @license http://framework.zend.com/license/new-bsd New BSD License
  32. */
  33. class Zend_Locale_Format
  34. {
  35. const STANDARD = 'auto';
  36. private static $_options = array('date_format' => null,
  37. 'number_format' => null,
  38. 'format_type' => 'iso',
  39. 'fix_date' => false,
  40. 'locale' => null,
  41. 'cache' => null,
  42. 'precision' => null);
  43. private static $_signs = array(
  44. 'Latn' => array('0', '1', '2', '3', '4', '5', '6', '7', '8', '9'), // Latn - default latin
  45. 'Arab' => array( '٠', '١', '٢', '٣', '٤', '٥', '٦', '٧', '٨', '٩'), // 0660 - 0669 arabic
  46. 'Deva' => array( '०', '१', '२', '३', '४', '५', '६', '७', '८', '९'), // 0966 - 096F devanagari
  47. 'Beng' => array( '০', '১', '২', '৩', '৪', '৫', '৬', '৭', '৮', '৯'), // 09E6 - 09EF bengali
  48. 'Guru' => array( '੦', '੧', '੨', '੩', '੪', '੫', '੬', '੭', '੮', '੯'), // 0A66 - 0A6F gurmukhi
  49. 'Gujr' => array( '૦', '૧', '૨', '૩', '૪', '૫', '૬', '૭', '૮', '૯'), // 0AE6 - 0AEF gujarati
  50. 'Orya' => array( '୦', '୧', '୨', '୩', '୪', '୫', '୬', '୭', '୮', '୯'), // 0B66 - 0B6F orija
  51. 'Taml' => array( '௦', '௧', '௨', '௩', '௪', '௫', '௬', '௭', '௮', '௯'), // 0BE6 - 0BEF tamil
  52. 'Telu' => array( '౦', '౧', '౨', '౩', '౪', '౫', '౬', '౭', '౮', '౯'), // 0C66 - 0C6F telugu
  53. 'Knda' => array( '೦', '೧', '೨', '೩', '೪', '೫', '೬', '೭', '೮', '೯'), // 0CE6 - 0CEF kannada
  54. 'Mlym' => array( '൦', '൧', '൨', '൩', '൪', '൫', '൬', '൭', '൮', '൯ '), // 0D66 - 0D6F malayalam
  55. 'Tale' => array( '๐', '๑', '๒', '๓', '๔', '๕', '๖', '๗', '๘', '๙ '), // 0E50 - 0E59 thai
  56. 'Laoo' => array( '໐', '໑', '໒', '໓', '໔', '໕', '໖', '໗', '໘', '໙'), // 0ED0 - 0ED9 lao
  57. 'Tibt' => array( '༠', '༡', '༢', '༣', '༤', '༥', '༦', '༧', '༨', '༩ '), // 0F20 - 0F29 tibetan
  58. 'Mymr' => array( '၀', '၁', '၂', '၃', '၄', '၅', '၆', '၇', '၈', '၉'), // 1040 - 1049 myanmar
  59. 'Khmr' => array( '០', '១', '២', '៣', '៤', '៥', '៦', '៧', '៨', '៩'), // 17E0 - 17E9 khmer
  60. 'Mong' => array( '᠐', '᠑', '᠒', '᠓', '᠔', '᠕', '᠖', '᠗', '᠘', '᠙'), // 1810 - 1819 mongolian
  61. 'Limb' => array( '᥆', '᥇', '᥈', '᥉', '᥊', '᥋', '᥌', '᥍', '᥎', '᥏'), // 1946 - 194F limbu
  62. 'Talu' => array( '᧐', '᧑', '᧒', '᧓', '᧔', '᧕', '᧖', '᧗', '᧘', '᧙'), // 19D0 - 19D9 tailue
  63. 'Bali' => array( '᭐', '᭑', '᭒', '᭓', '᭔', '᭕', '᭖', '᭗', '᭘', '᭙'), // 1B50 - 1B59 balinese
  64. 'Nkoo' => array( '߀', '߁', '߂', '߃', '߄', '߅', '߆', '߇', '߈', '߉') // 07C0 - 07C9 nko
  65. );
  66. /**
  67. * Sets class wide options, if no option was given, the actual set options will be returned
  68. * The 'precision' option of a value is used to truncate or stretch extra digits. -1 means not to touch the extra digits.
  69. * The 'locale' option helps when parsing numbers and dates using separators and month names.
  70. * The date format 'format_type' option selects between CLDR/ISO date format specifier tokens and PHP's date() tokens.
  71. * The 'fix_date' option enables or disables heuristics that attempt to correct invalid dates.
  72. * The 'number_format' option can be used to specify a default number format string
  73. * The 'date_format' option can be used to specify a default date format string, but beware of using getDate(),
  74. * checkDateFormat() and getTime() after using setOptions() with a 'format'. To use these four methods
  75. * with the default date format for a locale, use array('date_format' => null, 'locale' => $locale) for their options.
  76. *
  77. * @param array $options Array of options, keyed by option name: format_type = 'iso' | 'php', fix_date = true | false,
  78. * locale = Zend_Locale | locale string, precision = whole number between -1 and 30
  79. * @throws Zend_Locale_Exception
  80. * @return Options array if no option was given
  81. */
  82. public static function setOptions(array $options = array())
  83. {
  84. self::$_options = self::_checkOptions($options) + self::$_options;
  85. return self::$_options;
  86. }
  87. /**
  88. * Internal function for checking the options array of proper input values
  89. * See {@link setOptions()} for details.
  90. *
  91. * @param array $options Array of options, keyed by option name: format_type = 'iso' | 'php', fix_date = true | false,
  92. * locale = Zend_Locale | locale string, precision = whole number between -1 and 30
  93. * @throws Zend_Locale_Exception
  94. * @return Options array if no option was given
  95. */
  96. private static function _checkOptions(array $options = array())
  97. {
  98. if (count($options) == 0) {
  99. return self::$_options;
  100. }
  101. foreach ($options as $name => $value) {
  102. $name = strtolower($name);
  103. if ($name !== 'locale') {
  104. if (gettype($value) === 'string') {
  105. $value = strtolower($value);
  106. }
  107. }
  108. switch($name) {
  109. case 'number_format' :
  110. if ($value == Zend_Locale_Format::STANDARD) {
  111. $locale = self::$_options['locale'];
  112. if (isset($options['locale'])) {
  113. $locale = $options['locale'];
  114. }
  115. $options['number_format'] = Zend_Locale_Data::getContent($locale, 'decimalnumber');
  116. } else if ((gettype($value) !== 'string') and ($value !== NULL)) {
  117. require_once 'Zend/Locale/Exception.php';
  118. throw new Zend_Locale_Exception("Unknown number format type '" . gettype($value) . "'. "
  119. . "Format '$value' must be a valid number format string.");
  120. }
  121. break;
  122. case 'date_format' :
  123. if ($value == Zend_Locale_Format::STANDARD) {
  124. $locale = self::$_options['locale'];
  125. if (isset($options['locale'])) {
  126. $locale = $options['locale'];
  127. }
  128. $options['date_format'] = Zend_Locale_Format::getDateFormat($locale);
  129. } else if ((gettype($value) !== 'string') and ($value !== NULL)) {
  130. require_once 'Zend/Locale/Exception.php';
  131. throw new Zend_Locale_Exception("Unknown dateformat type '" . gettype($value) . "'. "
  132. . "Format '$value' must be a valid ISO or PHP date format string.");
  133. } else {
  134. if (((isset($options['format_type']) === true) and ($options['format_type'] == 'php')) or
  135. ((isset($options['format_type']) === false) and (self::$_options['format_type'] == 'php'))) {
  136. $options['date_format'] = Zend_Locale_Format::convertPhpToIsoFormat($value);
  137. }
  138. }
  139. break;
  140. case 'format_type' :
  141. if (($value != 'php') && ($value != 'iso')) {
  142. require_once 'Zend/Locale/Exception.php';
  143. throw new Zend_Locale_Exception("Unknown date format type '$value'. Only 'iso' and 'php'"
  144. . " are supported.");
  145. }
  146. break;
  147. case 'fix_date' :
  148. if (($value !== true) && ($value !== false)) {
  149. require_once 'Zend/Locale/Exception.php';
  150. throw new Zend_Locale_Exception("Enabling correction of dates must be either true or false"
  151. . "(fix_date='$value').");
  152. }
  153. break;
  154. case 'locale' :
  155. $options['locale'] = Zend_Locale::findLocale($value);
  156. break;
  157. case 'cache' :
  158. if ($value instanceof Zend_Cache_Core) {
  159. Zend_Locale_Data::setCache($value);
  160. }
  161. break;
  162. case 'precision' :
  163. if ($value === NULL) {
  164. $value = -1;
  165. }
  166. if (($value < -1) || ($value > 30)) {
  167. require_once 'Zend/Locale/Exception.php';
  168. throw new Zend_Locale_Exception("'$value' precision is not a whole number less than 30.");
  169. }
  170. break;
  171. default:
  172. require_once 'Zend/Locale/Exception.php';
  173. throw new Zend_Locale_Exception("Unknown option: '$name' = '$value'");
  174. break;
  175. }
  176. }
  177. return $options;
  178. }
  179. /**
  180. * Changes the numbers/digits within a given string from one script to another
  181. * 'Decimal' representated the stardard numbers 0-9, if a script does not exist
  182. * an exception will be thrown.
  183. *
  184. * Examples for conversion from Arabic to Latin numerals:
  185. * convertNumerals('١١٠ Tests', 'Arab'); -> returns '100 Tests'
  186. * Example for conversion from Latin to Arabic numerals:
  187. * convertNumerals('100 Tests', 'Latn', 'Arab'); -> returns '١١٠ Tests'
  188. *
  189. * @param string $input String to convert
  190. * @param string $from Script to parse, see {@link Zend_Locale::getScriptList()} for details.
  191. * @param string $to OPTIONAL Script to convert to
  192. * @return string Returns the converted input
  193. * @throws Zend_Locale_Exception
  194. */
  195. public static function convertNumerals($input, $from, $to = null)
  196. {
  197. if (is_string($from)) {
  198. $from = ucfirst(strtolower($from));
  199. }
  200. if (isset(self::$_signs[$from]) === false) {
  201. require_once 'Zend/Locale/Exception.php';
  202. throw new Zend_Locale_Exception("Unknown script '$from'. Use 'Latn' for digits 0,1,2,3,4,5,6,7,8,9.");
  203. }
  204. if (is_string($to)) {
  205. $to = ucfirst(strtolower($to));
  206. }
  207. if (($to !== null) and (isset(self::$_signs[$to]) === false)) {
  208. require_once 'Zend/Locale/Exception.php';
  209. throw new Zend_Locale_Exception("Unknown script '$to'. Use 'Latn' for digits 0,1,2,3,4,5,6,7,8,9.");
  210. }
  211. if (isset(self::$_signs[$from])) {
  212. for ($X = 0; $X < 10; ++$X) {
  213. $source[$X + 10] = "/" . self::$_signs[$from][$X] . "/u";
  214. }
  215. }
  216. if (isset(self::$_signs[$to])) {
  217. for ($X = 0; $X < 10; ++$X) {
  218. $dest[$X + 10] = self::$_signs[$to][$X];
  219. }
  220. } else {
  221. for ($X = 0; $X < 10; ++$X) {
  222. $dest[$X + 10] = $X;
  223. }
  224. }
  225. return preg_replace($source, $dest, $input);
  226. }
  227. /**
  228. * Returns the first found number from an string
  229. * Parsing depends on given locale (grouping and decimal)
  230. *
  231. * Examples for input:
  232. * ' 2345.4356,1234' = 23455456.1234
  233. * '+23,3452.123' = 233452.123
  234. * ' 12343 ' = 12343
  235. * '-9456km' = -9456
  236. * '0' = 0
  237. * '(-){0,1}(\d+(\.){0,1})*(\,){0,1})\d+'
  238. * '١١٠ Tests' = 110 call: getNumber($string, 'Arab');
  239. *
  240. * @param string $input Input string to parse for numbers
  241. * @param array $options Options: locale, precision. See {@link setOptions()} for details.
  242. * @return string Returns the extracted number
  243. * @throws Zend_Locale_Exception
  244. */
  245. public static function getNumber($input, array $options = array())
  246. {
  247. $options = self::_checkOptions($options) + self::$_options;
  248. if (!is_string($input)) {
  249. return $input;
  250. }
  251. // Get correct signs for this locale
  252. $symbols = Zend_Locale_Data::getList($options['locale'],'symbols');
  253. // Parse input locale aware
  254. $regex = '/([' . $symbols['minus'] . '-]){0,1}(\d+(\\' . $symbols['group'] . '){0,1})*(\\' .
  255. $symbols['decimal'] . '){0,1}\d+/';
  256. preg_match($regex, $input, $found);
  257. if (!isset($found[0])) {
  258. require_once 'Zend/Locale/Exception.php';
  259. throw new Zend_Locale_Exception('No value in ' . $input . ' found');
  260. }
  261. $found = $found[0];
  262. // Change locale input to be default number
  263. if ($symbols['minus'] != "-")
  264. $found = strtr($found,$symbols['minus'],'-');
  265. $found = str_replace($symbols['group'],'', $found);
  266. // Do precision
  267. if (strpos($found, $symbols['decimal']) !== false) {
  268. if ($symbols['decimal'] != '.') {
  269. $found = str_replace($symbols['decimal'], ".", $found);
  270. }
  271. $pre = substr($found, strpos($found, '.') + 1);
  272. if ($options['precision'] === null) {
  273. $options['precision'] = strlen($pre);
  274. }
  275. if (strlen($pre) >= $options['precision']) {
  276. $found = substr($found, 0, strlen($found) - strlen($pre) + $options['precision']);
  277. }
  278. }
  279. return $found;
  280. }
  281. /**
  282. * Returns a locale formatted number depending on the given options.
  283. * The seperation and fraction sign is used from the set locale.
  284. * ##0.# -> 12345.12345 -> 12345.12345
  285. * ##0.00 -> 12345.12345 -> 12345.12
  286. * ##,##0.00 -> 12345.12345 -> 12,345.12
  287. *
  288. * @param string $input Localized number string
  289. * @param array $options Options: number_format, locale, precision. See {@link setOptions()} for details.
  290. * @return string locale formatted number
  291. * @throws Zend_Locale_Exception
  292. */
  293. public static function toNumber($value, array $options = array())
  294. {
  295. // load class within method for speed
  296. require_once 'Zend/Locale/Math.php';
  297. $value = Zend_Locale_Math::normalize($value);
  298. $options = self::_checkOptions($options) + self::$_options;
  299. $options['locale'] = (string) $options['locale'];
  300. // Get correct signs for this locale
  301. $symbols = Zend_Locale_Data::getList($options['locale'], 'symbols');
  302. iconv_set_encoding('internal_encoding', 'UTF-8');
  303. // Get format
  304. $format = $options['number_format'];
  305. if ($format === null) {
  306. $format = Zend_Locale_Data::getContent($options['locale'], 'decimalnumber');
  307. if (iconv_strpos($format, ';') !== false) {
  308. if (call_user_func(Zend_Locale_Math::$comp, $value, 0, $options['precision']) < 0) {
  309. $format = iconv_substr($format, iconv_strpos($format, ';') + 1);
  310. } else {
  311. $format = iconv_substr($format, 0, iconv_strpos($format, ';'));
  312. }
  313. }
  314. } else {
  315. // seperate negative format pattern when available
  316. if (iconv_strpos($format, ';') !== false) {
  317. if (call_user_func(Zend_Locale_Math::$comp, $value, 0, $options['precision']) < 0) {
  318. $format = iconv_substr($format, iconv_strpos($format, ';') + 1);
  319. } else {
  320. $format = iconv_substr($format, 0, iconv_strpos($format, ';'));
  321. }
  322. }
  323. if (strpos($format, '.')) {
  324. if (is_numeric($options['precision'])) {
  325. $value = Zend_Locale_Math::round($value, $options['precision']);
  326. } else {
  327. if (substr($format, strpos($format, '.') + 1, 3) == '###') {
  328. $options['precision'] = null;
  329. } else {
  330. $options['precision'] = strlen(substr($format, strpos($format, '.') + 1,
  331. strrpos($format, '0') - strpos($format, '.')));
  332. $format = substr($format, 0, strpos($format, '.') + 1) . '###'
  333. . substr($format, strrpos($format, '0') + 1);
  334. }
  335. }
  336. } else {
  337. $value = Zend_Locale_Math::round($value, 0);
  338. $options['precision'] = 0;
  339. }
  340. $value = Zend_Locale_Math::normalize($value);
  341. }
  342. if (strpos($format, '0') === false) {
  343. require_once 'Zend/Locale/Exception.php';
  344. throw new Zend_Locale_Exception('Wrong format... missing 0');
  345. }
  346. // get number parts
  347. $pos = iconv_strpos($value, '.');
  348. if ($pos !== false) {
  349. if ($options['precision'] === null) {
  350. $precstr = iconv_substr($value, $pos + 1);
  351. } else {
  352. $precstr = iconv_substr($value, $pos + 1, $options['precision']);
  353. if (iconv_strlen($precstr) < $options['precision']) {
  354. $precstr = $precstr . str_pad("0", ($options['precision'] - iconv_strlen($precstr)), "0");
  355. }
  356. }
  357. } else {
  358. if ($options['precision'] > 0) {
  359. $precstr = str_pad("0", ($options['precision']), "0");
  360. }
  361. }
  362. if ($options['precision'] === null) {
  363. if (isset($precstr)) {
  364. $options['precision'] = iconv_strlen($precstr);
  365. } else {
  366. $options['precision'] = 0;
  367. }
  368. }
  369. // get fraction and format lengths
  370. if (strpos($value, '.') !== false) {
  371. $number = substr((string) $value, 0, strpos($value, '.'));
  372. } else {
  373. $number = $value;
  374. }
  375. $prec = call_user_func(Zend_Locale_Math::$sub, $value, $number, $options['precision']);
  376. $prec = Zend_Locale_Math::normalize($prec);
  377. if (iconv_strpos($prec, '-') !== false) {
  378. $prec = iconv_substr($prec, 1);
  379. }
  380. if (($prec == 0) and ($options['precision'] > 0)) {
  381. $prec = "0.0";
  382. }
  383. if (($options['precision'] + 2) > iconv_strlen($prec)) {
  384. $prec = str_pad((string) $prec, $options['precision'] + 2, "0", STR_PAD_RIGHT);
  385. }
  386. if (iconv_strpos($number, '-') !== false) {
  387. $number = iconv_substr($number, 1);
  388. }
  389. $group = iconv_strrpos($format, ',');
  390. $group2 = iconv_strpos ($format, ',');
  391. $point = iconv_strpos ($format, '0');
  392. // Add fraction
  393. $rest = "";
  394. if (($value < 0) && (strpos($format, '.'))) {
  395. $rest = substr(substr($format, strpos($format, '.') + 1), -1, 1);
  396. }
  397. if ($options['precision'] == '0') {
  398. $format = iconv_substr($format, 0, $point) . iconv_substr($format, iconv_strrpos($format, '#') + 2);
  399. } else {
  400. $format = iconv_substr($format, 0, $point) . $symbols['decimal']
  401. . iconv_substr($prec, 2)
  402. . iconv_substr($format, iconv_strrpos($format, '#') + 1 + strlen($prec));
  403. }
  404. if (($value < 0) and ($rest != '0') and ($rest != '#')) {
  405. $format .= $rest;
  406. }
  407. // Add seperation
  408. if ($group == 0) {
  409. // no seperation
  410. $format = $number . iconv_substr($format, $point);
  411. } else if ($group == $group2) {
  412. // only 1 seperation
  413. $seperation = ($point - $group);
  414. for ($x = iconv_strlen($number); $x > $seperation; $x -= $seperation) {
  415. if (iconv_substr($number, 0, $x - $seperation) !== "") {
  416. $number = iconv_substr($number, 0, $x - $seperation) . $symbols['group']
  417. . iconv_substr($number, $x - $seperation);
  418. }
  419. }
  420. $format = iconv_substr($format, 0, iconv_strpos($format, '#')) . $number . iconv_substr($format, $point);
  421. } else {
  422. // 2 seperations
  423. if (iconv_strlen($number) > ($point - $group)) {
  424. $seperation = ($point - $group);
  425. $number = iconv_substr($number, 0, iconv_strlen($number) - $seperation) . $symbols['group']
  426. . iconv_substr($number, iconv_strlen($number) - $seperation);
  427. if ((iconv_strlen($number) - 1) > ($point - $group + 1)) {
  428. $seperation2 = ($group - $group2 - 1);
  429. for ($x = iconv_strlen($number) - $seperation2 - 2; $x > $seperation2; $x -= $seperation2) {
  430. $number = iconv_substr($number, 0, $x - $seperation2) . $symbols['group']
  431. . iconv_substr($number, $x - $seperation2);
  432. }
  433. }
  434. }
  435. $format = iconv_substr($format, 0, iconv_strpos($format, '#')) . $number . iconv_substr($format, $point);
  436. }
  437. // set negative sign
  438. if (call_user_func(Zend_Locale_Math::$comp, $value, 0, $options['precision']) < 0) {
  439. if (iconv_strpos($format, '-') === false) {
  440. $format = $symbols['minus'] . $format;
  441. } else {
  442. $format = str_replace('-', $symbols['minus'], $format);
  443. }
  444. }
  445. return (string) $format;
  446. }
  447. /**
  448. * Checks if the input contains a normalized or localized number
  449. *
  450. * @param string $input Localized number string
  451. * @param array $options Options: locale. See {@link setOptions()} for details.
  452. * @return boolean Returns true if a number was found
  453. */
  454. public static function isNumber($input, array $options = array())
  455. {
  456. $options = self::_checkOptions($options) + self::$_options;
  457. // Get correct signs for this locale
  458. $symbols = Zend_Locale_Data::getList($options['locale'],'symbols');
  459. // Parse input locale aware
  460. $regex = '/^[' . $symbols['minus'] . $symbols['plus'] . '-+]?'
  461. . '\d(\d*(\\' . $symbols['group'] . ')?\d+)*'
  462. . '((\\' . $symbols['decimal'] . ')\d*)?([' . $symbols['exponent'] . 'eE]'
  463. . '([' . $symbols['minus'] . $symbols['plus'] . '+-])?\d+)?$/';
  464. preg_match($regex, $input, $found);
  465. if (!isset($found[0]))
  466. return false;
  467. return true;
  468. }
  469. /**
  470. * Alias for getNumber
  471. *
  472. * @param string $value Number to localize
  473. * @param array $options Options: locale, precision. See {@link setOptions()} for details.
  474. * @return float
  475. */
  476. public static function getFloat($input, array $options = array())
  477. {
  478. return floatval(self::getNumber($input, $options));
  479. }
  480. /**
  481. * Returns a locale formatted integer number
  482. * Alias for toNumber()
  483. *
  484. * @param string $value Number to normalize
  485. * @param array $options Options: locale, precision. See {@link setOptions()} for details.
  486. * @return string Locale formatted number
  487. */
  488. public static function toFloat($value, array $options = array())
  489. {
  490. $options['number_format'] = Zend_Locale_Format::STANDARD;
  491. return self::toNumber($value, $options);
  492. }
  493. /**
  494. * Returns if a float was found
  495. * Alias for isNumber()
  496. *
  497. * @param string $input Localized number string
  498. * @param array $options Options: locale. See {@link setOptions()} for details.
  499. * @return boolean Returns true if a number was found
  500. */
  501. public static function isFloat($value, array $options = array())
  502. {
  503. return self::isNumber($value, $options);
  504. }
  505. /**
  506. * Returns the first found integer from an string
  507. * Parsing depends on given locale (grouping and decimal)
  508. *
  509. * Examples for input:
  510. * ' 2345.4356,1234' = 23455456
  511. * '+23,3452.123' = 233452
  512. * ' 12343 ' = 12343
  513. * '-9456km' = -9456
  514. * '0' = 0
  515. * '(-){0,1}(\d+(\.){0,1})*(\,){0,1})\d+'
  516. *
  517. * @param string $input Input string to parse for numbers
  518. * @param array $options Options: locale. See {@link setOptions()} for details.
  519. * @return integer Returns the extracted number
  520. */
  521. public static function getInteger($input, array $options = array())
  522. {
  523. $options['precision'] = 0;
  524. return intval(self::getFloat($input, $options));
  525. }
  526. /**
  527. * Returns a localized number
  528. *
  529. * @param string $value Number to normalize
  530. * @param array $options Options: locale. See {@link setOptions()} for details.
  531. * @return string Locale formatted number
  532. */
  533. public static function toInteger($value, array $options = array())
  534. {
  535. $options['precision'] = 0;
  536. $options['number_format'] = Zend_Locale_Format::STANDARD;
  537. return self::toNumber($value, $options);
  538. }
  539. /**
  540. * Returns if a integer was found
  541. *
  542. * @param string $input Localized number string
  543. * @param array $options Options: locale. See {@link setOptions()} for details.
  544. * @return boolean Returns true if a integer was found
  545. */
  546. public static function isInteger($value, array $options = array())
  547. {
  548. if (!self::isNumber($value, $options)) {
  549. return false;
  550. }
  551. if (self::getInteger($value, $options) == self::getFloat($value, $options)) {
  552. return true;
  553. }
  554. return false;
  555. }
  556. /**
  557. * Converts a format string from PHP's date format to ISO format
  558. * Remember that Zend Date always returns localized string, so a month name which returns the english
  559. * month in php's date() will return the translated month name with this function... use 'en' as locale
  560. * if you are in need of the original english names
  561. *
  562. * The conversion has the following restrictions:
  563. * 'a', 'A' - Meridiem is not explicit upper/lowercase, you have to upper/lowercase the translated value yourself
  564. *
  565. * @param string $format Format string in PHP's date format
  566. * @return string Format string in ISO format
  567. */
  568. public static function convertPhpToIsoFormat($format)
  569. {
  570. $convert = array('d' => 'dd' , 'D' => 'EE' , 'j' => 'd' , 'l' => 'EEEE', 'N' => 'e' , 'S' => 'SS' ,
  571. 'w' => 'eee' , 'z' => 'D' , 'W' => 'ww' , 'F' => 'MMMM', 'm' => 'MM' , 'M' => 'MMM' ,
  572. 'n' => 'M' , 't' => 'ddd' , 'L' => 'l' , 'o' => 'YYYY', 'Y' => 'yyyy', 'y' => 'yy' ,
  573. 'a' => 'a' , 'A' => 'a' , 'B' => 'B' , 'g' => 'h' , 'G' => 'H' , 'h' => 'hh' ,
  574. 'H' => 'HH' , 'i' => 'mm' , 's' => 'ss' , 'e' => 'zzzz', 'I' => 'I' , 'O' => 'Z' ,
  575. 'P' => 'ZZZZ', 'T' => 'z' , 'Z' => 'X' , 'c' => 'yyyy-MM-ddTHH:mm:ssZZZZ',
  576. 'r' => 'r' , 'U' => 'U');
  577. $values = str_split($format);
  578. foreach ($values as $key => $value) {
  579. if (isset($convert[$value]) === true) {
  580. $values[$key] = $convert[$value];
  581. }
  582. }
  583. return join($values);
  584. }
  585. /**
  586. * Parse date and split in named array fields
  587. *
  588. * @param string $date Date string to parse
  589. * @param array $options Options: format_type, fix_date, locale, date_format. See {@link setOptions()} for details.
  590. * @return array Possible array members: day, month, year, hour, minute, second, fixed, format
  591. */
  592. private static function _parseDate($date, $options)
  593. {
  594. $options = self::_checkOptions($options) + self::$_options;
  595. $test = array('h', 'H', 'm', 's', 'y', 'Y', 'M', 'd', 'D', 'E', 'S', 'l', 'B', 'I',
  596. 'X', 'r', 'U', 'G', 'w', 'e', 'a', 'A', 'Z', 'z', 'v');
  597. $format = $options['date_format'];
  598. $number = $date; // working copy
  599. $result['date_format'] = $format; // save the format used to normalize $number (convenience)
  600. $result['locale'] = $options['locale']; // save the locale used to normalize $number (convenience)
  601. $day = iconv_strpos($format, 'd');
  602. $month = iconv_strpos($format, 'M');
  603. $year = iconv_strpos($format, 'y');
  604. $hour = iconv_strpos($format, 'H');
  605. $min = iconv_strpos($format, 'm');
  606. $sec = iconv_strpos($format, 's');
  607. $am = null;
  608. if ($hour === false) {
  609. $hour = iconv_strpos($format, 'h');
  610. }
  611. if ($year === false) {
  612. $year = iconv_strpos($format, 'Y');
  613. }
  614. if ($day === false) {
  615. $day = iconv_strpos($format, 'E');
  616. if ($day === false) {
  617. $day = iconv_strpos($format, 'D');
  618. }
  619. }
  620. if ($day !== false) {
  621. $parse[$day] = 'd';
  622. if (!empty($options['locale']) && ($options['locale'] !== 'root') &&
  623. (!is_object($options['locale']) || ((string) $options['locale'] !== 'root'))) {
  624. // erase day string
  625. $daylist = Zend_Locale_Data::getList($options['locale'], 'day');
  626. foreach($daylist as $key => $name) {
  627. if (iconv_strpos($number, $name) !== false) {
  628. $number = str_replace($name, "EEEE", $number);
  629. break;
  630. }
  631. }
  632. }
  633. }
  634. $position = false;
  635. if ($month !== false) {
  636. $parse[$month] = 'M';
  637. if (!empty($options['locale']) && ($options['locale'] !== 'root') &&
  638. (!is_object($options['locale']) || ((string) $options['locale'] !== 'root'))) {
  639. // prepare to convert month name to their numeric equivalents, if requested,
  640. // and we have a $options['locale']
  641. $position = self::_replaceMonth($number, Zend_Locale_Data::getList($options['locale'],
  642. 'month'));
  643. if ($position === false) {
  644. $position = self::_replaceMonth($number, Zend_Locale_Data::getList($options['locale'],
  645. 'month', array('gregorian', 'format', 'abbreviated')));
  646. }
  647. }
  648. }
  649. if ($year !== false) {
  650. $parse[$year] = 'y';
  651. }
  652. if ($hour !== false) {
  653. $parse[$hour] = 'H';
  654. }
  655. if ($min !== false) {
  656. $parse[$min] = 'm';
  657. }
  658. if ($sec !== false) {
  659. $parse[$sec] = 's';
  660. }
  661. if (empty($parse)) {
  662. require_once 'Zend/Locale/Exception.php';
  663. throw new Zend_Locale_Exception("Unknown date format, neither date nor time in '" . $format . "' found");
  664. }
  665. ksort($parse);
  666. // get daytime
  667. if (iconv_strpos($format, 'a') !== false) {
  668. if (iconv_strpos(strtoupper($number), strtoupper(Zend_Locale_Data::getContent($options['locale'], 'am'))) !== false) {
  669. $am = true;
  670. } else if (iconv_strpos(strtoupper($number), strtoupper(Zend_Locale_Data::getContent($options['locale'], 'pm'))) !== false) {
  671. $am = false;
  672. }
  673. }
  674. // split number parts
  675. $split = false;
  676. preg_match_all('/\d+/u', $number, $splitted);
  677. if (count($splitted[0]) == 0) {
  678. require_once 'Zend/Locale/Exception.php';
  679. throw new Zend_Locale_Exception("No date part in '$date' found.");
  680. }
  681. if (count($splitted[0]) == 1) {
  682. $split = 0;
  683. }
  684. $cnt = 0;
  685. foreach($parse as $key => $value) {
  686. switch($value) {
  687. case 'd':
  688. if ($split === false) {
  689. if (count($splitted[0]) > $cnt) {
  690. $result['day'] = $splitted[0][$cnt];
  691. }
  692. } else {
  693. $result['day'] = iconv_substr($splitted[0][0], $split, 2);
  694. $split += 2;
  695. }
  696. ++$cnt;
  697. break;
  698. case 'M':
  699. if ($split === false) {
  700. if (count($splitted[0]) > $cnt) {
  701. $result['month'] = $splitted[0][$cnt];
  702. }
  703. } else {
  704. $result['month'] = iconv_substr($splitted[0][0], $split, 2);
  705. $split += 2;
  706. }
  707. ++$cnt;
  708. break;
  709. case 'y':
  710. $length = 2;
  711. if ((iconv_substr($format, $year, 4) == 'yyyy')
  712. || (iconv_substr($format, $year, 4) == 'YYYY')) {
  713. $length = 4;
  714. }
  715. if ($split === false) {
  716. if (count($splitted[0]) > $cnt) {
  717. $result['year'] = $splitted[0][$cnt];
  718. }
  719. } else {
  720. $result['year'] = iconv_substr($splitted[0][0], $split, $length);
  721. $split += $length;
  722. }
  723. ++$cnt;
  724. break;
  725. case 'H':
  726. if ($split === false) {
  727. if (count($splitted[0]) > $cnt) {
  728. $result['hour'] = $splitted[0][$cnt];
  729. }
  730. } else {
  731. $result['hour'] = iconv_substr($splitted[0][0], $split, 2);
  732. $split += 2;
  733. }
  734. ++$cnt;
  735. break;
  736. case 'm':
  737. if ($split === false) {
  738. if (count($splitted[0]) > $cnt) {
  739. $result['minute'] = $splitted[0][$cnt];
  740. }
  741. } else {
  742. $result['minute'] = iconv_substr($splitted[0][0], $split, 2);
  743. $split += 2;
  744. }
  745. ++$cnt;
  746. break;
  747. case 's':
  748. if ($split === false) {
  749. if (count($splitted[0]) > $cnt) {
  750. $result['second'] = $splitted[0][$cnt];
  751. }
  752. } else {
  753. $result['second'] = iconv_substr($splitted[0][0], $split, 2);
  754. $split += 2;
  755. }
  756. ++$cnt;
  757. break;
  758. }
  759. }
  760. // AM/PM correction
  761. if ($hour !== false) {
  762. if (($am === true) and ($result['hour'] == 12)){
  763. $result['hour'] = 0;
  764. } else if (($am === false) and ($result['hour'] != 12)) {
  765. $result['hour'] += 12;
  766. }
  767. }
  768. if ($options['fix_date'] === true) {
  769. $result['fixed'] = 0; // nothing has been "fixed" by swapping date parts around (yet)
  770. }
  771. if ($day !== false) {
  772. // fix false month
  773. if (isset($result['day']) and isset($result['month'])) {
  774. if (($position !== false) and ((iconv_strpos($date, $result['day']) === false) or
  775. (isset($result['year']) and (iconv_strpos($date, $result['year']) === false)))) {
  776. if ($options['fix_date'] !== true) {
  777. require_once 'Zend/Locale/Exception.php';
  778. throw new Zend_Locale_Exception("Unable to parse date '$date' using '" . $format
  779. . "' (false month, $position, $month)");
  780. }
  781. $temp = $result['day'];
  782. $result['day'] = $result['month'];
  783. $result['month'] = $temp;
  784. $result['fixed'] = 1;
  785. }
  786. }
  787. // fix switched values d <> y
  788. if (isset($result['day']) and isset($result['year'])) {
  789. if ($result['day'] > 31) {
  790. if ($options['fix_date'] !== true) {
  791. require_once 'Zend/Locale/Exception.php';
  792. throw new Zend_Locale_Exception("Unable to parse date '$date' using '"
  793. . $format . "' (d <> y)");
  794. }
  795. $temp = $result['year'];
  796. $result['year'] = $result['day'];
  797. $result['day'] = $temp;
  798. $result['fixed'] = 2;
  799. }
  800. }
  801. // fix switched values M <> y
  802. if (isset($result['month']) and isset($result['year'])) {
  803. if ($result['month'] > 31) {
  804. if ($options['fix_date'] !== true) {
  805. require_once 'Zend/Locale/Exception.php';
  806. throw new Zend_Locale_Exception("Unable to parse date '$date' using '"
  807. . $format . "' (M <> y)");
  808. }
  809. $temp = $result['year'];
  810. $result['year'] = $result['month'];
  811. $result['month'] = $temp;
  812. $result['fixed'] = 3;
  813. }
  814. }
  815. // fix switched values M <> d
  816. if (isset($result['month']) and isset($result['day'])) {
  817. if ($result['month'] > 12) {
  818. if ($options['fix_date'] !== true || $result['month'] > 31) {
  819. require_once 'Zend/Locale/Exception.php';
  820. throw new Zend_Locale_Exception("Unable to parse date '$date' using '"
  821. . $format . "' (M <> d)");
  822. }
  823. $temp = $result['day'];
  824. $result['day'] = $result['month'];
  825. $result['month'] = $temp;
  826. $result['fixed'] = 4;
  827. }
  828. }
  829. }
  830. if (isset($result['year'])) {
  831. if (((iconv_strlen($result['year']) == 2) && ($result['year'] < 10)) ||
  832. (((iconv_strpos($format, 'yy') !== false) && (iconv_strpos($format, 'yyyy') === false)) ||
  833. ((iconv_strpos($format, 'YY') !== false) && (iconv_strpos($format, 'YYYY') === false)))) {
  834. if (($result['year'] >= 0) && ($result['year'] < 100)) {
  835. if ($result['year'] < 70) {
  836. $result['year'] = (int) $result['year'] + 100;
  837. }
  838. $result['year'] = (int) $result['year'] + 1900;
  839. }
  840. }
  841. }
  842. return $result;
  843. }
  844. /**
  845. * Search $number for a month name found in $monthlist, and replace if found.
  846. *
  847. * @param string $number Date string (modified)
  848. * @param array $monthlist List of month names
  849. *
  850. * @return int|false Position of replaced string (false if nothing replaced)
  851. */
  852. protected static function _replaceMonth(&$number, $monthlist)
  853. {
  854. // If $locale was invalid, $monthlist will default to a "root" identity
  855. // mapping for each month number from 1 to 12.
  856. // If no $locale was given, or $locale was invalid, do not use this identity mapping to normalize.
  857. // Otherwise, translate locale aware month names in $number to their numeric equivalents.
  858. $position = false;
  859. if ($monthlist && $monthlist[1] != 1) {
  860. foreach($monthlist as $key => $name) {
  861. if (($position = iconv_strpos($number, $name)) !== false) {
  862. $number = str_ireplace($name, $key, $number);
  863. return $position;
  864. }
  865. }
  866. }
  867. return false;
  868. }
  869. /**
  870. * Returns the default date format for $locale.
  871. *
  872. * @param string|Zend_Locale $locale OPTIONAL Locale of $number, possibly in string form (e.g. 'de_AT')
  873. * @return string format
  874. * @throws Zend_Locale_Exception throws an exception when locale data is broken
  875. */
  876. public static function getDateFormat($locale = null)
  877. {
  878. $format = Zend_Locale_Data::getContent($locale, 'date');
  879. if (empty($format)) {
  880. require_once 'Zend/Locale/Exception.php';
  881. throw new Zend_Locale_Exception("failed to receive data from locale $locale");
  882. }
  883. return $format;
  884. }
  885. /**
  886. * Returns an array with the normalized date from an locale date
  887. * a input of 10.01.2006 without a $locale would return:
  888. * array ('day' => 10, 'month' => 1, 'year' => 2006)
  889. * The 'locale' option is only used to convert human readable day
  890. * and month names to their numeric equivalents.
  891. * The 'format' option allows specification of self-defined date formats,
  892. * when not using the default format for the 'locale'.
  893. *
  894. * @param string $date Date string
  895. * @param array $options Options: format_type, fix_date, locale, date_format. See {@link setOptions()} for details.
  896. * @return array Possible array members: day, month, year, hour, minute, second, fixed, format
  897. */
  898. public static function getDate($date, array $options = array())
  899. {
  900. $options = self::_checkOptions($options) + self::$_options;
  901. if (empty($options['date_format'])) {
  902. $options['format_type'] = 'iso';
  903. $options['date_format'] = self::getDateFormat($options['locale']);
  904. }
  905. return self::_parseDate($date, $options);
  906. }
  907. /**
  908. * Returns if the given datestring contains all date parts from the given format.
  909. * If no format is given, the default date format from the locale is used
  910. * If you want to check if the date is a proper date you should use Zend_Date::isDate()
  911. *
  912. * @param string $date Date string
  913. * @param array $options Options: format_type, fix_date, locale, date_format. See {@link setOptions()} for details.
  914. * @return boolean
  915. */
  916. public static function checkDateFormat($date, array $options = array())
  917. {
  918. try {
  919. $date = self::getDate($date, $options);
  920. } catch (Exception $e) {
  921. return false;
  922. }
  923. if (empty($options['date_format'])) {
  924. $options['format_type'] = 'iso';
  925. $options['date_format'] = self::getDateFormat($options['locale']);
  926. }
  927. $options = self::_checkOptions($options) + self::$_options;
  928. // day expected but not parsed
  929. if ((iconv_strpos($options['date_format'], 'd') !== false) and (!isset($date['day']) or ($date['day'] == ""))) {
  930. return false;
  931. }
  932. // month expected but not parsed
  933. if ((iconv_strpos($options['date_format'], 'M') !== false) and (!isset($date['month']) or ($date['month'] == ""))) {
  934. return false;
  935. }
  936. // year expected but not parsed
  937. if (((iconv_strpos($options['date_format'], 'Y') !== false) or
  938. (iconv_strpos($options['date_format'], 'y') !== false)) and (!isset($date['year']) or ($date['year'] == ""))) {
  939. return false;
  940. }
  941. // second expected but not parsed
  942. if ((iconv_strpos($options['date_format'], 's') !== false) and (!isset($date['second']) or ($date['second'] == ""))) {
  943. return false;
  944. }
  945. // minute expected but not parsed
  946. if ((iconv_strpos($options['date_format'], 'm') !== false) and (!isset($date['minute']) or ($date['minute'] == ""))) {
  947. return false;
  948. }
  949. // hour expected but not parsed
  950. if (((iconv_strpos($options['date_format'], 'H') !== false) or
  951. (iconv_strpos($options['date_format'], 'h') !== false)) and (!isset($date['hour']) or ($date['hour'] == ""))) {
  952. return false;
  953. }
  954. return true;
  955. }
  956. /**
  957. * Returns the default time format for $locale.
  958. *
  959. * @param string|Zend_Locale $locale OPTIONAL Locale of $number, possibly in string form (e.g. 'de_AT')
  960. * @return string format
  961. */
  962. public static function getTimeFormat($locale = null)
  963. {
  964. $format = Zend_Locale_Data::getContent($locale, 'time');
  965. if (empty($format)) {
  966. require_once 'Zend/Locale/Exception.php';
  967. throw new Zend_Locale_Exception("failed to receive data from locale $locale");
  968. }
  969. return $format;
  970. }
  971. /**
  972. * Returns an array with 'hour', 'minute', and 'second' elements extracted from $time
  973. * according to the order described in $format. For a format of 'H:m:s', and
  974. * an input of 11:20:55, getTime() would return:
  975. * array ('hour' => 11, 'minute' => 20, 'second' => 55)
  976. * The optional $locale parameter may be used to help extract times from strings
  977. * containing both a time and a day or month name.
  978. *
  979. * @param string $time Time string
  980. * @param array $options Options: format_type, fix_date, locale, date_format. See {@link setOptions()} for details.
  981. * @return array Possible array members: day, month, year, hour, minute, second, fixed, format
  982. */
  983. public static function getTime($time, array $options = array())
  984. {
  985. $options = self::_checkOptions($options) + self::$_options;
  986. if (empty($options['date_format'])) {
  987. $options['format_type'] = 'iso';
  988. $options['date_format'] = self::getTimeFormat($options['locale']);
  989. }
  990. return self::_parseDate($time, $options);
  991. }
  992. }