Text_LanguageDetect
[ class tree: Text_LanguageDetect ] [ index: Text_LanguageDetect ] [ all elements ]

Source for file LanguageDetect.php

Documentation is available at LanguageDetect.php

  1. <?php
  2.  
  3. /**
  4.  * Detects the language of a given piece of text.
  5.  *
  6.  * Attempts to detect the language of a sample of text by correlating ranked
  7.  * 3-gram frequencies to a table of 3-gram frequencies of known languages.
  8.  *
  9.  * Implements a version of a technique originally proposed by Cavnar & Trenkle
  10.  * (1994): "N-Gram-Based Text Categorization"
  11.  *
  12.  * PHP version 5
  13.  *
  14.  * @category  Text
  15.  * @package   Text_LanguageDetect
  16.  * @author    Nicholas Pisarro <infinityminusnine+pear@gmail.com>
  17.  * @copyright 2005-2006 Nicholas Pisarro
  18.  * @license   http://www.debian.org/misc/bsd.license BSD
  19.  * @version   SVN: $Id: LanguageDetect.php 322353 2012-01-16 08:41:43Z cweiske $
  20.  * @link      http://pear.php.net/package/Text_LanguageDetect/
  21.  * @link      http://langdetect.blogspot.com/
  22.  */
  23.  
  24. require_once 'Text/LanguageDetect/Exception.php';
  25. require_once 'Text/LanguageDetect/Parser.php';
  26. require_once 'Text/LanguageDetect/ISO639.php';
  27.  
  28. /**
  29.  * Language detection class
  30.  *
  31.  * Requires the langauge model database (lang.dat) that should have
  32.  * accompanied this class definition in order to be instantiated.
  33.  *
  34.  * Example usage:
  35.  *
  36.  * <code>
  37.  * require_once 'Text/LanguageDetect.php';
  38.  *
  39.  * $l = new Text_LanguageDetect;
  40.  *
  41.  * $stdin = fopen('php://stdin', 'r');
  42.  *
  43.  * echo "Supported languages:\n";
  44.  *
  45.  * try {
  46.  *     $langs = $l->getLanguages();
  47.  * } catch (Text_LanguageDetect_Exception $e) {
  48.  *     die($e->getMessage());
  49.  * }
  50.  *
  51.  * sort($langs);
  52.  * echo join(', ', $langs);
  53.  *
  54.  * while ($line = fgets($stdin)) {
  55.  *     print_r($l->detect($line, 4));
  56.  * }
  57.  * </code>
  58.  *
  59.  * @category  Text
  60.  * @package   Text_LanguageDetect
  61.  * @author    Nicholas Pisarro <infinityminusnine+pear@gmail.com>
  62.  * @copyright 2005 Nicholas Pisarro
  63.  * @license   http://www.debian.org/misc/bsd.license BSD
  64.  * @version   Release: @package_version@
  65.  * @link      http://pear.php.net/package/Text_LanguageDetect/
  66.  * @todo      allow users to generate their own language models
  67.  */
  68. {
  69.     /**
  70.      * The filename that stores the trigram data for the detector
  71.      *
  72.      * If this value starts with a slash (/) or a dot (.) the value of
  73.      * $this->_data_dir will be ignored
  74.      *
  75.      * @var      string 
  76.      * @access   private
  77.      */
  78.     var $_db_filename 'lang.dat';
  79.  
  80.     /**
  81.      * The filename that stores the unicode block definitions
  82.      *
  83.      * If this value starts with a slash (/) or a dot (.) the value of
  84.      * $this->_data_dir will be ignored
  85.      *
  86.      * @var string 
  87.      * @access private
  88.      */
  89.     var $_unicode_db_filename 'unicode_blocks.dat';
  90.  
  91.     /**
  92.      * The data directory
  93.      *
  94.      * Should be set by PEAR installer
  95.      *
  96.      * @var      string 
  97.      * @access   private
  98.      */
  99.     var $_data_dir '@data_dir@';
  100.  
  101.     /**
  102.      * The trigram data for comparison
  103.      *
  104.      * Will be loaded on start from $this->_db_filename
  105.      *
  106.      * @var      array 
  107.      * @access   private
  108.      */
  109.     var $_lang_db = array();
  110.  
  111.     /**
  112.      * stores the map of the trigram data to unicode characters
  113.      *
  114.      * @access private
  115.      * @var array 
  116.      */
  117.     var $_unicode_map;
  118.  
  119.     /**
  120.      * The size of the trigram data arrays
  121.      *
  122.      * @var      int 
  123.      * @access   private
  124.      */
  125.     var $_threshold = 300;
  126.  
  127.     /**
  128.      * the maximum possible score.
  129.      *
  130.      * needed for score normalization. Different depending on the
  131.      * perl compatibility setting
  132.      *
  133.      * @access  private
  134.      * @var     int 
  135.      * @see     setPerlCompatible()
  136.      */
  137.     var $_max_score = 0;
  138.  
  139.     /**
  140.      * Whether or not to simulate perl's Language::Guess exactly
  141.      *
  142.      * @access  private
  143.      * @var     bool 
  144.      * @see     setPerlCompatible()
  145.      */
  146.     var $_perl_compatible = false;
  147.  
  148.     /**
  149.      * Whether to use the unicode block detection to speed up processing
  150.      *
  151.      * @access private
  152.      * @var bool 
  153.      */
  154.     var $_use_unicode_narrowing = true;
  155.  
  156.     /**
  157.      * stores the result of the clustering operation
  158.      *
  159.      * @access  private
  160.      * @var     array 
  161.      * @see     clusterLanguages()
  162.      */
  163.     var $_clusters;
  164.  
  165.     /**
  166.      * Which type of "language names" are accepted and returned:
  167.      *
  168.      * 0 - language name ("english")
  169.      * 2 - 2-letter ISO 639-1 code ("en")
  170.      * 3 - 3-letter ISO 639-2 code ("eng")
  171.      */
  172.     var $_name_mode = 0;
  173.  
  174.     /**
  175.      * Constructor
  176.      *
  177.      * Will attempt to load the language database. If it fails, you will get
  178.      * an exception.
  179.      */
  180.     function __construct()
  181.     {
  182.         $data $this->_readdb($this->_db_filename);
  183.         $this->_checkTrigram($data['trigram']);
  184.         $this->_lang_db $data['trigram'];
  185.  
  186.         if (isset($data['trigram-unicodemap'])) {
  187.             $this->_unicode_map $data['trigram-unicodemap'];
  188.         }
  189.  
  190.         // Not yet implemented:
  191.         if (isset($data['trigram-clusters'])) {
  192.             $this->_clusters $data['trigram-clusters'];
  193.         }
  194.     }
  195.  
  196.     /**
  197.      * Returns the path to the location of the database
  198.      *
  199.      * @param string $fname File name to load
  200.      *
  201.      * @return string expected path to the language model database
  202.      * @access private
  203.      */
  204.     function _get_data_loc($fname)
  205.     {
  206.         if ($fname{0== '/' || $fname{0== '.'{
  207.             // if filename starts with a slash, assume it's an absolute pathname
  208.             // and skip whatever is in $this->_data_dir
  209.             return $fname;
  210.  
  211.         elseif ($this->_data_dir != '@' 'data_dir' '@'{
  212.             // if the data dir was set by the PEAR installer, use that
  213.             return $this->_data_dir '/Text_LanguageDetect/' $fname;
  214.  
  215.         else {
  216.             // assume this was just unpacked somewhere
  217.             // try the local working directory if otherwise
  218.             return __DIR__ . '/../data/' $fname;
  219.         }
  220.     }
  221.  
  222.     /**
  223.      * Loads the language trigram database from filename
  224.      *
  225.      * Trigram datbase should be a serialize()'d array
  226.      *
  227.      * @param string $fname the filename where the data is stored
  228.      *
  229.      * @return array the language model data
  230.      * @throws Text_LanguageDetect_Exception
  231.      * @access private
  232.      */
  233.     function _readdb($fname)
  234.     {
  235.         // finds the correct data dir
  236.         $fname $this->_get_data_loc($fname);
  237.  
  238.         // input check
  239.         if (!file_exists($fname)) {
  240.             throw new Text_LanguageDetect_Exception(
  241.                 'Language database does not exist: ' $fname,
  242.                 Text_LanguageDetect_Exception::DB_NOT_FOUND
  243.             );
  244.         elseif (!is_readable($fname)) {
  245.             throw new Text_LanguageDetect_Exception(
  246.                 'Language database is not readable: ' $fname,
  247.                 Text_LanguageDetect_Exception::DB_NOT_READABLE
  248.             );
  249.         }
  250.  
  251.         return unserialize(file_get_contents($fname));
  252.     }
  253.  
  254.  
  255.     /**
  256.      * Checks if this object is ready to detect languages
  257.      *
  258.      * @param array $trigram Trigram data from database
  259.      *
  260.      * @return void 
  261.      * @access private
  262.      */
  263.     function _checkTrigram($trigram)
  264.     {
  265.         if (!is_array($trigram)) {
  266.             if (ini_get('magic_quotes_runtime')) {
  267.                 throw new Text_LanguageDetect_Exception(
  268.                     'Error loading database. Try turning magic_quotes_runtime off.',
  269.                     Text_LanguageDetect_Exception::MAGIC_QUOTES
  270.                 );
  271.             }
  272.             throw new Text_LanguageDetect_Exception(
  273.                 'Language database is not an array.',
  274.                 Text_LanguageDetect_Exception::DB_NOT_ARRAY
  275.             );
  276.         elseif (empty($trigram)) {
  277.             throw new Text_LanguageDetect_Exception(
  278.                 'Language database has no elements.',
  279.                 Text_LanguageDetect_Exception::DB_EMPTY
  280.             );
  281.         }
  282.     }
  283.  
  284.     /**
  285.      * Omits languages
  286.      *
  287.      * Pass this function the name of or an array of names of
  288.      * languages that you don't want considered
  289.      *
  290.      * If you're only expecting a limited set of languages, this can greatly
  291.      * speed up processing
  292.      *
  293.      * @param mixed $omit_list    language name or array of names to omit
  294.      * @param bool  $include_only if true will include (rather than
  295.      *                             exclude) only those in the list
  296.      *
  297.      * @return int number of languages successfully deleted
  298.      * @throws Text_LanguageDetect_Exception
  299.      */
  300.     public function omitLanguages($omit_list$include_only = false)
  301.     {
  302.         $deleted = 0;
  303.  
  304.         $omit_list $this->_convertFromNameMode($omit_list);
  305.  
  306.         if (!$include_only{
  307.             // deleting the given languages
  308.             if (!is_array($omit_list)) {
  309.                 $omit_list strtolower($omit_list)// case desensitize
  310.                 if (isset($this->_lang_db[$omit_list])) {
  311.                     unset($this->_lang_db[$omit_list]);
  312.                     $deleted++;
  313.                 }
  314.             else {
  315.                 foreach ($omit_list as $omit_lang{
  316.                     if (isset($this->_lang_db[$omit_lang])) {
  317.                         unset($this->_lang_db[$omit_lang]);
  318.                         $deleted++;
  319.                     }
  320.                 }
  321.             }
  322.  
  323.         else {
  324.             // deleting all except the given languages
  325.             if (!is_array($omit_list)) {
  326.                 $omit_list = array($omit_list);
  327.             }
  328.  
  329.             // case desensitize
  330.             foreach ($omit_list as $key => $omit_lang{
  331.                 $omit_list[$keystrtolower($omit_lang);
  332.             }
  333.  
  334.             foreach (array_keys($this->_lang_dbas $lang{
  335.                 if (!in_array($lang$omit_list)) {
  336.                     unset($this->_lang_db[$lang]);
  337.                     $deleted++;
  338.                 }
  339.             }
  340.         }
  341.  
  342.         // reset the cluster cache if the number of languages changes
  343.         // this will then have to be recalculated
  344.         if (isset($this->_clusters&& $deleted > 0{
  345.             $this->_clusters = null;
  346.         }
  347.  
  348.         return $deleted;
  349.     }
  350.  
  351.  
  352.     /**
  353.      * Returns the number of languages that this object can detect
  354.      *
  355.      * @access public
  356.      * @return int            the number of languages
  357.      * @throws   Text_LanguageDetect_Exception
  358.      */
  359.     function getLanguageCount()
  360.     {
  361.         return count($this->_lang_db);
  362.     }
  363.  
  364.     /**
  365.      * Checks if the language with the given name exists in the database
  366.      *
  367.      * @param mixed $lang Language name or array of language names
  368.      *
  369.      * @return bool true if language model exists
  370.      */
  371.     public function languageExists($lang)
  372.     {
  373.         $lang $this->_convertFromNameMode($lang);
  374.  
  375.         if (is_string($lang)) {
  376.             return isset($this->_lang_db[strtolower($lang)]);
  377.  
  378.         elseif (is_array($lang)) {
  379.             foreach ($lang as $test_lang{
  380.                 if (!isset($this->_lang_db[strtolower($test_lang)])) {
  381.                     return false;
  382.                 }
  383.             }
  384.             return true;
  385.  
  386.         else {
  387.             throw new Text_LanguageDetect_Exception(
  388.                 'Unsupported parameter type passed to languageExists()',
  389.                 Text_LanguageDetect_Exception::PARAM_TYPE
  390.             );
  391.         }
  392.     }
  393.  
  394.     /**
  395.      * Returns the list of detectable languages
  396.      *
  397.      * @access public
  398.      * @return array        the names of the languages known to this object<<<<<<<
  399.      * @throws   Text_LanguageDetect_Exception
  400.      */
  401.     function getLanguages()
  402.     {
  403.         return $this->_convertToNameMode(
  404.             array_keys($this->_lang_db)
  405.         );
  406.     }
  407.  
  408.     /**
  409.      * Make this object behave like Language::Guess
  410.      *
  411.      * @param bool $setting false to turn off perl compatibility
  412.      *
  413.      * @return void 
  414.      */
  415.     public function setPerlCompatible($setting = true)
  416.     {
  417.         if (is_bool($setting)) // input check
  418.             $this->_perl_compatible $setting;
  419.  
  420.             if ($setting == true{
  421.                 $this->_max_score $this->_threshold;
  422.             else {
  423.                 $this->_max_score = 0;
  424.             }
  425.         }
  426.  
  427.     }
  428.  
  429.     /**
  430.      * Sets the way how language names are accepted and returned.
  431.      *
  432.      * @param integer $name_mode One of the following modes:
  433.      *                            0 - language name ("english")
  434.      *                            2 - 2-letter ISO 639-1 code ("en")
  435.      *                            3 - 3-letter ISO 639-2 code ("eng")
  436.      *
  437.      * @return void 
  438.      */
  439.     function setNameMode($name_mode)
  440.     {
  441.         $this->_name_mode $name_mode;
  442.     }
  443.  
  444.     /**
  445.      * Whether to use unicode block ranges in detection
  446.      *
  447.      * Should speed up most detections if turned on (detault is on). In some
  448.      * circumstances it may be slower, such as for large text samples (> 10K)
  449.      * in languages that use latin scripts. In other cases it should speed up
  450.      * detection noticeably.
  451.      *
  452.      * @param bool $setting false to turn off
  453.      *
  454.      * @return void 
  455.      */
  456.     public function useUnicodeBlocks($setting = true)
  457.     {
  458.         if (is_bool($setting)) {
  459.             $this->_use_unicode_narrowing $setting;
  460.         }
  461.     }
  462.  
  463.     /**
  464.      * Converts a piece of text into trigrams
  465.      *
  466.      * @param string $text text to convert
  467.      *
  468.      * @return     array array of trigram frequencies
  469.      * @access     private
  470.      * @deprecated Superceded by the Text_LanguageDetect_Parser class
  471.      */
  472.     function _trigram($text)
  473.     {
  474.         $s = new Text_LanguageDetect_Parser($text);
  475.         $s->prepareTrigram();
  476.         $s->prepareUnicode(false);
  477.         $s->setPadStart(!$this->_perl_compatible);
  478.         $s->analyze();
  479.         return $s->getTrigramFreqs();
  480.     }
  481.  
  482.     /**
  483.      * Converts a set of trigrams from frequencies to ranks
  484.      *
  485.      * Thresholds (cuts off) the list at $this->_threshold
  486.      *
  487.      * @param array $arr array of trigram
  488.      *
  489.      * @return array ranks of trigrams
  490.      * @access protected
  491.      */
  492.     function _arr_rank($arr)
  493.     {
  494.  
  495.         // sorts alphabetically first as a standard way of breaking rank ties
  496.         $this->_bub_sort($arr);
  497.  
  498.         // below might also work, but seemed to introduce errors in testing
  499.         //ksort($arr);
  500.         //asort($arr);
  501.  
  502.         $rank = array();
  503.  
  504.         $i = 0;
  505.         foreach ($arr as $key => $value{
  506.             $rank[$key$i++;
  507.  
  508.             // cut off at a standard threshold
  509.             if ($i >= $this->_threshold{
  510.                 break;
  511.             }
  512.         }
  513.  
  514.         return $rank;
  515.     }
  516.  
  517.     /**
  518.      * Sorts an array by value breaking ties alphabetically
  519.      *
  520.      * @param array &$arr the array to sort
  521.      *
  522.      * @return void 
  523.      * @access private
  524.      */
  525.     function _bub_sort(&$arr)
  526.     {
  527.         // should do the same as this perl statement:
  528.         // sort { $trigrams{$b} == $trigrams{$a}
  529.         //   ?  $a cmp $b : $trigrams{$b} <=> $trigrams{$a} }
  530.  
  531.         // needs to sort by both key and value at once
  532.         // using the key to break ties for the value
  533.  
  534.         // converts array into an array of arrays of each key and value
  535.         // may be a better way of doing this
  536.         $combined = array();
  537.  
  538.         foreach ($arr as $key => $value{
  539.             $combined[= array($key$value);
  540.         }
  541.  
  542.         usort($combinedarray($this'_sort_func'));
  543.  
  544.         $replacement = array();
  545.         foreach ($combined as $key => $value{
  546.             list($new_key$new_value$value;
  547.             $replacement[$new_key$new_value;
  548.         }
  549.  
  550.         $arr $replacement;
  551.     }
  552.  
  553.     /**
  554.      * Sort function used by bubble sort
  555.      *
  556.      * Callback function for usort().
  557.      *
  558.      * @param array $a first param passed by usort()
  559.      * @param array $b second param passed by usort()
  560.      *
  561.      * @return int 1 if $a is greater, -1 if not
  562.      * @see    _bub_sort()
  563.      * @access private
  564.      */
  565.     function _sort_func($a$b)
  566.     {
  567.         // each is actually a key/value pair, so that it can compare using both
  568.         list($a_key$a_value$a;
  569.         list($b_key$b_value$b;
  570.  
  571.         if ($a_value == $b_value{
  572.             // if the values are the same, break ties using the key
  573.             return strcmp($a_key$b_key);
  574.  
  575.         else {
  576.             // if not, just sort normally
  577.             if ($a_value $b_value{
  578.                 return -1;
  579.             else {
  580.                 return 1;
  581.             }
  582.         }
  583.  
  584.         // 0 should not be possible because keys must be unique
  585.     }
  586.  
  587.     /**
  588.      * Calculates a linear rank-order distance statistic between two sets of
  589.      * ranked trigrams
  590.      *
  591.      * Sums the differences in rank for each trigram. If the trigram does not
  592.      * appear in both, consider it a difference of $this->_threshold.
  593.      *
  594.      * This distance measure was proposed by Cavnar & Trenkle (1994). Despite
  595.      * its simplicity it has been shown to be highly accurate for language
  596.      * identification tasks.
  597.      *
  598.      * @param array $arr1 the reference set of trigram ranks
  599.      * @param array $arr2 the target set of trigram ranks
  600.      *
  601.      * @return int the sum of the differences between the ranks of
  602.      *              the two trigram sets
  603.      * @access private
  604.      */
  605.     function _distance($arr1$arr2)
  606.     {
  607.         $sumdist = 0;
  608.  
  609.         foreach ($arr2 as $key => $value{
  610.             if (isset($arr1[$key])) {
  611.                 $distance abs($value $arr1[$key]);
  612.             else {
  613.                 // $this->_threshold sets the maximum possible distance value
  614.                 // for any one pair of trigrams
  615.                 $distance $this->_threshold;
  616.             }
  617.             $sumdist += $distance;
  618.         }
  619.  
  620.         return $sumdist;
  621.  
  622.         // todo: there are other distance statistics to try, e.g. relative
  623.         //       entropy, but they're probably more costly to compute
  624.     }
  625.  
  626.     /**
  627.      * Normalizes the score returned by _distance()
  628.      *
  629.      * Different if perl compatible or not
  630.      *
  631.      * @param int $score      the score from _distance()
  632.      * @param int $base_count the number of trigrams being considered
  633.      *
  634.      * @return float the normalized score
  635.      * @see    _distance()
  636.      * @access private
  637.      */
  638.     function _normalize_score($score$base_count = null)
  639.     {
  640.         if ($base_count === null{
  641.             $base_count $this->_threshold;
  642.         }
  643.  
  644.         if (!$this->_perl_compatible{
  645.             return 1 - ($score $base_count $this->_threshold);
  646.         else {
  647.             return floor($score $base_count);
  648.         }
  649.     }
  650.  
  651.  
  652.     /**
  653.      * Detects the closeness of a sample of text to the known languages
  654.      *
  655.      * Calculates the statistical difference between the text and
  656.      * the trigrams for each language, normalizes the score then
  657.      * returns results for all languages in sorted order
  658.      *
  659.      * If perl compatible, the score is 300-0, 0 being most similar.
  660.      * Otherwise, it's 0-1 with 1 being most similar.
  661.      *
  662.      * The $sample text should be at least a few sentences in length;
  663.      * should be ascii-7 or utf8 encoded, if another and the mbstring extension
  664.      * is present it will try to detect and convert. However, experience has
  665.      * shown that mb_detect_encoding() *does not work very well* with at least
  666.      * some types of encoding.
  667.      *
  668.      * @param string $sample a sample of text to compare.
  669.      * @param int    $limit  if specified, return an array of the most likely
  670.      *                        $limit languages and their scores.
  671.      *
  672.      * @return mixed sorted array of language scores, blank array if no
  673.      *                useable text was found
  674.      * @see    _distance()
  675.      * @throws Text_LanguageDetect_Exception
  676.      */
  677.     public function detect($sample$limit = 0)
  678.     {
  679.         // input check
  680.         if (!Text_LanguageDetect_Parser::validateString($sample)) {
  681.             return array();
  682.         }
  683.  
  684.         // check char encoding
  685.         // (only if mbstring extension is compiled and PHP > 4.0.6)
  686.         if (function_exists('mb_detect_encoding')
  687.             && function_exists('mb_convert_encoding')
  688.         {
  689.             // mb_detect_encoding isn't very reliable, to say the least
  690.             // detection should still work with a sufficient sample
  691.             //  of ascii characters
  692.             $encoding mb_detect_encoding($sample);
  693.  
  694.             // mb_detect_encoding() will return FALSE if detection fails
  695.             // don't attempt conversion if that's the case
  696.             if ($encoding != 'ASCII' && $encoding != 'UTF-8'
  697.                 && $encoding !== false
  698.             {
  699.                 // verify the encoding exists in mb_list_encodings
  700.                 if (in_array($encodingmb_list_encodings())) {
  701.                     $sample mb_convert_encoding($sample'UTF-8'$encoding);
  702.                 }
  703.             }
  704.         }
  705.  
  706.         $sample_obj = new Text_LanguageDetect_Parser($sample);
  707.         $sample_obj->prepareTrigram();
  708.         if ($this->_use_unicode_narrowing{
  709.             $sample_obj->prepareUnicode();
  710.         }
  711.         $sample_obj->setPadStart(!$this->_perl_compatible);
  712.         $sample_obj->analyze();
  713.  
  714.         $trigram_freqs =$sample_obj->getTrigramRanks();
  715.         $trigram_count count($trigram_freqs);
  716.  
  717.         if ($trigram_count == 0{
  718.             return array();
  719.         }
  720.  
  721.         $scores = array();
  722.  
  723.         // use unicode block detection to narrow down the possibilities
  724.         if ($this->_use_unicode_narrowing{
  725.             $blocks =$sample_obj->getUnicodeBlocks();
  726.  
  727.             if (is_array($blocks)) {
  728.                 $present_blocks array_keys($blocks);
  729.             else {
  730.                 throw new Text_LanguageDetect_Exception(
  731.                     'Error during block detection',
  732.                     Text_LanguageDetect_Exception::BLOCK_DETECTION
  733.                 );
  734.             }
  735.  
  736.             $possible_langs = array();
  737.  
  738.             foreach ($present_blocks as $blockname{
  739.                 if (isset($this->_unicode_map[$blockname])) {
  740.  
  741.                     $possible_langs array_merge(
  742.                         $possible_langs,
  743.                         array_keys($this->_unicode_map[$blockname])
  744.                     );
  745.  
  746.                     // todo: faster way to do this?
  747.                 }
  748.             }
  749.  
  750.             // could also try an intersect operation rather than a union
  751.             // in other words, choose languages whose trigrams contain
  752.             // ALL of the unicode blocks found in this sample
  753.             // would improve speed but would be completely thrown off by an
  754.             // unexpected character, like an umlaut appearing in english text
  755.  
  756.             $possible_langs array_intersect(
  757.                 array_keys($this->_lang_db),
  758.                 array_unique($possible_langs)
  759.             );
  760.  
  761.             // needs to intersect it with the keys of _lang_db in case
  762.             // languages have been omitted
  763.  
  764.         else {
  765.             // or just try 'em all
  766.             $possible_langs array_keys($this->_lang_db);
  767.         }
  768.  
  769.  
  770.         foreach ($possible_langs as $lang{
  771.             $scores[$lang$this->_normalize_score(
  772.                 $this->_distance($this->_lang_db[$lang]$trigram_freqs),
  773.                 $trigram_count
  774.             );
  775.         }
  776.  
  777.         unset($sample_obj);
  778.  
  779.         if ($this->_perl_compatible{
  780.             asort($scores);
  781.         else {
  782.             arsort($scores);
  783.         }
  784.  
  785.         // todo: drop languages with a score of $this->_max_score?
  786.  
  787.         // limit the number of returned scores
  788.         if ($limit && is_numeric($limit)) {
  789.             $limited_scores = array();
  790.  
  791.             $i = 0;
  792.             foreach ($scores as $key => $value{
  793.                 if ($i++ >= $limit{
  794.                     break;
  795.                 }
  796.  
  797.                 $limited_scores[$key$value;
  798.             }
  799.  
  800.             return $this->_convertToNameMode($limited_scorestrue);
  801.         else {
  802.             return $this->_convertToNameMode($scorestrue);
  803.         }
  804.     }
  805.  
  806.     /**
  807.      * Returns only the most similar language to the text sample
  808.      *
  809.      * Calls $this->detect() and returns only the top result
  810.      *
  811.      * @param string $sample text to detect the language of
  812.      *
  813.      * @return string the name of the most likely language
  814.      *                 or null if no language is similar
  815.      * @see    detect()
  816.      * @throws Text_LanguageDetect_Exception
  817.      */
  818.     public function detectSimple($sample)
  819.     {
  820.         $scores $this->detect($sample1);
  821.  
  822.         // if top language has the maximum possible score,
  823.         // then the top score will have been picked at random
  824.         if (!is_array($scores|| empty($scores)
  825.             || current($scores== $this->_max_score
  826.         {
  827.             return null;
  828.         else {
  829.             return key($scores);
  830.         }
  831.     }
  832.  
  833.     /**
  834.      * Returns an array containing the most similar language and a confidence
  835.      * rating
  836.      *
  837.      * Confidence is a simple measure calculated from the similarity score
  838.      * minus the similarity score from the next most similar language
  839.      * divided by the highest possible score. Languages that have closely
  840.      * related cousins (e.g. Norwegian and Danish) should generally have lower
  841.      * confidence scores.
  842.      *
  843.      * The similarity score answers the question "How likely is the text the
  844.      * returned language regardless of the other languages considered?" The
  845.      * confidence score is one way of answering the question "how likely is the
  846.      * text the detected language relative to the rest of the language model
  847.      * set?"
  848.      *
  849.      * To see how similar languages are a priori, see languageSimilarity()
  850.      *
  851.      * @param string $sample text for which language will be detected
  852.      *
  853.      * @return array most similar language, score and confidence rating
  854.      *                or null if no language is similar
  855.      * @see    detect()
  856.      * @throws Text_LanguageDetect_Exception
  857.      */
  858.     public function detectConfidence($sample)
  859.     {
  860.         $scores $this->detect($sample2);
  861.  
  862.         // if most similar language has the max score, it
  863.         // will have been picked at random
  864.         if (!is_array($scores|| empty($scores)
  865.             || current($scores== $this->_max_score
  866.         {
  867.             return null;
  868.         }
  869.  
  870.         $arr['language'key($scores);
  871.         $arr['similarity'current($scores);
  872.         if (next($scores!== false// if false then no next element
  873.             // the goal is to return a higher value if the distance between
  874.             // the similarity of the first score and the second score is high
  875.  
  876.             if ($this->_perl_compatible{
  877.                 $arr['confidence'(current($scores$arr['similarity'])
  878.                     / $this->_max_score;
  879.  
  880.             else {
  881.                 $arr['confidence'$arr['similarity'current($scores);
  882.  
  883.             }
  884.  
  885.         else {
  886.             $arr['confidence'= null;
  887.         }
  888.  
  889.         return $arr;
  890.     }
  891.  
  892.     /**
  893.      * Returns the distribution of unicode blocks in a given utf8 string
  894.      *
  895.      * For the block name of a single char, use unicodeBlockName()
  896.      *
  897.      * @param string $str          input string. Must be ascii or utf8
  898.      * @param bool   $skip_symbols if true, skip ascii digits, symbols and
  899.      *                              non-printing characters. Includes spaces,
  900.      *                              newlines and common punctutation characters.
  901.      *
  902.      * @return array 
  903.      * @throws Text_LanguageDetect_Exception
  904.      */
  905.     public function detectUnicodeBlocks($str$skip_symbols)
  906.     {
  907.         $skip_symbols = (bool)$skip_symbols;
  908.         $str          = (string)$str;
  909.  
  910.         $sample_obj = new Text_LanguageDetect_Parser($str);
  911.         $sample_obj->prepareUnicode();
  912.         $sample_obj->prepareTrigram(false);
  913.         $sample_obj->setUnicodeSkipSymbols($skip_symbols);
  914.         $sample_obj->analyze();
  915.         $blocks $sample_obj->getUnicodeBlocks();
  916.         unset($sample_obj);
  917.         return $blocks;
  918.     }
  919.  
  920.     /**
  921.      * Returns the block name for a given unicode value
  922.      *
  923.      * If passed a string, will assume it is being passed a UTF8-formatted
  924.      * character and will automatically convert. Otherwise it will assume it
  925.      * is being passed a numeric unicode value.
  926.      *
  927.      * Make sure input is of the correct type!
  928.      *
  929.      * @param mixed $unicode unicode value or utf8 char
  930.      *
  931.      * @return mixed the block name string or false if not found
  932.      * @throws Text_LanguageDetect_Exception
  933.      */
  934.     public function unicodeBlockName($unicode)
  935.     {
  936.         if (is_string($unicode)) {
  937.             // assume it is being passed a utf8 char, so convert it
  938.             if (self::utf8strlen($unicode> 1{
  939.                 throw new Text_LanguageDetect_Exception(
  940.                     'Pass a single char only to this method',
  941.                     Text_LanguageDetect_Exception::PARAM_TYPE
  942.                 );
  943.             }
  944.             $unicode $this->_utf8char2unicode($unicode);
  945.  
  946.         elseif (!is_int($unicode)) {
  947.             throw new Text_LanguageDetect_Exception(
  948.                 'Input must be of type string or int.',
  949.                 Text_LanguageDetect_Exception::PARAM_TYPE
  950.             );
  951.         }
  952.  
  953.         $blocks $this->_read_unicode_block_db();
  954.  
  955.         $result $this->_unicode_block_name($unicode$blocks);
  956.  
  957.         if ($result == -1{
  958.             return false;
  959.         else {
  960.             return $result[2];
  961.         }
  962.     }
  963.  
  964.     /**
  965.      * Searches the unicode block database
  966.      *
  967.      * Returns the block name for a given unicode value. unicodeBlockName() is
  968.      * the public interface for this function, which does input checks which
  969.      * this function omits for speed.
  970.      *
  971.      * @param int   $unicode     the unicode value
  972.      * @param array $blocks      the block database
  973.      * @param int   $block_count the number of defined blocks in the database
  974.      *
  975.      * @return mixed Block name, -1 if it failed
  976.      * @see    unicodeBlockName()
  977.      * @access protected
  978.      */
  979.     function _unicode_block_name($unicode$blocks$block_count = -1)
  980.     {
  981.         // for a reference, see
  982.         // http://www.unicode.org/Public/UNIDATA/Blocks.txt
  983.  
  984.         // assume that ascii characters are the most common
  985.         // so try it first for efficiency
  986.         if ($unicode <= $blocks[0][1]{
  987.             return $blocks[0];
  988.         }
  989.  
  990.         // the optional $block_count param is for efficiency
  991.         // so we this function doesn't have to run count() every time
  992.         if ($block_count != -1{
  993.             $high $block_count - 1;
  994.         else {
  995.             $high count($blocks- 1;
  996.         }
  997.  
  998.         $low = 1; // start with 1 because ascii was 0
  999.  
  1000.         // your average binary search algorithm
  1001.         while ($low <= $high{
  1002.             $mid floor(($low $high/ 2);
  1003.  
  1004.             if ($unicode $blocks[$mid][0]{
  1005.                 // if it's lower than the lower bound
  1006.                 $high $mid - 1;
  1007.  
  1008.             elseif ($unicode $blocks[$mid][1]{
  1009.                 // if it's higher than the upper bound
  1010.                 $low $mid + 1;
  1011.  
  1012.             else {
  1013.                 // found it
  1014.                 return $blocks[$mid];
  1015.             }
  1016.         }
  1017.  
  1018.         // failed to find the block
  1019.         return -1;
  1020.  
  1021.         // todo: differentiate when it's out of range or when it falls
  1022.         //       into an unassigned range?
  1023.     }
  1024.  
  1025.     /**
  1026.      * Brings up the unicode block database
  1027.      *
  1028.      * @return array the database of unicode block definitions
  1029.      * @throws Text_LanguageDetect_Exception
  1030.      * @access protected
  1031.      */
  1032.     function _read_unicode_block_db()
  1033.     {
  1034.         // since the unicode definitions are always going to be the same,
  1035.         // might as well share the memory for the db with all other instances
  1036.         // of this class
  1037.         static $data;
  1038.  
  1039.         if (!isset($data)) {
  1040.             $data $this->_readdb($this->_unicode_db_filename);
  1041.         }
  1042.  
  1043.         return $data;
  1044.     }
  1045.  
  1046.     /**
  1047.      * Calculate the similarities between the language models
  1048.      *
  1049.      * Use this function to see how similar languages are to each other.
  1050.      *
  1051.      * If passed 2 language names, will return just those languages compared.
  1052.      * If passed 1 language name, will return that language compared to
  1053.      * all others.
  1054.      * If passed none, will return an array of every language model compared
  1055.      * to every other one.
  1056.      *
  1057.      * @param string $lang1 the name of the first language to be compared
  1058.      * @param string $lang2 the name of the second language to be compared
  1059.      *
  1060.      * @return array scores of every language compared
  1061.      *                or the score of just the provided languages
  1062.      *                or null if one of the supplied languages does not exist
  1063.      * @throws Text_LanguageDetect_Exception
  1064.      */
  1065.     public function languageSimilarity($lang1 = null$lang2 = null)
  1066.     {
  1067.         $lang1 $this->_convertFromNameMode($lang1);
  1068.         $lang2 $this->_convertFromNameMode($lang2);
  1069.         if ($lang1 != null{
  1070.             $lang1 strtolower($lang1);
  1071.  
  1072.             // check if language model exists
  1073.             if (!isset($this->_lang_db[$lang1])) {
  1074.                 return null;
  1075.             }
  1076.  
  1077.             if ($lang2 != null{
  1078.                 if (!isset($this->_lang_db[$lang2])) {
  1079.                     // check if language model exists
  1080.                     return null;
  1081.                 }
  1082.  
  1083.                 $lang2 strtolower($lang2);
  1084.  
  1085.                 // compare just these two languages
  1086.                 return $this->_normalize_score(
  1087.                     $this->_distance(
  1088.                         $this->_lang_db[$lang1],
  1089.                         $this->_lang_db[$lang2]
  1090.                     )
  1091.                 );
  1092.  
  1093.             else {
  1094.                 // compare just $lang1 to all languages
  1095.                 $return_arr = array();
  1096.                 foreach ($this->_lang_db as $key => $value{
  1097.                     if ($key != $lang1{
  1098.                         // don't compare a language to itself
  1099.                         $return_arr[$key$this->_normalize_score(
  1100.                             $this->_distance($this->_lang_db[$lang1]$value)
  1101.                         );
  1102.                     }
  1103.                 }
  1104.                 asort($return_arr);
  1105.  
  1106.                 return $return_arr;
  1107.             }
  1108.  
  1109.  
  1110.         else {
  1111.             // compare all languages to each other
  1112.             $return_arr = array();
  1113.             foreach (array_keys($this->_lang_dbas $lang1{
  1114.                 foreach (array_keys($this->_lang_dbas $lang2{
  1115.                     // skip comparing languages to themselves
  1116.                     if ($lang1 != $lang2{
  1117.  
  1118.                         if (isset($return_arr[$lang2][$lang1])) {
  1119.                             // don't re-calculate what's already been done
  1120.                             $return_arr[$lang1][$lang2]
  1121.                                 = $return_arr[$lang2][$lang1];
  1122.  
  1123.                         else {
  1124.                             // calculate
  1125.                             $return_arr[$lang1][$lang2]
  1126.                                 = $this->_normalize_score(
  1127.                                     $this->_distance(
  1128.                                         $this->_lang_db[$lang1],
  1129.                                         $this->_lang_db[$lang2]
  1130.                                     )
  1131.                                 );
  1132.  
  1133.                         }
  1134.                     }
  1135.                 }
  1136.             }
  1137.             return $return_arr;
  1138.         }
  1139.     }
  1140.  
  1141.     /**
  1142.      * Cluster known languages according to languageSimilarity()
  1143.      *
  1144.      * WARNING: this method is EXPERIMENTAL. It is not recommended for common
  1145.      * use, and it may disappear or its functionality may change in future
  1146.      * releases without notice.
  1147.      *
  1148.      * Uses a nearest neighbor technique to generate the maximum possible
  1149.      * number of dendograms from the similarity data.
  1150.      *
  1151.      * @access      public
  1152.      * @return      array language cluster data
  1153.      * @throws      Text_LanguageDetect_Exception
  1154.      * @see         languageSimilarity()
  1155.      * @deprecated  this function will eventually be removed and placed into
  1156.      *               the model generation class
  1157.      */
  1158.     function clusterLanguages()
  1159.     {
  1160.         // todo: set the maximum number of clusters
  1161.         // return cached result, if any
  1162.         if (isset($this->_clusters)) {
  1163.             return $this->_clusters;
  1164.         }
  1165.  
  1166.         $langs array_keys($this->_lang_db);
  1167.  
  1168.         $arr $this->languageSimilarity();
  1169.  
  1170.         sort($langs);
  1171.  
  1172.         foreach ($langs as $lang{
  1173.             if (!isset($this->_lang_db[$lang])) {
  1174.                 throw new Text_LanguageDetect_Exception(
  1175.                     "missing $lang!",
  1176.                     Text_LanguageDetect_Exception::UNKNOWN_LANGUAGE
  1177.                 );
  1178.             }
  1179.         }
  1180.  
  1181.         // http://www.psychstat.missouristate.edu/multibook/mlt04m.html
  1182.         foreach ($langs as $old_key => $lang1{
  1183.             $langs[$lang1$lang1;
  1184.             unset($langs[$old_key]);
  1185.         }
  1186.  
  1187.         $result_data $really_map = array();
  1188.  
  1189.         $i = 0;
  1190.         while (count($langs> 2 && $i++ < 200{
  1191.             $highest_score = -1;
  1192.             $highest_key1 '';
  1193.             $highest_key2 '';
  1194.             foreach ($langs as $lang1{
  1195.                 foreach ($langs as $lang2{
  1196.                     if ($lang1 != $lang2
  1197.                         && $arr[$lang1][$lang2$highest_score
  1198.                     {
  1199.                         $highest_score $arr[$lang1][$lang2];
  1200.                         $highest_key1 $lang1;
  1201.                         $highest_key2 $lang2;
  1202.                     }
  1203.                 }
  1204.             }
  1205.  
  1206.             if (!$highest_key1{
  1207.                 // should not ever happen
  1208.                 throw new Text_LanguageDetect_Exception(
  1209.                     "no highest key? (step: $i)",
  1210.                     Text_LanguageDetect_Exception::NO_HIGHEST_KEY
  1211.                 );
  1212.             }
  1213.  
  1214.             if ($highest_score == 0{
  1215.                 // languages are perfectly dissimilar
  1216.                 break;
  1217.             }
  1218.  
  1219.             // $highest_key1 and $highest_key2 are most similar
  1220.             $sum1 array_sum($arr[$highest_key1]);
  1221.             $sum2 array_sum($arr[$highest_key2]);
  1222.  
  1223.             // use the score for the one that is most similar to the rest of
  1224.             // the field as the score for the group
  1225.             // todo: could try averaging or "centroid" method instead
  1226.             // seems like that might make more sense
  1227.             // actually nearest neighbor may be better for binary searching
  1228.  
  1229.  
  1230.             // for "Complete Linkage"/"furthest neighbor"
  1231.             // sign should be <
  1232.             // for "Single Linkage"/"nearest neighbor" method
  1233.             // should should be >
  1234.             // results seem to be pretty much the same with either method
  1235.  
  1236.             // figure out which to delete and which to replace
  1237.             if ($sum1 $sum2{
  1238.                 $replaceme $highest_key1;
  1239.                 $deleteme $highest_key2;
  1240.             else {
  1241.                 $replaceme $highest_key2;
  1242.                 $deleteme $highest_key1;
  1243.             }
  1244.  
  1245.             $newkey $replaceme ':' $deleteme;
  1246.  
  1247.             // $replaceme is most similar to remaining languages
  1248.             // replace $replaceme with '$newkey', deleting $deleteme
  1249.  
  1250.             // keep a record of which fork is really which language
  1251.             $really_lang $replaceme;
  1252.             while (isset($really_map[$really_lang])) {
  1253.                 $really_lang $really_map[$really_lang];
  1254.             }
  1255.             $really_map[$newkey$really_lang;
  1256.  
  1257.  
  1258.             // replace the best fitting key, delete the other
  1259.             foreach ($arr as $key1 => $arr2{
  1260.                 foreach ($arr2 as $key2 => $value2{
  1261.                     if ($key2 == $replaceme{
  1262.                         $arr[$key1][$newkey$arr[$key1][$key2];
  1263.                         unset($arr[$key1][$key2]);
  1264.                         // replacing $arr[$key1][$key2] with $arr[$key1][$newkey]
  1265.                     }
  1266.  
  1267.                     if ($key1 == $replaceme{
  1268.                         $arr[$newkey][$key2$arr[$key1][$key2];
  1269.                         unset($arr[$key1][$key2]);
  1270.                         // replacing $arr[$key1][$key2] with $arr[$newkey][$key2]
  1271.                     }
  1272.  
  1273.                     if ($key1 == $deleteme || $key2 == $deleteme{
  1274.                         // deleting $arr[$key1][$key2]
  1275.                         unset($arr[$key1][$key2]);
  1276.                     }
  1277.                 }
  1278.             }
  1279.  
  1280.  
  1281.             unset($langs[$highest_key1]);
  1282.             unset($langs[$highest_key2]);
  1283.             $langs[$newkey$newkey;
  1284.  
  1285.  
  1286.             // some of these may be overkill
  1287.             $result_data[$newkey= array(
  1288.                                 'newkey' => $newkey,
  1289.                                 'count' => $i,
  1290.                                 'diff' => abs($sum1 $sum2),
  1291.                                 'score' => $highest_score,
  1292.                                 'bestfit' => $replaceme,
  1293.                                 'otherfit' => $deleteme,
  1294.                                 'really' => $really_lang,
  1295.                             );
  1296.         }
  1297.  
  1298.         $return_val = array(
  1299.                 'open_forks' => $langs,
  1300.                         // the top level of clusters
  1301.                         // clusters that are mutually exclusive
  1302.                         // or specified by a specific maximum
  1303.  
  1304.                 'fork_data' => $result_data,
  1305.                         // data for each split
  1306.  
  1307.                 'name_map' => $really_map,
  1308.                         // which cluster is really which language
  1309.                         // using the nearest neighbor technique, the cluster
  1310.                         // inherits all of the properties of its most-similar member
  1311.                         // this keeps track
  1312.             );
  1313.  
  1314.  
  1315.         // saves the result in the object
  1316.         $this->_clusters $return_val;
  1317.  
  1318.         return $return_val;
  1319.     }
  1320.  
  1321.  
  1322.     /**
  1323.      * Perform an intelligent detection based on clusterLanguages()
  1324.      *
  1325.      * WARNING: this method is EXPERIMENTAL. It is not recommended for common
  1326.      * use, and it may disappear or its functionality may change in future
  1327.      * releases without notice.
  1328.      *
  1329.      * This compares the sample text to top the top level of clusters. If the
  1330.      * sample is similar to the cluster it will drop down and compare it to the
  1331.      * languages in the cluster, and so on until it hits a leaf node.
  1332.      *
  1333.      * this should find the language in considerably fewer compares
  1334.      * (the equivalent of a binary search), however clusterLanguages() is costly
  1335.      * and the loss of accuracy from this technique is significant.
  1336.      *
  1337.      * This method may need to be 'fuzzier' in order to become more accurate.
  1338.      *
  1339.      * This function could be more useful if the universe of possible languages
  1340.      * was very large, however in such cases some method of Bayesian inference
  1341.      * might be more helpful.
  1342.      *
  1343.      * @param string $str input string
  1344.      *
  1345.      * @return array language scores (only those compared)
  1346.      * @throws Text_LanguageDetect_Exception
  1347.      * @see    clusterLanguages()
  1348.      */
  1349.     public function clusteredSearch($str)
  1350.     {
  1351.         // input check
  1352.         if (!Text_LanguageDetect_Parser::validateString($str)) {
  1353.             return array();
  1354.         }
  1355.  
  1356.         // clusterLanguages() will return a cached result if possible
  1357.         // so it's safe to call it every time
  1358.         $result $this->clusterLanguages();
  1359.  
  1360.         $dendogram_start $result['open_forks'];
  1361.         $dendogram_data  $result['fork_data'];
  1362.         $dendogram_alias $result['name_map'];
  1363.  
  1364.         $sample_obj = new Text_LanguageDetect_Parser($str);
  1365.         $sample_obj->prepareTrigram();
  1366.         $sample_obj->setPadStart(!$this->_perl_compatible);
  1367.         $sample_obj->analyze();
  1368.         $sample_result $sample_obj->getTrigramRanks();
  1369.         $sample_count  count($sample_result);
  1370.  
  1371.         // input check
  1372.         if ($sample_count == 0{
  1373.             return array();
  1374.         }
  1375.  
  1376.         $i = 0; // counts the number of steps
  1377.  
  1378.         foreach ($dendogram_start as $lang{
  1379.             if (isset($dendogram_alias[$lang])) {
  1380.                 $lang_key $dendogram_alias[$lang];
  1381.             else {
  1382.                 $lang_key $lang;
  1383.             }
  1384.  
  1385.             $scores[$lang$this->_normalize_score(
  1386.                 $this->_distance($this->_lang_db[$lang_key]$sample_result),
  1387.                 $sample_count
  1388.             );
  1389.  
  1390.             $i++;
  1391.         }
  1392.  
  1393.         if ($this->_perl_compatible{
  1394.             asort($scores);
  1395.         else {
  1396.             arsort($scores);
  1397.         }
  1398.  
  1399.         $top_score current($scores);
  1400.         $top_key key($scores);
  1401.  
  1402.         // of starting forks, $top_key is the most similar to the sample
  1403.  
  1404.         $cur_key $top_key;
  1405.         while (isset($dendogram_data[$cur_key])) {
  1406.             $lang1 $dendogram_data[$cur_key]['bestfit'];
  1407.             $lang2 $dendogram_data[$cur_key]['otherfit'];
  1408.             foreach (array($lang1$lang2as $lang{
  1409.                 if (isset($dendogram_alias[$lang])) {
  1410.                     $lang_key $dendogram_alias[$lang];
  1411.                 else {
  1412.                     $lang_key $lang;
  1413.                 }
  1414.  
  1415.                 $scores[$lang$this->_normalize_score(
  1416.                     $this->_distance($this->_lang_db[$lang_key]$sample_result),
  1417.                     $sample_count
  1418.                 );
  1419.  
  1420.                 //todo: does not need to do same comparison again
  1421.             }
  1422.  
  1423.             $i++;
  1424.  
  1425.             if ($scores[$lang1$scores[$lang2]{
  1426.                 $cur_key $lang1;
  1427.                 $loser_key $lang2;
  1428.             else {
  1429.                 $cur_key $lang2;
  1430.                 $loser_key $lang1;
  1431.             }
  1432.  
  1433.             $diff $scores[$cur_key$scores[$loser_key];
  1434.  
  1435.             // $cur_key ({$dendogram_alias[$cur_key]}) wins
  1436.             // over $loser_key ({$dendogram_alias[$loser_key]})
  1437.             // with a difference of $diff
  1438.         }
  1439.  
  1440.         // found result in $i compares
  1441.  
  1442.         // rather than sorting the result, preserve it so that you can see
  1443.         // which paths the algorithm decided to take along the tree
  1444.  
  1445.         // but sometimes the last item is only the second highest
  1446.         if (($this->_perl_compatible  && (end($scoresprev($scores)))
  1447.             || (!$this->_perl_compatible && (end($scoresprev($scores)))
  1448.         {
  1449.             $real_last_score current($scores);
  1450.             $real_last_key key($scores);
  1451.  
  1452.             // swaps the 2nd-to-last item for the last item
  1453.             unset($scores[$real_last_key]);
  1454.             $scores[$real_last_key$real_last_score;
  1455.         }
  1456.  
  1457.  
  1458.         if (!$this->_perl_compatible{
  1459.             $scores array_reverse($scorestrue);
  1460.             // second param requires php > 4.0.3
  1461.         }
  1462.  
  1463.         return $scores;
  1464.     }
  1465.  
  1466.     /**
  1467.      * ut8-safe strlen()
  1468.      *
  1469.      * Returns the numbers of characters (not bytes) in a utf8 string
  1470.      *
  1471.      * @param string $str string to get the length of
  1472.      *
  1473.      * @return int number of chars
  1474.      */
  1475.     public static function utf8strlen($str)
  1476.     {
  1477.         // utf8_decode() will convert unknown chars to '?', which is actually
  1478.         // ideal for counting.
  1479.  
  1480.         return strlen(utf8_decode($str));
  1481.  
  1482.         // idea stolen from dokuwiki
  1483.     }
  1484.  
  1485.     /**
  1486.      * Returns the unicode value of a utf8 char
  1487.      *
  1488.      * @param string $char a utf8 (possibly multi-byte) char
  1489.      *
  1490.      * @return int unicode value
  1491.      * @access protected
  1492.      * @link   http://en.wikipedia.org/wiki/UTF-8
  1493.      */
  1494.     function _utf8char2unicode($char)
  1495.     {
  1496.         // strlen() here will actually get the binary length of a single char
  1497.         switch (strlen($char)) {
  1498.         case 1:
  1499.             // normal ASCII-7 byte
  1500.             // 0xxxxxxx -->  0xxxxxxx
  1501.             return ord($char{0});
  1502.  
  1503.         case 2:
  1504.             // 2 byte unicode
  1505.             // 110zzzzx 10xxxxxx --> 00000zzz zxxxxxxx
  1506.             $z (ord($char{0}0x000001F<< 6;
  1507.             $x (ord($char{1}0x0000003F);
  1508.             return ($z $x);
  1509.  
  1510.         case 3:
  1511.             // 3 byte unicode
  1512.             // 1110zzzz 10zxxxxx 10xxxxxx --> zzzzzxxx xxxxxxxx
  1513.             $z =  (ord($char{0}0x0000000F<< 12;
  1514.             $x1 (ord($char{1}0x0000003F<< 6;
  1515.             $x2 (ord($char{2}0x0000003F);
  1516.             return ($z $x1 $x2);
  1517.  
  1518.         case 4:
  1519.             // 4 byte unicode
  1520.             // 11110zzz 10zzxxxx 10xxxxxx 10xxxxxx -->
  1521.             // 000zzzzz xxxxxxxx xxxxxxxx
  1522.             $z1 (ord($char{0}0x00000007<< 18;
  1523.             $z2 (ord($char{1}0x0000003F<< 12;
  1524.             $x1 (ord($char{2}0x0000003F<< 6;
  1525.             $x2 (ord($char{3}0x0000003F);
  1526.             return ($z1 $z2 $x1 $x2);
  1527.         }
  1528.     }
  1529.  
  1530.     /**
  1531.      * utf8-safe fast character iterator
  1532.      *
  1533.      * Will get the next character starting from $counter, which will then be
  1534.      * incremented. If a multi-byte char the bytes will be concatenated and
  1535.      * $counter will be incremeted by the number of bytes in the char.
  1536.      *
  1537.      * @param string $str             the string being iterated over
  1538.      * @param int    &$counter        the iterator, will increment by reference
  1539.      * @param bool   $special_convert whether to do special conversions
  1540.      *
  1541.      * @return char the next (possibly multi-byte) char from $counter
  1542.      * @access private
  1543.      */
  1544.     static function _next_char($str&$counter$special_convert = false)
  1545.     {
  1546.         $char $str{$counter++};
  1547.         $ord ord($char);
  1548.  
  1549.         // for a description of the utf8 system see
  1550.         // http://www.phpclasses.org/browse/file/5131.html
  1551.  
  1552.         // normal ascii one byte char
  1553.         if ($ord <= 127{
  1554.             // special conversions needed for this package
  1555.             // (that only apply to regular ascii characters)
  1556.             // lower case, and convert all non-alphanumeric characters
  1557.             // other than "'" to space
  1558.             if ($special_convert && $char != ' ' && $char != "'"{
  1559.                 if ($ord >= 65 && $ord <= 90// A-Z
  1560.                     $char chr($ord + 32)// lower case
  1561.                 elseif ($ord < 97 || $ord > 122// NOT a-z
  1562.                     $char ' '// convert to space
  1563.                 }
  1564.             }
  1565.  
  1566.             return $char;
  1567.  
  1568.         elseif ($ord >> 5 == 6// two-byte char
  1569.             // multi-byte chars
  1570.             $nextchar $str{$counter++}// get next byte
  1571.  
  1572.             // lower-casing of non-ascii characters is still incomplete
  1573.  
  1574.             if ($special_convert{
  1575.                 // lower case latin accented characters
  1576.                 if ($ord == 195{
  1577.                     $nextord ord($nextchar);
  1578.                     $nextord_adj $nextord + 64;
  1579.                     // for a reference, see
  1580.                     // http://www.ramsch.org/martin/uni/fmi-hp/iso8859-1.html
  1581.  
  1582.                     // &Agrave; - &THORN; but not &times;
  1583.                     if ($nextord_adj >= 192
  1584.                         && $nextord_adj <= 222
  1585.                         && $nextord_adj != 215
  1586.                     {
  1587.                         $nextchar chr($nextord + 32);
  1588.                     }
  1589.  
  1590.                 elseif ($ord == 208{
  1591.                     // lower case cyrillic alphabet
  1592.                     $nextord ord($nextchar);
  1593.                     // if A - Pe
  1594.                     if ($nextord >= 144 && $nextord <= 159{
  1595.                         // lower case
  1596.                         $nextchar chr($nextord + 32);
  1597.  
  1598.                     elseif ($nextord >= 160 && $nextord <= 175{
  1599.                         // if Er - Ya
  1600.                         // lower case
  1601.                         $char chr(209)// == $ord++
  1602.                         $nextchar chr($nextord - 32);
  1603.                     }
  1604.                 }
  1605.             }
  1606.  
  1607.             // tag on next byte
  1608.             return $char $nextchar;
  1609.         elseif ($ord >> 4  == 14// three-byte char
  1610.  
  1611.             // tag on next 2 bytes
  1612.             return $char $str{$counter++$str{$counter++};
  1613.  
  1614.         elseif ($ord >> 3 == 30// four-byte char
  1615.  
  1616.             // tag on next 3 bytes
  1617.             return $char $str{$counter++$str{$counter++$str{$counter++};
  1618.  
  1619.         else {
  1620.             // error?
  1621.         }
  1622.     }
  1623.  
  1624.     /**
  1625.      * Converts an $language input parameter from the configured mode
  1626.      * to the language name that is used internally.
  1627.      *
  1628.      * Works for strings and arrays.
  1629.      *
  1630.      * @param string|array$lang       A language description ("english"/"en"/"eng")
  1631.      * @param boolean      $convertKey If $lang is an array, setting $key
  1632.      *                                  converts the keys to the language name.
  1633.      *
  1634.      * @return string|arrayLanguage name
  1635.      */
  1636.     function _convertFromNameMode($lang$convertKey = false)
  1637.     {
  1638.         if ($this->_name_mode == 0{
  1639.             return $lang;
  1640.         }
  1641.  
  1642.         if ($this->_name_mode == 2{
  1643.             $method 'code2ToName';
  1644.         else {
  1645.             $method 'code3ToName';
  1646.         }
  1647.  
  1648.         if (is_string($lang)) {
  1649.             return (string)Text_LanguageDetect_ISO639::$method($lang);
  1650.         }
  1651.  
  1652.         $newlang = array();
  1653.         foreach ($lang as $key => $val{
  1654.             if ($convertKey{
  1655.                 $newkey = (string)Text_LanguageDetect_ISO639::$method($key);
  1656.                 $newlang[$newkey$val;
  1657.             else {
  1658.                 $newlang[$key= (string)Text_LanguageDetect_ISO639::$method($val);
  1659.             }
  1660.         }
  1661.         return $newlang;
  1662.     }
  1663.  
  1664.     /**
  1665.      * Converts an $language output parameter from the language name that is
  1666.      * used internally to the configured mode.
  1667.      *
  1668.      * Works for strings and arrays.
  1669.      *
  1670.      * @param string|array$lang       A language description ("english"/"en"/"eng")
  1671.      * @param boolean      $convertKey If $lang is an array, setting $key
  1672.      *                                  converts the keys to the language name.
  1673.      *
  1674.      * @return string|arrayLanguage name
  1675.      */
  1676.     function _convertToNameMode($lang$convertKey = false)
  1677.     {
  1678.         if ($this->_name_mode == 0{
  1679.             return $lang;
  1680.         }
  1681.  
  1682.         if ($this->_name_mode == 2{
  1683.             $method 'nameToCode2';
  1684.         else {
  1685.             $method 'nameToCode3';
  1686.         }
  1687.  
  1688.         if (is_string($lang)) {
  1689.             return Text_LanguageDetect_ISO639::$method($lang);
  1690.         }
  1691.  
  1692.         $newlang = array();
  1693.         foreach ($lang as $key => $val{
  1694.             if ($convertKey{
  1695.                 $newkey = Text_LanguageDetect_ISO639::$method($key);
  1696.                 $newlang[$newkey$val;
  1697.             else {
  1698.                 $newlang[$key= Text_LanguageDetect_ISO639::$method($val);
  1699.             }
  1700.         }
  1701.         return $newlang;
  1702.     }
  1703. }
  1704.  
  1705. /* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
  1706.  
  1707. ?>

Documentation generated on Mon, 16 Jan 2012 10:00:04 +0000 by phpDocumentor 1.4.3. PEAR Logo Copyright © PHP Group 2004.