stop_words_list = array_flip( $this->stop_words_list ); } /** * Split string to words and phrases. * * @param string $text Text with '|' as formerly tags. * @param int $words_count How many words can have a phrase. * @param bool $return_array Return as array with frequencies instead of array of tokens. * * @return array */ public function tokenize( string $text, int $words_count = 5, bool $return_array = false ) : array { $result = []; // result as array of tokens. $result123 = array_fill( 1, $words_count, [] ); // result as array with tokens and count of each token. $this->counters = [ 1 => [] ]; $parts = explode( '|', $text ); $content_blocks = []; $this->used_indexes = []; $this->first_indexes = []; $word_len = 2; $this->counters[ $word_len ] = []; foreach ( $parts as $part ) { $part = trim( $part ); if ( '' !== $part ) { $substrings = explode( ' ', $part ); $a = count( $content_blocks ); $content_blocks[] = $substrings; $queue = []; // reset queue, so we do not include words from different blocks to phrase. $queue_len = 0; foreach ( $substrings as $b => $word ) { $len = function_exists( 'mb_strlen' ) ? mb_strlen( $word ) : strlen( $word ); if ( ! $this->is_stop_word( $word ) && ! is_numeric( $word ) && $len > 1 ) { // skip any stop words or numbers or too short words. if ( self::ADD_SINGLE_TIME_WORDS ) { // @phpstan-ignore-line -- constant can have any value. $result[] = $word; // add word. } if ( $return_array ) { if ( isset( $this->counters[1][ $word ] ) ) { $result[] = $word; // add word that found more that once only. if ( 1 === $this->counters[1][ $word ]++ ) { $result[] = $word; // add word twice. } } else { $this->counters[1][ $word ] = 1; } } } // but allow phrases with stop words. $queue[] = $word; // add to queue. ++$queue_len; if ( $queue_len > $word_len ) { // remove first item from queue. array_shift( $queue ); --$queue_len; } if ( $queue_len === $word_len ) { // add phrases from queue to result. $index = "{$queue[0]} {$queue[1]}"; $this->add_phrase( $result, $word_len, $index, $a, $b - 1 ); // b-1 because of queue length = 2, and we need index of starting word. } } } } if ( $return_array ) { $result123[1] = array_filter( $this->counters[1], function( $value ) { return $value > 1; } ); // [ string word => int count]. } if ( $words_count < 3 ) { return $return_array ? $result123 : $result; } $this->first_indexes = []; /* * === phrase length 3, 4... * Build longer phrases using already found phrases from previous iteration. * Save new phrases if they used more than once. * $this->used_indexes - is a pointer to begin of each existing phrase. * $word_len - is a desired phrase length. */ for ( $word_len = 3; $word_len <= $words_count; $word_len++ ) { // === Remove all phrases with count < 2. $word_len_1 = $word_len - 1; $this->counters[ $word_len_1 ] = array_filter( $this->counters[ $word_len_1 ], function( $v ) { return $v[0] > 1; } ); if ( $return_array && is_array( $this->counters[ $word_len_1 ] ) ) { foreach ( $this->counters[ $word_len_1 ] as $word => list( $count, $indexes ) ) { $result123[ $word_len_1 ][ $word ] = $count; } } $this->counters[ $word_len ] = []; $existing_phrases = $this->used_indexes; // source array is unique because we check indexes before add. if ( count( $existing_phrases ) ) { $this->used_indexes = []; $this->first_indexes = []; foreach ( $existing_phrases as &$pointer ) { $a = &$pointer[0]; $b_current = &$pointer[1]; // can we add a word before the current phrase to new phrase? if ( $b_current > 0 ) { $b = $b_current - 1; $index = implode( ' ', array_slice( $content_blocks[ $a ], $b, $word_len ) ); $this->add_phrase( $result, $word_len, $index, $a, $b ); } // can we add a word after the current phrase to new phrase? if ( $b_current + $word_len < count( $content_blocks[ $a ] ) - 1 ) { $b = $b_current; // start from same index, but 1 word longer. $index = implode( ' ', array_slice( $content_blocks[ $a ], $b, $word_len ) ); $this->add_phrase( $result, $word_len, $index, $a, $b ); } } } } // clean memory. $this->used_indexes = []; $this->first_indexes = []; if ( $return_array ) { foreach ( $this->counters[ $words_count ] as $word => list( $count, $indexes ) ) { if ( $count > 1 ) { $result123[ $words_count ][ $word ] = $count; } } $result = []; } $this->counters = []; return $return_array ? $result123 : $result; } /** * Add phrase to index and update counters * * @param array $result Result array, where to add. * @param int $word_len Word length. * @param string $index Phrase to add. * @param int $a [$a, $b] - pair of indexes in $content_blocks[$a][$b] array, where this phrase started. * @param int $b [$a, $b] - pair of indexes in $content_blocks[$a][$b] array, where this phrase started. * @return void */ private function add_phrase( array &$result, int $word_len, string &$index, int $a, int $b ) : void { if ( isset( $this->counters[ $word_len ][ $index ] ) ) { // check is $a $b pair unique? if ( in_array( [ $a, $b ], $this->counters[ $word_len ][ $index ][1] ) ) { // phpcs:ignore WordPress.PHP.StrictInArray.MissingTrueStrict return; // this indexes already added. } $result[] = $index; // save to results - for current match. ++$this->counters[ $word_len ][ $index ][0]; $this->counters[ $word_len ][ $index ][1][] = [ $a, $b ]; if ( isset( $this->first_indexes[ $index ] ) ) { $result[] = $index; // save to results - for first match. $this->used_indexes[] = $this->first_indexes[ $index ]; unset( $this->first_indexes[ $index ] ); } $this->used_indexes[] = [ $a, $b ]; } else { $this->counters[ $word_len ][ $index ] = [ 1, [ [ $a, $b ] ] ]; $this->first_indexes[ $index ] = [ $a, $b ]; } } /** * Is the word a stop word? * * @param string $token Token (Char or word or string). * @return bool */ private function is_stop_word( string &$token ) : bool { return isset( $this->stop_words_list[ $token ] ); } /** * Return all stop words. * * @return array Associative array where word is index. */ public function get_stop_words() : array { return $this->stop_words_list; } }