* @author Chris Smith * @param string * @param integer number of UTF-8 characters offset (from left) * @param integer (optional) length in UTF-8 characters from offset * @return mixed string or false if failure */ function utf8_substr($str, $offset, $length = null) { /* * Notes: * * no mb string support, so we'll use pcre regex's with 'u' flag * pcre only supports repetitions of less than 65536, in order to accept up to MAXINT values for * offset and length, we'll repeat a group of 65535 characters when needed (ok, up to MAXINT-65536) * * substr documentation states false can be returned in some cases (e.g. offset > string length) * mb_substr never returns false, it will return an empty string instead. * * calculating the number of characters in the string is a relatively expensive operation, so * we only carry it out when necessary. It isn't necessary for +ve offsets and no specified length */ // cast parameters to appropriate types to avoid multiple notices/warnings $str = (string)$str; // generates E_NOTICE for PHP4 objects, but not PHP5 objects $offset = (int)$offset; if (!is_null($length)) $length = (int)$length; // handle trivial cases if ($length === 0) return ''; if ($offset < 0 && $length < 0 && $length < $offset) return ''; $offset_pattern = ''; $length_pattern = ''; // normalise -ve offsets (we could use a tail anchored pattern, but they are horribly slow!) if ($offset < 0) { $strlen = strlen(utf8_decode($str)); // see notes $offset = $strlen + $offset; if ($offset < 0) $offset = 0; } // establish a pattern for offset, a non-captured group equal in length to offset if ($offset > 0) { $Ox = (int)($offset/65535); $Oy = $offset%65535; if ($Ox) $offset_pattern = '(?:.{65535}){'.$Ox.'}'; $offset_pattern = '^(?:'.$offset_pattern.'.{'.$Oy.'})'; } else { $offset_pattern = '^'; // offset == 0; just anchor the pattern } // establish a pattern for length if (is_null($length)) { $length_pattern = '(.*)$'; // the rest of the string } else { if (!isset($strlen)) $strlen = strlen(utf8_decode($str)); // see notes if ($offset > $strlen) return ''; // another trivial case if ($length > 0) { $length = min($strlen-$offset, $length); // reduce any length that would go passed the end of the string $Lx = (int)($length/65535); $Ly = $length%65535; // +ve length requires ... a captured group of length characters if ($Lx) $length_pattern = '(?:.{65535}){'.$Lx.'}'; $length_pattern = '('.$length_pattern.'.{'.$Ly.'})'; } else if ($length < 0) { if ($length < ($offset - $strlen)) return ''; $Lx = (int)((-$length)/65535); $Ly = (-$length)%65535; // -ve length requires ... capture everything except a group of -length characters // anchored at the tail-end of the string if ($Lx) $length_pattern = '(?:.{65535}){'.$Lx.'}'; $length_pattern = '(.*)(?:'.$length_pattern.'.{'.$Ly.'})$'; } } if (!preg_match('#'.$offset_pattern.$length_pattern.'#us',$str,$match)) return ''; return $match[1]; } function test_str() { return<< 0) { $prefix = drupal_substr($str, 0, 1); // echo $prefix; $str = drupal_substr($str, 1); } } $time = microtime_float()-$start; echo "drupal_substr chopper: $time\n"; // Commented out because it takes too long /* $start = microtime_float(); for ($i = 0; $i < TRIALS; ++$i) { $str = test_str(); $len = drupal_strlen($str); for ($j = 0; $j < $len; ++$j) { $char = drupal_substr($str, $j, 1); // echo $char; } } $time = microtime_float()-$start; echo "drupal_substr iterate: $time\n"; */ $start = microtime_float(); for ($i = 0; $i < TRIALS; ++$i) { $str = test_str(); $len = drupal_strlen($str); for ($j = 0; $j < $len; ++$j) { $u32 = iconv('UTF8', 'UTF32', $str); $char = iconv('UTF32', 'UTF8', substr($u32, ($j+1)<<2, 4)); // echo $char; } } $time = microtime_float()-$start; echo "\niconv utf32: $time\n"; $start = microtime_float(); for ($i = 0; $i < TRIALS; ++$i) { $str = test_str(); $len = drupal_strlen($str); for ($j = 0; $j < $len; ++$j) { $char = utf8_substr($str, $j, 1); // echo $char; } } $time = microtime_float()-$start; echo "utf8_substr iterate: $time\n"; $start = microtime_float(); for ($i = 0; $i < TRIALS; ++$i) { $str = test_str(); preg_match_all('/.|\n/u', $str, $matches); $chars = $matches[0]; $len = count($chars); for ($j = 0; $j < $len; ++$j) { $char = $chars[$j]; // echo $char; } } $time = microtime_float()-$start; echo "\npreg_match_all: $time\n"; unicode_check(); global $multibyte; if ($multibyte == UNICODE_MULTIBYTE) { $start = microtime_float(); for ($i = 0; $i < TRIALS; ++$i) { $str = test_str(); $len = drupal_strlen($str); for ($j = 0; $j < $len; ++$j) { $char = drupal_substr($str, $j, 1); // echo $char; } } $time = microtime_float()-$start; echo "drupal_substr iterate: $time\n"; }