aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates
diff options
context:
space:
mode:
Diffstat (limited to 'crates')
-rw-r--r--crates/shirabe-external-packages/src/symfony/console/completion/completion_input.rs12
-rw-r--r--crates/shirabe-external-packages/src/symfony/console/helper/table.rs4
-rw-r--r--crates/shirabe-php-shim/src/array.rs185
-rw-r--r--crates/shirabe-php-shim/src/string.rs151
4 files changed, 307 insertions, 45 deletions
diff --git a/crates/shirabe-external-packages/src/symfony/console/completion/completion_input.rs b/crates/shirabe-external-packages/src/symfony/console/completion/completion_input.rs
index 482e7e1..bd51b8e 100644
--- a/crates/shirabe-external-packages/src/symfony/console/completion/completion_input.rs
+++ b/crates/shirabe-external-packages/src/symfony/console/completion/completion_input.rs
@@ -135,13 +135,11 @@ impl CompletionInput {
let argument_value = self.inner.inner.arguments[&current_argument_name].clone();
self.completion_name = Some(current_argument_name.clone());
- if let PhpMixed::Array(argument_value) = &argument_value {
- self.completion_value = if !argument_value.is_empty() {
- argument_value[shirabe_php_shim::array_key_last(argument_value)].to_string()
- } else {
- // null
- String::new()
- };
+ if let PhpMixed::List(argument_value) = &argument_value {
+ self.completion_value = argument_value
+ .last()
+ .map(|v| v.to_string())
+ .unwrap_or_default();
} else {
self.completion_value = argument_value.to_string();
}
diff --git a/crates/shirabe-external-packages/src/symfony/console/helper/table.rs b/crates/shirabe-external-packages/src/symfony/console/helper/table.rs
index 9c868c4..27b07aa 100644
--- a/crates/shirabe-external-packages/src/symfony/console/helper/table.rs
+++ b/crates/shirabe-external-packages/src/symfony/console/helper/table.rs
@@ -990,10 +990,10 @@ impl Table {
Self::vec_set(&mut row, *column, unmerged_row[column].clone());
}
}
- shirabe_php_shim::array_splice_mixed(
+ shirabe_php_shim::array_splice(
&mut rows,
unmerged_row_key,
- 0,
+ Some(0),
vec![Self::from_row_vec(row)],
);
}
diff --git a/crates/shirabe-php-shim/src/array.rs b/crates/shirabe-php-shim/src/array.rs
index 8c1988d..dc06b01 100644
--- a/crates/shirabe-php-shim/src/array.rs
+++ b/crates/shirabe-php-shim/src/array.rs
@@ -144,8 +144,17 @@ pub fn array_diff(_array1: &[String], _array2: &[String]) -> Vec<String> {
.collect()
}
-pub fn array_unique<T: Clone>(_array: &[T]) -> Vec<T> {
- todo!()
+// PHP's default array_unique flag is SORT_STRING, comparing elements as
+// strings. For the `String`/`&str`-like element types used by every caller,
+// `PartialEq` is equivalent. First occurrence is kept, matching PHP.
+pub fn array_unique<T: Clone + PartialEq>(array: &[T]) -> Vec<T> {
+ let mut result: Vec<T> = Vec::new();
+ for item in array {
+ if !result.contains(item) {
+ result.push(item.clone());
+ }
+ }
+ result
}
pub fn array_intersect_key(
@@ -376,12 +385,13 @@ pub fn array_is_list(array: &PhpMixed) -> bool {
}
pub fn array_splice<T>(
- _array: &mut Vec<T>,
- _offset: i64,
- _length: Option<i64>,
- _replacement: Vec<T>,
+ array: &mut Vec<T>,
+ offset: i64,
+ length: Option<i64>,
+ replacement: Vec<T>,
) -> Vec<T> {
- todo!()
+ let (start, end) = php_slice_bounds(array.len() as i64, offset, length);
+ array.splice(start..end, replacement).collect()
}
pub fn array_pop_first<T>(array: &mut Vec<T>) -> Option<T> {
@@ -392,8 +402,113 @@ pub fn array_pop_first<T>(array: &mut Vec<T>) -> Option<T> {
}
}
-pub fn array_merge_recursive(_arrays: Vec<PhpMixed>) -> PhpMixed {
- todo!()
+pub fn array_merge_recursive(arrays: Vec<PhpMixed>) -> PhpMixed {
+ let mut acc: Vec<(MergeKey, PhpMixed)> = Vec::new();
+ let mut next_int: i64 = 0;
+ for arr in arrays {
+ merge_recursive_into(&mut acc, &mut next_int, arr);
+ }
+ merge_build(acc)
+}
+
+#[derive(Clone)]
+enum MergeKey {
+ Int(i64),
+ Str(String),
+}
+
+// Split a PHP array value into (key, value) entries. Integer and integer-like
+// string keys become `Int`, everything else stays `Str`, matching how PHP
+// normalises array keys.
+fn merge_entries(value: PhpMixed) -> Vec<(MergeKey, PhpMixed)> {
+ match value {
+ PhpMixed::List(items) => items
+ .into_iter()
+ .enumerate()
+ .map(|(i, v)| (MergeKey::Int(i as i64), v))
+ .collect(),
+ PhpMixed::Array(map) => map
+ .into_iter()
+ .map(|(k, v)| (merge_parse_key(k), v))
+ .collect(),
+ _ => panic!("array_merge_recursive(): Argument must be of type array"),
+ }
+}
+
+fn merge_parse_key(key: String) -> MergeKey {
+ if let Ok(n) = key.parse::<i64>() {
+ if n.to_string() == key {
+ return MergeKey::Int(n);
+ }
+ }
+ MergeKey::Str(key)
+}
+
+// Wrap a scalar into a single-element list, mirroring PHP's behaviour of
+// promoting a non-array value to an array before a recursive merge.
+fn merge_wrap_array(value: PhpMixed) -> PhpMixed {
+ match value {
+ PhpMixed::List(_) | PhpMixed::Array(_) => value,
+ scalar => PhpMixed::List(vec![scalar]),
+ }
+}
+
+fn merge_recursive_into(acc: &mut Vec<(MergeKey, PhpMixed)>, next_int: &mut i64, src: PhpMixed) {
+ for (key, value) in merge_entries(src) {
+ match key {
+ MergeKey::Int(_) => {
+ acc.push((MergeKey::Int(*next_int), value));
+ *next_int += 1;
+ }
+ MergeKey::Str(s) => {
+ let existing = acc
+ .iter()
+ .position(|(k, _)| matches!(k, MergeKey::Str(es) if *es == s));
+ match existing {
+ Some(pos) => {
+ let merged = merge_two_recursive(acc[pos].1.clone(), value);
+ acc[pos].1 = merged;
+ }
+ None => acc.push((MergeKey::Str(s), value)),
+ }
+ }
+ }
+ }
+}
+
+fn merge_two_recursive(a: PhpMixed, b: PhpMixed) -> PhpMixed {
+ let mut acc = merge_entries(merge_wrap_array(a));
+ let mut next_int = acc
+ .iter()
+ .filter_map(|(k, _)| match k {
+ MergeKey::Int(n) => Some(*n + 1),
+ MergeKey::Str(_) => None,
+ })
+ .max()
+ .unwrap_or(0);
+ merge_recursive_into(&mut acc, &mut next_int, merge_wrap_array(b));
+ merge_build(acc)
+}
+
+// Re-assemble entries into a PhpMixed, preferring a `List` when the keys are a
+// dense 0..n integer sequence (PHP renders such an array as a list).
+fn merge_build(acc: Vec<(MergeKey, PhpMixed)>) -> PhpMixed {
+ let is_list = acc
+ .iter()
+ .enumerate()
+ .all(|(i, (k, _))| matches!(k, MergeKey::Int(n) if *n == i as i64));
+ if is_list {
+ PhpMixed::List(acc.into_iter().map(|(_, v)| v).collect())
+ } else {
+ PhpMixed::Array(
+ acc.into_iter()
+ .map(|(k, v)| match k {
+ MergeKey::Int(n) => (n.to_string(), v),
+ MergeKey::Str(s) => (s, v),
+ })
+ .collect(),
+ )
+ }
}
pub fn array_slice<V: Clone>(
@@ -442,19 +557,6 @@ pub fn array_diff_key(
.collect()
}
-pub fn array_key_last(_array: &IndexMap<String, PhpMixed>) -> usize {
- todo!()
-}
-
-pub fn array_splice_mixed(
- _array: &mut Vec<PhpMixed>,
- _offset: i64,
- _length: i64,
- _replacement: Vec<PhpMixed>,
-) {
- todo!()
-}
-
/// Map a PHP array key (always stored as a `String` here) back to its PHP value
/// type: an integer-like key becomes an int, anything else stays a string.
fn php_key_to_mixed(key: &str) -> PhpMixed {
@@ -498,8 +600,8 @@ pub fn in_array(needle: PhpMixed, haystack: &PhpMixed, strict: bool) -> bool {
values.iter().any(|value| **value == needle)
}
-pub fn krsort<V>(_array: &mut IndexMap<i64, V>) {
- todo!()
+pub fn krsort<V>(array: &mut IndexMap<i64, V>) {
+ array.sort_by(|k1, _, k2, _| k2.cmp(k1));
}
pub fn uasort<T, F>(array: &mut Vec<T>, compare: F)
@@ -522,8 +624,14 @@ pub fn sort<T: Ord>(_array: &mut Vec<T>) {
_array.sort();
}
-pub fn sort_with_flags<T: Ord>(_array: &mut Vec<T>, _flags: i64) {
- todo!()
+pub fn sort_with_flags<T: Ord>(array: &mut Vec<T>, flags: i64) {
+ if flags != SORT_REGULAR {
+ // TODO(phase-d): flag-specific comparison (SORT_NUMERIC/SORT_STRING/
+ // SORT_NATURAL/SORT_FLAG_CASE) cannot be expressed for a generic
+ // `T: Ord` element. No caller passes a non-regular flag yet.
+ todo!("sort() with flags other than SORT_REGULAR");
+ }
+ array.sort();
}
pub const SORT_REGULAR: i64 = 0;
@@ -540,12 +648,25 @@ where
_array.sort_by(|a, b| compare(a, b).cmp(&0));
}
-pub fn ksort<V>(_array: &mut IndexMap<String, V>) {
- todo!()
+pub fn ksort<V>(array: &mut IndexMap<String, V>) {
+ array.sort_by(|k1, _, k2, _| php_sort_regular_key(k1, k2));
}
-pub fn asort<V: Ord>(_array: &mut IndexMap<String, V>) {
- todo!()
+// PHP's default SORT_REGULAR comparison for array keys: two integer-like keys
+// compare numerically, otherwise byte-wise as strings.
+// TODO(phase-d): full SORT_REGULAR semantics for mixed integer/non-numeric-string
+// keys are not reproduced; every current caller uses homogeneous string keys.
+fn php_sort_regular_key(a: &str, b: &str) -> std::cmp::Ordering {
+ if let (Ok(na), Ok(nb)) = (a.parse::<i64>(), b.parse::<i64>()) {
+ if na.to_string() == a && nb.to_string() == b {
+ return na.cmp(&nb);
+ }
+ }
+ a.cmp(b)
+}
+
+pub fn asort<V: Ord>(array: &mut IndexMap<String, V>) {
+ array.sort_by(|_, v1, _, v2| v1.cmp(v2));
}
pub fn uksort<V, F>(array: &mut IndexMap<String, V>, callback: F)
@@ -556,8 +677,8 @@ where
array.sort_by(|k1, _, k2, _| callback(k1, k2).cmp(&0));
}
-pub fn sort_natural_flag_case(_values: &mut Vec<String>) {
- todo!()
+pub fn sort_natural_flag_case(values: &mut Vec<String>) {
+ values.sort_by(|a, b| crate::strnatcasecmp(a, b).cmp(&0));
}
pub fn count_mixed(value: &PhpMixed) -> i64 {
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 {