NaturalSort.php 3.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121
  1. <?php
  2. /**
  3. * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
  4. * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
  5. * SPDX-License-Identifier: AGPL-3.0-only
  6. */
  7. namespace OC;
  8. use Psr\Log\LoggerInterface;
  9. class NaturalSort {
  10. private static $instance;
  11. private $collator;
  12. private $cache = [];
  13. /**
  14. * Instantiate a new \OC\NaturalSort instance.
  15. * @param object $injectedCollator
  16. */
  17. public function __construct($injectedCollator = null) {
  18. // inject an instance of \Collator('en_US') to force using the php5-intl Collator
  19. // or inject an instance of \OC\NaturalSort_DefaultCollator to force using Owncloud's default collator
  20. if (isset($injectedCollator)) {
  21. $this->collator = $injectedCollator;
  22. \OC::$server->get(LoggerInterface::class)->debug('forced use of ' . get_class($injectedCollator));
  23. }
  24. }
  25. /**
  26. * Split the given string in chunks of numbers and strings
  27. * @param string $t string
  28. * @return array of strings and number chunks
  29. */
  30. private function naturalSortChunkify($t) {
  31. // Adapted and ported to PHP from
  32. // http://my.opera.com/GreyWyvern/blog/show.dml/1671288
  33. if (isset($this->cache[$t])) {
  34. return $this->cache[$t];
  35. }
  36. $tz = [];
  37. $x = 0;
  38. $y = -1;
  39. $n = null;
  40. while (isset($t[$x])) {
  41. $c = $t[$x];
  42. // only include the dot in strings
  43. $m = ((!$n && $c === '.') || ($c >= '0' && $c <= '9'));
  44. if ($m !== $n) {
  45. // next chunk
  46. $y++;
  47. $tz[$y] = '';
  48. $n = $m;
  49. }
  50. $tz[$y] .= $c;
  51. $x++;
  52. }
  53. $this->cache[$t] = $tz;
  54. return $tz;
  55. }
  56. /**
  57. * Returns the string collator
  58. * @return \Collator string collator
  59. */
  60. private function getCollator() {
  61. if (!isset($this->collator)) {
  62. // looks like the default is en_US_POSIX which yields wrong sorting with
  63. // German umlauts, so using en_US instead
  64. if (class_exists('Collator')) {
  65. $this->collator = new \Collator('en_US');
  66. } else {
  67. $this->collator = new \OC\NaturalSort_DefaultCollator();
  68. }
  69. }
  70. return $this->collator;
  71. }
  72. /**
  73. * Compare two strings to provide a natural sort
  74. * @param string $a first string to compare
  75. * @param string $b second string to compare
  76. * @return int -1 if $b comes before $a, 1 if $a comes before $b
  77. * or 0 if the strings are identical
  78. */
  79. public function compare($a, $b) {
  80. // Needed because PHP doesn't sort correctly when numbers are enclosed in
  81. // parenthesis, even with NUMERIC_COLLATION enabled.
  82. // For example it gave ["test (2).txt", "test.txt"]
  83. // instead of ["test.txt", "test (2).txt"]
  84. $aa = self::naturalSortChunkify($a);
  85. $bb = self::naturalSortChunkify($b);
  86. for ($x = 0; isset($aa[$x]) && isset($bb[$x]); $x++) {
  87. $aChunk = $aa[$x];
  88. $bChunk = $bb[$x];
  89. if ($aChunk !== $bChunk) {
  90. // test first character (character comparison, not number comparison)
  91. if ($aChunk[0] >= '0' && $aChunk[0] <= '9' && $bChunk[0] >= '0' && $bChunk[0] <= '9') {
  92. $aNum = (int)$aChunk;
  93. $bNum = (int)$bChunk;
  94. return $aNum - $bNum;
  95. }
  96. return self::getCollator()->compare($aChunk, $bChunk);
  97. }
  98. }
  99. return count($aa) - count($bb);
  100. }
  101. /**
  102. * Returns a singleton
  103. * @return \OC\NaturalSort instance
  104. */
  105. public static function getInstance() {
  106. if (!isset(self::$instance)) {
  107. self::$instance = new \OC\NaturalSort();
  108. }
  109. return self::$instance;
  110. }
  111. }