self::OFF, self::ALTERNATE_HANDLING => self::NON_IGNORABLE, self::CASE_FIRST => self::OFF, self::CASE_LEVEL => self::OFF, self::NORMALIZATION_MODE => self::OFF, self::STRENGTH => self::DEFAULT_STRENGTH, self::HIRAGANA_QUATERNARY_MODE => self::OFF, self::NUMERIC_COLLATION => self::OFF ]; /** * Possible values for attributes. * @see \Collator */ private const ATTRIBUTES_POSSIBLE_VALUES = [ self::FRENCH_COLLATION => [self::ON, self::OFF, self::DEFAULT_VALUE], self::ALTERNATE_HANDLING => [self::NON_IGNORABLE, self::SHIFTED, self::DEFAULT_VALUE], self::CASE_FIRST => [self::OFF, self::LOWER_FIRST, self::UPPER_FIRST, self::DEFAULT_VALUE], self::CASE_LEVEL => [self::OFF, self::ON, self::DEFAULT_VALUE], self::NORMALIZATION_MODE => [self::OFF, self::ON, self::DEFAULT_VALUE], self::STRENGTH => [self::PRIMARY, self::SECONDARY, self::TERTIARY, self::QUATERNARY, self::IDENTICAL, self::DEFAULT_STRENGTH], self::HIRAGANA_QUATERNARY_MODE => [self::OFF, self::ON, self::DEFAULT_VALUE], self::NUMERIC_COLLATION => [self::OFF, self::ON, self::DEFAULT_VALUE], ]; private $attributes = []; private $locale; /** * @see \Collator::__construct() */ public function __construct(string $locale) { $this->locale = $locale; // Set the default values for the attributes foreach(self::ATTRIBUTES_DEFAULT_VALUES as $attribute => $default_value) { $this->setAttribute($attribute, $default_value); } } /** * @see \Collator::asort() */ public function asort(array &$array, int $flags = self::SORT_REGULAR): bool { return $this->genericSort(true, $array, $flags); } /** * @see \Collator::sort() */ public function sort(array &$array, int $flags = self::SORT_REGULAR): bool { return $this->genericSort(false, $array, $flags); } /** * @return int<-1,1>|false * @see \Collator::compare() */ public function compare(string $string1, string $string2) { // Primary and secondary strengths are case-insensitive. The sort() method in this class // cannot perform a locale aware, case-insensitive sort, so make the two strings the same // case here, before trying the sort. if (in_array($this->getStrength(), [self::PRIMARY, self::SECONDARY], true)) { $string1 = mb_strtolower($string1); $string2 = mb_strtolower($string2); } // Trivial case if ($string1 === $string2) { return 0; } // Sort the array. If the order is reversed, then $string1 > $string2. // (When this function is being used as a callback for usort, and if the original array // is sorted in ascending order - which it well might be if it's the result of // an SQL query with an ORDER BY - then it's fastest to test for $string1 > $string2 // first, as below.) $original_array = [$string1, $string2]; $array = $original_array; $this->sort($array); if ($array !== $original_array) { return 1; } // Otherwise, flip the array and try again. If the order is reversed, then $string2 > $string1. $original_array = [$string2, $string1]; $array = $original_array; $this->sort($array); if ($array !== $original_array) { return -1; } // Otherwise they must be equal return 0; } /** * @see \Collator::create() */ public static function create(string $locale): ?self { return new self($locale); } /** * @return int|false * @see \Collator::getAttribute() */ public function getAttribute(int $attribute) { if (!array_key_exists($attribute, $this->attributes)) { return false; } return $this->attributes[$attribute]; } /** * @return int|false * @see \Collator::getErrorCode() */ public function getErrorCode() { throw new Exception("Not yet implemented"); } /** * @return string|false * @see \Collator::getErrorMessage() */ public function getErrorMessage() { throw new Exception("Not yet implemented"); } /** * @return string|false * @see \Collator::getLocale() */ public function getLocale() { throw new Exception("Not yet implemented"); } /** * @return string|false * @see \Collator::getSortKey() */ public function getSortKey(string $string) { throw new Exception("Not yet implemented"); } /** * @see \Collator::getStrength() */ public function getStrength(): int { return $this->getAttribute(self::STRENGTH); } /** * @see \Collator::setAttribute() */ public function setAttribute(int $attribute, int $value): bool { if (!in_array($value, self::ATTRIBUTES_POSSIBLE_VALUES[$attribute])) { return false; } // TODO: The manual (https://www.php.net/manual/en/class.collator.php#collator.constants.french-collation) // TODO: says that FRENCH_COLLATION "is automatically set to On for the French locales and a few others". // TODO: However, this doesn't seem to be the case in testing: it's always Off. Probably doesn't matter // TODO: in practice though as this emulator won't be able to do anything about it anyway. $this->attributes[$attribute] = ($value === self::DEFAULT_VALUE) ? self::ATTRIBUTES_DEFAULT_VALUES[$attribute] : $value; return true; } /** * @see \Collator::setStrength() * @return true */ public function setStrength(int $strength) { $this->setAttribute(self::STRENGTH, $strength); return true; } /** * @see \Collator::sortWithSortKeys() */ public function sortWithSortKeys(array &$array): bool { throw new Exception("Not yet implemented"); } private function genericSort(bool $maintain_index_association, array &$array, int $flags = self::SORT_REGULAR): bool { $locale_switcher = new LocaleSwitcher(LC_COLLATE, $this->locale); $locale_switcher->switch(); // Convert the flags to the equivalent value for the ordinary functions sort()/asort(). // Note that the sort()/asort() flags can be just one of SORT_REGULAR, SORT_NUMERIC, SORT_STRING, SORT_LOCALE_STRING or SORT_NATURAL. // Then SORT_FLAG_CASE can be combined with SORT_STRING or SORT_NATURAL. // This means that the ordinary PHP sort() functions do not support locale-aware natural or case-insensitive sorting. switch ($flags) { case self::SORT_STRING: case self::SORT_REGULAR: // If NUMERIC_COLLATION is on, then use SORT_NATURAL, otherwise use SORT_LOCALE_STRING $ordinary_flags = ($this->getAttribute(self::NUMERIC_COLLATION) === self::ON) ? SORT_NATURAL : SORT_LOCALE_STRING; break; case self::SORT_NUMERIC: $ordinary_flags = SORT_NUMERIC; break; default: throw new \InvalidArgumentException("Invalid flags value '$flags'"); break; } // Primary and secondary strengths are case-insensitive. // SORT_FLAG_CASE can only be used with SORT_STRING or SORT_NATURAL. if (in_array($ordinary_flags, [SORT_STRING, SORT_NATURAL]) && in_array($this->getStrength(), [self::PRIMARY, self::SECONDARY], true)) { $ordinary_flags |= SORT_FLAG_CASE; } if ($maintain_index_association) { asort($array, $ordinary_flags); } else { sort($array, $ordinary_flags); } $locale_switcher->restore(); return true; } }