aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/shirabe-php-shim/src/string.rs
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-06-21 20:12:28 +0900
committernsfisis <nsfisis@gmail.com>2026-06-21 20:12:28 +0900
commitaa270b53045cc1ce779447f4f19448236e9c4c67 (patch)
tree635fbbeb6699f5bd4b831c4d850211ff2428a209 /crates/shirabe-php-shim/src/string.rs
parent4575b55e7769e14d2d88d79ec97e9939c978ed9a (diff)
downloadphp-shirabe-aa270b53045cc1ce779447f4f19448236e9c4c67.tar.gz
php-shirabe-aa270b53045cc1ce779447f4f19448236e9c4c67.tar.zst
php-shirabe-aa270b53045cc1ce779447f4f19448236e9c4c67.zip
feat(php-shim): implement array sort/merge/unique/splice helpers
Implement array_unique, array_splice, array_merge_recursive, krsort, ksort, asort, sort_with_flags and sort_natural_flag_case, plus the strnatcmp/strnatcasecmp port they rely on. Drop array_key_last and array_splice_mixed in favour of existing helpers at the call sites. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Diffstat (limited to 'crates/shirabe-php-shim/src/string.rs')
-rw-r--r--crates/shirabe-php-shim/src/string.rs151
1 files changed, 147 insertions, 4 deletions
diff --git a/crates/shirabe-php-shim/src/string.rs b/crates/shirabe-php-shim/src/string.rs
index 8b6f361..0521ad4 100644
--- a/crates/shirabe-php-shim/src/string.rs
+++ b/crates/shirabe-php-shim/src/string.rs
@@ -153,8 +153,8 @@ pub fn strpbrk(haystack: &str, char_list: &str) -> Option<String> {
None
}
-pub fn strnatcasecmp(_s1: &str, _s2: &str) -> i64 {
- todo!()
+pub fn strnatcasecmp(s1: &str, s2: &str) -> i64 {
+ strnatcmp_ex(s1.as_bytes(), s2.as_bytes(), true)
}
pub fn strrpos(_haystack: &str, _needle: &str) -> Option<usize> {
@@ -204,8 +204,151 @@ pub fn strcmp(_s1: &str, _s2: &str) -> i64 {
_s1.cmp(_s2) as i64
}
-pub fn strnatcmp(_s1: &str, _s2: &str) -> i64 {
- todo!()
+pub fn strnatcmp(s1: &str, s2: &str) -> i64 {
+ strnatcmp_ex(s1.as_bytes(), s2.as_bytes(), false)
+}
+
+// Port of PHP's strnatcmp_ex (ext/standard/strnatcmp.c). Operating on byte
+// slices, an out-of-range index reads as 0, reproducing the NUL terminator that
+// the C implementation relies on.
+fn strnatcmp_ex(a: &[u8], b: &[u8], fold_case: bool) -> i64 {
+ let a_len = a.len();
+ let b_len = b.len();
+ if a_len == 0 || b_len == 0 {
+ return match a_len.cmp(&b_len) {
+ std::cmp::Ordering::Less => -1,
+ std::cmp::Ordering::Greater => 1,
+ std::cmp::Ordering::Equal => 0,
+ };
+ }
+
+ let mut ap = 0usize;
+ let mut bp = 0usize;
+ let mut leading = true;
+ loop {
+ let mut ca = natcmp_at(a, ap);
+ let mut cb = natcmp_at(b, bp);
+
+ // Skip over leading zeros.
+ while leading && ca == b'0' && natcmp_at(a, ap + 1).is_ascii_digit() {
+ ap += 1;
+ ca = natcmp_at(a, ap);
+ }
+ while leading && cb == b'0' && natcmp_at(b, bp + 1).is_ascii_digit() {
+ bp += 1;
+ cb = natcmp_at(b, bp);
+ }
+ leading = false;
+
+ // Skip consecutive whitespace.
+ while natcmp_is_space(ca) {
+ ap += 1;
+ ca = natcmp_at(a, ap);
+ }
+ while natcmp_is_space(cb) {
+ bp += 1;
+ cb = natcmp_at(b, bp);
+ }
+
+ // Process a run of digits.
+ if ca.is_ascii_digit() && cb.is_ascii_digit() {
+ let fractional = ca == b'0' || cb == b'0';
+ let result = if fractional {
+ natcmp_compare_left(a, &mut ap, b, &mut bp)
+ } else {
+ natcmp_compare_right(a, &mut ap, b, &mut bp)
+ };
+ if result != 0 {
+ return result;
+ }
+ }
+
+ if ap == a_len && bp == b_len {
+ return 0;
+ } else if ap == a_len {
+ return -1;
+ } else if bp == b_len {
+ return 1;
+ }
+
+ if fold_case {
+ ca = natcmp_at(a, ap).to_ascii_uppercase();
+ cb = natcmp_at(b, bp).to_ascii_uppercase();
+ } else {
+ ca = natcmp_at(a, ap);
+ cb = natcmp_at(b, bp);
+ }
+
+ if ca < cb {
+ return -1;
+ } else if ca > cb {
+ return 1;
+ }
+
+ ap += 1;
+ bp += 1;
+ }
+}
+
+fn natcmp_at(s: &[u8], i: usize) -> u8 {
+ if i < s.len() { s[i] } else { 0 }
+}
+
+fn natcmp_is_space(c: u8) -> bool {
+ matches!(c, b' ' | b'\t' | b'\n' | 0x0b | 0x0c | b'\r')
+}
+
+// Compare two right-aligned numbers: the longest run of digits wins; failing
+// that, the first differing digit decides, but only once magnitudes are known
+// equal (tracked in `bias`).
+fn natcmp_compare_right(a: &[u8], ap: &mut usize, b: &[u8], bp: &mut usize) -> i64 {
+ let mut bias = 0i64;
+ loop {
+ let ca = natcmp_at(a, *ap);
+ let cb = natcmp_at(b, *bp);
+ let a_digit = ca.is_ascii_digit();
+ let b_digit = cb.is_ascii_digit();
+ if !a_digit && !b_digit {
+ return bias;
+ } else if !a_digit {
+ return -1;
+ } else if !b_digit {
+ return 1;
+ } else if ca < cb {
+ if bias == 0 {
+ bias = -1;
+ }
+ } else if ca > cb {
+ if bias == 0 {
+ bias = 1;
+ }
+ }
+ *ap += 1;
+ *bp += 1;
+ }
+}
+
+// Compare two left-aligned numbers: the first differing digit decides.
+fn natcmp_compare_left(a: &[u8], ap: &mut usize, b: &[u8], bp: &mut usize) -> i64 {
+ loop {
+ let ca = natcmp_at(a, *ap);
+ let cb = natcmp_at(b, *bp);
+ let a_digit = ca.is_ascii_digit();
+ let b_digit = cb.is_ascii_digit();
+ if !a_digit && !b_digit {
+ return 0;
+ } else if !a_digit {
+ return -1;
+ } else if !b_digit {
+ return 1;
+ } else if ca < cb {
+ return -1;
+ } else if ca > cb {
+ return 1;
+ }
+ *ap += 1;
+ *bp += 1;
+ }
}
pub fn strcspn(string: &str, characters: &str) -> usize {