Как взорвать другой раздел из текстового файла в массив, используя php (и не регулярное выражение)?

Этот вопрос почти дублируется в Как преобразовать структурированные текстовые файлы в многомерный массив PHP, но я опубликовал его снова, так как не смог понять решения на основе регулярных выражений, которые были даны. Кажется, лучше попробовать и решить это, используя только PHP, чтобы я мог на самом деле учиться на нем (регулярное выражение слишком сложно понять на данном этапе).

Предположим, что следующий текстовый файл:

HD Alcoa Earnings Soar; Outlook Stays Upbeat 
BY By James R. Hagerty and Matthew Day 
PD 12 July 2011
LP 

Alcoa Inc. profit more than doubled in the second quarter.
The giant aluminum producer managed to meet analysts' forecasts.

However, profits wereless than expected

TD
Licence this article via our website:

http://example.com

Я прочитал этот текстовый файл с PHP, нужен надежный способ поместить содержимое файла в массив, например:

array(
  [HD] => Alcoa Earnings Soar; Outlook Stays Upbeat,
  [BY] => By James R. Hagerty and Matthew Day,
  [PD] => 12 July 2011,
  [LP] => Alcoa Inc. profit...than expected,
  [TD] => Licence this article via our website: http://example.com
)

Слова HD BY PD LP TD являются ключами для идентификации нового раздела в файле. В массиве все новые строки могут быть удалены из значений. В идеале я мог бы сделать это без регулярных выражений. Я считаю, что взрыва на всех клавишах может быть одним из способов сделать это, но это будет очень грязно:

$fields = array('HD', 'BY', 'PD', 'LP', 'TD');
$parts = explode($text, "\nHD ");
$HD = $parts[0];

Есть ли у кого-нибудь более четкое представление о том, как перебирать текст, возможно, один раз, и делить его на массив, как указано выше?

Ответ 1

Это еще один, даже более короткий подход без использования регулярных выражений.

/**
 * @param  array  array of stopwords eq: array('HD', 'BY', ...)
 * @param  string Text to search in
 * @param  string End Of Line symbol
 * @return array  [ stopword => string, ... ]
 */
function extract_parts(array $parts, $str, $eol=PHP_EOL) {
  $ret=array_fill_keys($parts, '');
  $current=null;
  foreach(explode($eol, $str) AS $line) {
    $substr = substr($line, 0, 2);
    if (isset($ret[$substr])) {
      $current = $substr;
      $line = trim(substr($line, 2));
    }
    if ($current) $ret[$current] .= $line;
  }
  return $ret;
}

$ret = extract_parts(array('HD', 'BY', 'PD', 'LP', 'TD'), $str);
var_dump($ret);

Почему бы не использовать регулярные выражения?

Так как документация php, особенно в preg_ *, рекомендует не использовать регулярные выражения, если это не требуется. Мне было интересно, какой из примеров ответов на этот вопрос имеет лучший результат.

Результат удивил меня:

Answer 1 by: hek2mgl     2.698 seconds (regexp)
Answer 2 by: Emo Mosley  2.38  seconds
Answer 3 by: anubhava    3.131 seconds (regexp)
Answer 4 by: jgb         1.448 seconds

Я бы ожидал, что варианты regexp будут самыми быстрыми.

В любом случае неплохо не использовать регулярные выражения. Другими словами: использование регулярных выражений не является лучшим решением в целом. Вы должны принять решение о наилучшем решении в каждом конкретном случае.

Вы можете повторить измерение с помощью этого script.


Edit

Вот краткий, более оптимизированный пример с использованием шаблона regexp. Все еще не так быстро, как мой пример выше, но быстрее, чем другие примеры на основе regexp.

Формат вывода может быть оптимизирован (пробелы/разрывы строк).

function extract_parts_regexp($str) {
  $a=array();
  preg_match_all('/(?<k>[A-Z]{2})(?<v>.*?)(?=\n[A-Z]{2}|$)/Ds', $str, $a);
  return array_combine($a['k'], $a['v']);
}

Ответ 2

Объявление от имени УПРОЩЕННОГО, FAST и READABLE кода регулярного выражения!

(From Pr0no в комментариях) Как вы думаете, вы могли бы упростить регулярное выражение или получить подсказку о том, как начать с php-решения? Да, Pr0n0, я считаю, что я могу упростить регулярное выражение.

Я хотел бы сделать так, что регулярное выражение, безусловно, лучший инструмент для работы, и что это не должно быть пугающим и нечитаемым выражениям, как мы видели ранее. Я нарушил эту функцию до понятных частей.

Я избегал сложных функций регулярного выражения, таких как группы захвата и выражения подстановочных знаков, и сосредоточился на попытке создать что-то простое, что вам будет комфортно возвращаться через 3 месяца.

Моя предлагаемая функция (прокомментировано)

function headerSplit($input) {

    // First, let put our headers (any two consecutive uppercase characters at the start of a line) in an array
    preg_match_all(
        "/^[A-Z]{2}/m",       /* Find 2 uppercase letters at start of a line   */
        $input,               /* In the '$input' string                        */
        $matches              /* And store them in a $matches array            */
    );

    // Next, let split our string into an array, breaking on those headers
    $split = preg_split(
        "/^[A-Z]{2}/m",       /* Find 2 uppercase letters at start of a line   */
        $input,               /* In the '$input' string                        */
        null,                 /* No maximum limit of matches                   */
        PREG_SPLIT_NO_EMPTY   /* Don't give us an empty first element          */
    );

    // Finally, put our values into a new associative array
    $result = array();
    foreach($matches[0] as $key => $value) {
        $result[$value] = str_replace(
            "\r\n",              /* Search for a new line character            */
            " ",                 /* And replace with a space                   */
            trim($split[$key])   /* After trimming the string                  */
        );
    }

    return $result;
}

И вывод (обратите внимание: вам может потребоваться заменить \r\n на \n в функции str_replace в зависимости от вашей операционной системы):

array(5) {
  ["HD"]=> string(41) "Alcoa Earnings Soar; Outlook Stays Upbeat"
  ["BY"]=> string(35) "By James R. Hagerty and Matthew Day"
  ["PD"]=> string(12) "12 July 2011"
  ["LP"]=> string(172) "Alcoa Inc. profit more than doubled in the second quarter.  The giant aluminum producer managed to meet analysts' forecasts.    However, profits wereless than expected"
  ["TD"]=> string(59) "Licence this article via our website:    http://example.com"
}

Удаление комментариев для более чистой функции

Сжатая версия этой функции. Он точно такой же, как и выше, но с удаленными комментариями:

function headerSplit($input) {
    preg_match_all("/^[A-Z]{2}/m",$input,$matches);
    $split = preg_split("/^[A-Z]{2}/m",$input,null,PREG_SPLIT_NO_EMPTY);
    $result = array();
    foreach($matches[0] as $key => $value) $result[$value] = str_replace("\r\n"," ",trim($split[$key]));
    return $result;
}

Теоретически, не имеет значения, какой из них вы используете в своем живом коде, поскольку комментарии синтаксического анализа мало влияют на производительность, поэтому используйте тот, с которым вам удобнее.

Разбивка используемого здесь регулярного выражения

В функции есть только одно выражение (хотя и используется дважды), для простоты разбивайте его:

"/^[A-Z]{2}/m"

/     - This is a delimiter, representing the start of the pattern.
^     - This means 'Match at the beginning of the text'.
[A-Z] - This means match any uppercase character.
{2}   - This means match exactly two of the previous character (so exactly two uppercase characters).
/     - This is the second delimiter, meaning the pattern is over.
m     - This is 'multi-line mode', telling regex to treat each line as a new string.

Это маленькое выражение достаточно мощное, чтобы соответствовать HD, но не HDM в начале строки, а не HD (например, в Full HD) в середине строки. Вы не сможете легко достичь этого с помощью вариантов без регулярного выражения.

Если вы хотите, чтобы два или более (вместо ровно 2) последовательных символов верхнего регистра означали новый раздел, используйте /^[A-Z]{2,}/m.

Использование списка предопределенных заголовков

Прочитав последний вопрос и ваш комментарий в сообщении @jgb, похоже, вы хотите использовать предварительно определенный список заголовков. Вы можете это сделать, заменив наше регулярное выражение на "/^(HD|BY|WC|PD|SN|SC|PG|LA|CY|LP|TD|CO|IN|NS|RE|IPC|PUB|AN)/m - | обрабатывается как "или" в регулярных выражениях.

Бенчмаркинг - читаемый не означает медленное

Как-то бенчмаркинг стал частью разговора, и хотя я думаю, что он не имеет смысла, чтобы предоставить вам читаемое и поддерживаемое решение, Я переписал JGB, чтобы показать вам несколько вещей.

Вот мои результаты, показывающие, что этот код на основе regex является самым быстрым вариантом здесь (эти результаты основаны на 5000 итерациях):

SWEETIE BELLE SOLUTION (2 UPPERCASE IS A HEADER):         0.054 seconds
SWEETIE BELLE SOLUTION (2+ UPPERCASE IS A HEADER):        0.057 seconds
MATEWKA SOLUTION (MODIFIED, 2 UPPERCASE IS A HEADER):     0.069 seconds
BABA SOLUTION (2 UPPERCASE IS A HEADER):                  0.075 seconds
SWEETIE BELLE SOLUTION (USES DEFINED LIST OF HEADERS):    0.086 seconds
JGB SOLUTION (USES DEFINED LIST OF HEADERS, MODIFIED):    0.107 seconds

И тесты для решений с неправильно отформатированным выходом:

MATEWKA SOLUTION:                                         0.056 seconds
JGB SOLUTION:                                             0.061 seconds
HEK2MGL SOLUTION:                                         0.106 seconds
ANUBHAVA SOLUTION:                                        0.167 seconds

Причина, по которой я предложил модифицированную версию функции JGB, состоит в том, что его исходная функция не удаляет новые строки перед добавлением абзацев в выходной массив. Операции с небольшими строками имеют огромную разницу в производительности и должны быть одинаково оценены, чтобы получить справедливую оценку производительности.

Кроме того, с функцией jgb, если вы перейдете в полный список заголовков, вы получите кучу нулевых значений в своих массивах, так как не чтобы проверить, присутствует ли ключ перед его назначением. Это приведет к другой производительности, если вы захотите зациклиться на этих значениях позже, поскольку сначала вам нужно будет проверить empty.

Ответ 3

Вот простое решение без регулярного выражения

$data = explode("\n", $str);
$output = array();
$key = null;

foreach($data as $text) {
    $newKey = substr($text, 0, 2);
    if (ctype_upper($newKey)) {
        $key = $newKey;
        $text = substr($text, 2);
    }
    $text = trim($text);
    isset($output[$key]) ? $output[$key] .= $text : $output[$key] = $text;
}
print_r($output);

Выход

Array
(
    [HD] => Alcoa Earnings Soar; Outlook Stays Upbeat
    [BY] => By James R. Hagerty and Matthew Day
    [PD] => 12 July 2011
    [LP] => Alcoa Inc. profit more than doubled in the second quarter.The giant aluminum producer managed to meet analysts' forecasts.However, profits wereless than expected
    [TD] => Licence this article via our website:http://example.com
)

Смотрите Live Demo

Примечание

Вы также можете сделать следующее:

  • Проверка дубликатов данных
  • Убедитесь, что используются только HD|BY|PD|LP|TD
  • Удалите $text = trim($text), чтобы новые строки сохранялись в тексте

Ответ 4

Если это всего одна запись на файл, вот вы:

$record = array();
foreach(file('input.txt') as $line) {
    if(preg_match('~^(HD|BY|PD|LP|TD) ?(.*)?$~', $line, $matches)) {
        $currentKey = $matches[1];
        $record[$currentKey] = $matches[2];
    } else {
        $record[$currentKey] .= str_replace("\n", ' ', $line);
    }   
}

Код выполняет итерацию по каждой строке ввода и проверяет, начинается ли строка с идентификатора. Если это так, currentKey устанавливается на этот идентификатор. Все последующие материалы, если новый идентификатор не был найден, будут добавлены к этому ключу в массиве после удаления новых строк.

var_dump($record);

Вывод:

array(5) {
  'HD' =>
  string(42) "Alcoa Earnings Soar; Outlook Stays Upbeat "
  'BY' =>
  string(36) "By James R. Hagerty and Matthew Day "
  'PD' =>
  string(12) "12 July 2011"
  'LP' =>
  string(169) " Alcoa Inc. profit more than doubled in the second quarter. The giant aluminum producer managed to meet analysts' forecasts.  However, profits wereless than expected  "
  'TD' =>
  string(58) "Licence this article via our website:  http://example.com "
}

Примечание. Если для каждого файла есть несколько записей, вы можете уточнить парсер для возврата многомерного массива:

$records = array();
foreach(file('input.txt') as $line) {
    if(preg_match('~^(HD|BY|PD|LP|TD) ?(.*)?$~', $line, $matches)) {
        $currentKey = $matches[1];

        // start a new record if `HD` was found.
        if($currentKey === 'HD') {
            if(is_array($record)) {
                $records []= $record;
            }
            $record = array();
        }
        $record[$currentKey] = $matches[2];
    } else {
        $record[$currentKey] .= str_replace("\n", ' ', $line);
    }   
}

Однако сам формат данных выглядит хрупким для меня. Что делать, если LP выглядит так:

LP dfks ldsfjksdjlf
lkdsjflk dsfjksld..
HD defsdf sdf sd....

Вы видите, что в моем примере в данных LP есть HD. Чтобы сохранить синтаксический анализ данных, вам придется избегать таких ситуаций.

Ответ 5

ОБНОВЛЕНИЕ:

Учитывая опубликованный файл ввода и код примера, я изменил свой ответ. Я добавил предоставленные OP "части", которые определяют коды секций и делают функцию способной обрабатывать коды с двумя или более цифрами. Ниже приведена некорректная процедурная функция, которая должна давать желаемые результаты:

# Parses the given text file and populates an array with coded sections.
# INPUT:
#   filename = (string) path and filename to text file to parse
# RETURNS: (assoc array)
#   null is returned if there was a file error or no data was found
#   otherwise an associated array of the field sections is returned
function getSections($parts, $lines) {
   $sections = array();
   $code = "";
   $str = "";
   # examine each line to build section array
   for($i=0; $i<sizeof($lines); $i++) {
      $line = trim($lines[$i]);
      # check for special field codes
      $words = explode(' ', $line, 2);
      $left = $words[0];
      #echo "DEBUG: left[$left]\n";
      if(in_array($left, $parts)) {
         # field code detected; first, finish previous section, if exists
         if($code) {
            # store the previous section
            $sections[$code] = trim($str);
         }
         # begin to process new section
         $code = $left;
         $str = trim(substr($line, strlen($code)));
      } else if($code && $line) {
         # keep a running string of section content
         $str .= " ".$line;
      }
   } # for i
   # check for no data
   if(!$code)
      return(null);
   # store the last section and return results
   $sections[$code] = trim($str);
   return($sections);
} # getSections()


$parts = array('HD', 'BY', 'WC', 'PD', 'SN', 'SC', 'PG', 'LA', 'CY', 'LP', 'TD', 'CO', 'IN', 'NS', 'RE', 'IPC', 'PUB', 'AN');

$datafile = $argv[1]; # NOTE: I happen to be testing this from command-line
# load file as array of lines
$lines = file($datafile);
if($lines === false)
   die("ERROR: unable to open file ".$datafile."\n");
$data = getSections($parts, $lines);
echo "Results from ".$datafile.":\n";
if($data)
   print_r($data);
else
   echo "ERROR: no data detected in ".$datafile."\n";

Результаты:

Array
(   
    [HD] => Alcoa Earnings Soar; Outlook Stays Upbeat
    [BY] => By James R. Hagerty and Matthew Day
    [PD] => 12 July 2011
    [LP] => Alcoa Inc. profit more than doubled in the second quarter. The giant aluminum producer managed to meet analysts' forecasts. However, profits wereless than expected
    [TD] => Licence this article via our website: http://example.com
)

Ответ 6

Это одна из проблем, когда я думаю, что использование регулярного выражения не должно быть проблемой с учетом правил для анализа данных. Рассмотрим такой код:

$s = file_get_contents('input'); // read input file into a string
$match = array(); // will hold final output
if (preg_match_all('~(^|[A-Z]{2})\s(.*?)(?=[A-Z]{2}\s|$)~s', $s, $arr)) {
    for ( $i = 0; $i < count($arr[1]); $i++ )
       $match[ trim($arr[1][$i]) ] = str_replace( "\n", "", $arr[2][$i] );
}
print_r($match);

Как вы можете видеть, как компактный код становится из-за того, как preg_match_all использовался для сопоставления данных из входного файла.

ВЫВОД:

Array
(
    [HD] => Alcoa Earnings Soar; Outlook Stays Upbeat 
    [BY] => By James R. Hagerty and Matthew Day 
    [PD] => 12 July 2011
    [LP] => Alcoa Inc. profit more than doubled in the second quarter.The giant aluminum producer managed to meet analysts' forecasts.However, profits wereless than expected
    [TD] => Licence this article via our website:http://example.com
)

Ответ 7

Не выполняйте цикл. Как насчет этого (предполагая одну запись на файл)?

$inrec = file_get_contents('input');
$inrec = str_replace( "\n'", "'", str_replace( array( 'HD ', 'BY ', 'PD ', 'LP', 'TD' ), array( "'HD' => '", "','BY' => '", "','PD' => '", "','LP' => '", "','TD' => '" ), str_replace( "'", "\\'", $inrec ) ) )."'";
eval( '$record = array('.$inrec.');' );
var_export($record);

результаты:

array (
  'HD' => 'Alcoa Earnings Soar; Outlook Stays Upbeat ',
  'BY' => 'By James R. Hagerty and Matthew Day ',
  'PD' => '12 July 2011',
  'LP' => ' 

Alcoa Inc.\ profit more than doubled in the second quarter.
The giant aluminum producer managed to meet analysts\' forecasts.

However, profits wereless than expected
',
  'TD' => '
Licence this article via our website:

http://example.com',
)

Если на файл может быть больше, чем на запись, попробуйте что-то вроде:

$inrecs = explode( 'HD ', file_get_contents('input') );
$records = array();
foreach ( $inrecs as $inrec ) {
   $inrec = str_replace( "\n'", "'", str_replace( array( 'HD ', 'BY ', 'PD ', 'LP', 'TD' ), array( "'HD' => '", "','BY' => '", "','PD' => '", "','LP' => '", "','TD' => '" ), str_replace( "'", "\\'", 'HD ' . $inrec ) ) )."'";
   eval( '$records[] = array('.$inrec.');' );
}
var_export($records);

Edit

Здесь версия с функциями $inrec распадается, поэтому ее можно легко понять - и с помощью нескольких настроек: strips new-lines, обрезает ведущие и конечные пробелы и обращает внимание на обратную связь в EVAL в случае, если данные из ненадежного источника.

$inrec = file_get_contents('input');
$inrec = str_replace( '\\', '\\\\', $inrec );       // Preceed all backslashes with backslashes
$inrec = str_replace( "'", "\\'", $inrec );         // Precede all single quotes with backslashes
$inrec = str_replace( PHP_EOL, " ", $inrec );       // Replace all new lines with spaces
$inrec = str_replace( array( 'HD ', 'BY ', 'PD ', 'LP ', 'TD ' ), array( "'HD' => trim('", "'),'BY' => trim('", "'),'PD' => trim('", "'),'LP' => trim('", "'),'TD' => trim('" ), $inrec )."')";
eval( '$record = array('.$inrec.');' );
var_export($record);

Результаты:

array (
  'HD' => 'Alcoa Earnings Soar; Outlook Stays Upbeat',
  'BY' => 'By James R. Hagerty and Matthew Day',
  'PD' => '12 July 2011',
  'LP' => 'Alcoa Inc.\ profit more than doubled in the second quarter. The giant aluminum producer managed to meet analysts\' forecasts.  However, profits wereless than expected',
  'TD' => 'Licence this article via our website:  http://example.com',
)

Ответ 8

Обновление

Мне казалось, что в сценарии с несколькими записями построение $repl за пределами цикла записи будет работать еще лучше. Здесь 2-байтная версия ключевого слова:

$inrecs = file_get_contents('input');
$inrecs = str_replace( PHP_EOL, " ", $inrecs );
$keys  = array( 'HD', 'BY', 'PD', 'LP', 'TD' );
$split = chr(255);
$repl = explode( ',', $split . implode( ','.$split, $keys ) );
$inrecs = explode( 'HD ', $inrecs );
array_shift( $inrecs );
$records = array();
foreach( $inrecs as $inrec ) $records[] = parseRecord( $keys, $repl, 'HD '.$inrec );

function parseRecord( $keys, $repl, $rec ) {
    $split = chr(255);
    $lines = explode( $split, str_replace( $keys, $repl, $rec ) );
    array_shift( $lines );
    $out = array();
    foreach ( $lines as $line ) $out[ substr( $line, 0, 2 ) ] = trim( substr( $line, 3 ) );
    return $out;
}

Бенчмарк (спасибо @jgb):

Answer 1 by: hek2mgl     6.783 seconds (regexp)
Answer 2 by: Emo Mosley  4.738 seconds
Answer 3 by: anubhava    6.299 seconds (regexp)
Answer 4 by: jgb         2.47 seconds
Answer 5 by: gwc         3.589 seconds (eval)
Answer 6 by: gwc         1.871 seconds

Здесь другой ответ для нескольких записей ввода (при условии, что каждая запись начинается с "HD" ) и поддерживает 2 байта, 2 или 3 байта или ключевые слова с переменной длиной.

$inrecs = file_get_contents('input');
$inrecs = str_replace( PHP_EOL, " ", $inrecs );
$keys  = array( 'HD', 'BY', 'PD', 'LP', 'TD' );
$inrecs = explode( 'HD ', $inrecs );
array_shift( $inrecs );
$records = array();
foreach( $inrecs as $inrec ) $records[] = parseRecord( $keys, 'HD '.$inrec );

Запишите запись с двумя байтовыми ключевыми словами:

function parseRecord( $keys, $rec ) {
    $split = chr(255);
    $repl = explode( ',', $split . implode( ','.$split, $keys ) );
    $lines = explode( $split, str_replace( $keys, $repl, $rec ) );
    array_shift( $lines );
    $out = array();
    foreach ( $lines as $line ) $out[ substr( $line, 0, 2 ) ] = trim( substr( $line, 3 ) );
    return $out;
}

Записывать запись с 2 или 3 байтовыми ключевыми словами (предполагает пробел или PHP_EOL между ключом и контентом):

function parseRecord( $keys, $rec ) {
    $split = chr(255);
    $repl = explode( ',', $split . implode( ','.$split, $keys ) );
    $lines = explode( $split, str_replace( $keys, $repl, $rec ) );
    array_shift( $lines );
    $out = array();
    foreach ( $lines as $line ) $out[ trim( substr( $line, 0, 3 ) ) ] = trim( substr( $line, 3 ) );
    return $out;
}

Записывать запись с ключевыми словами переменной длины (предполагает пробел или PHP_EOL между ключом и контентом):

function parseRecord( $keys, $rec ) {
    $split = chr(255);
    $repl = explode( ',', $split . implode( ','.$split, $keys ) );
    $lines = explode( $split, str_replace( $keys, $repl, $rec ) );
    array_shift( $lines );
    $out = array();
    foreach ( $lines as $line ) {
        $keylen = strpos( $line.' ', ' ' );
        $out[ trim( substr( $line, 0, $keylen ) ) ] = trim( substr( $line, $keylen+1 ) );
    }
    return $out;
}
Ожидается, что каждая функция parseRecord выше будет немного хуже, чем ее предшественник.

Результаты:

Array
(
    [0] => Array
        (
            [HD] => Alcoa Earnings Soar; Outlook Stays Upbeat
            [BY] => By James R. Hagerty and Matthew Day
            [PD] => 12 July 2011
            [LP] => Alcoa Inc. profit more than doubled in the second quarter. The giant aluminum producer managed to meet analysts' forecasts.  However, profits wereless than expected
            [TD] => Licence this article via our website:  http://example.com
        )

)

Ответ 9

Я подготовил собственное решение, которое получилось немного быстрее, чем jgb answer. Здесь код:

function answer_5(array $parts, $str) {
    $result = array_fill_keys($parts, '');
    $poss = $result;
    foreach($poss as $key => &$val) {
        $val = strpos($str, "\n" . $key) + 2;
    }

    arsort($poss);

    foreach($poss as $key => $pos) {
        $result[$key] = trim(substr($str, $pos+1));
        $str = substr($str, 0, $pos-1);
    }
    return str_replace("\n", "", $result);
}

И вот сравнение производительности:

Answer 1 by: hek2mgl    2.791 seconds (regexp) 
Answer 2 by: Emo Mosley 2.553 seconds 
Answer 3 by: anubhava   3.087 seconds (regexp) 
Answer 4 by: jgb        1.53  seconds 
Answer 5 by: matewka    1.403 seconds

Тестирование среды было таким же, как и jgb (100000 итераций - script, заимствованных из здесь).

Наслаждайтесь и, пожалуйста, оставляйте комментарии.