use crate::PhpMixed; use crate::php_to_string; use indexmap::IndexMap; pub fn array_values(_array: &IndexMap) -> Vec { _array.values().cloned().collect() } pub fn array_keys(_array: &IndexMap) -> Vec { _array.keys().cloned().collect() } pub fn array_push(_array: &mut Vec, _value: String) -> i64 { _array.push(_value); _array.len() as i64 } pub fn array_search_in_vec(_needle: &str, _haystack: &[String]) -> Option { _haystack.iter().position(|s| s.as_str() == _needle) } pub fn array_map_str_fn String>(_callback: F, _array: &[String]) -> Vec { _array.iter().map(|s| _callback(s)).collect() } pub fn array_slice_mixed(value: &PhpMixed, offset: i64, length: Option) -> PhpMixed { match value { PhpMixed::List(items) => { let (start, end) = php_slice_bounds(items.len() as i64, offset, length); PhpMixed::List(items[start..end].to_vec()) } PhpMixed::Array(map) => { let (start, end) = php_slice_bounds(map.len() as i64, offset, length); PhpMixed::Array( map.iter() .skip(start) .take(end - start) .map(|(k, v)| (k.clone(), v.clone())) .collect(), ) } _ => panic!("array_slice(): Argument #1 ($array) must be of type array"), } } pub fn array_slice_strs(value: &[String], offset: i64, length: Option) -> Vec { let (start, end) = php_slice_bounds(value.len() as i64, offset, length); value[start..end].to_vec() } pub fn array_fill_keys(keys: PhpMixed, value: PhpMixed) -> PhpMixed { let entries: Vec<&PhpMixed> = match &keys { PhpMixed::List(items) => items.iter().collect(), PhpMixed::Array(map) => map.values().collect(), _ => panic!("array_fill_keys(): Argument #1 ($keys) must be of type array"), }; let mut result: IndexMap = IndexMap::new(); for key in entries { result.insert(php_to_string(key), value.clone()); } PhpMixed::Array(result) } /// PHP `array_merge`. /// /// Must reproduce PHP's mixed integer/string key semantics: /// - string keys: a later array's value overwrites an earlier one, keeping the /// earlier key's position; /// - integer-like keys ("0","1",...): values are appended and renumbered /// sequentially across all inputs (they are NOT overwritten by key). /// /// A naive per-entry `IndexMap::insert` is INCORRECT for inputs that mix string /// and integer keys (e.g. an AliasPackage's provides/replaces, where /// self.version expansion appends links under "0","1",... keys). See the typed /// [`array_merge_map`] variant used by such call sites. pub fn array_merge(array1: PhpMixed, array2: PhpMixed) -> PhpMixed { let mut result: IndexMap = IndexMap::new(); let mut next_int: i64 = 0; for array in [array1, array2] { match array { PhpMixed::List(items) => { for value in items { result.insert(next_int.to_string(), value); next_int += 1; } } PhpMixed::Array(map) => { for (key, value) in map { if let Ok(n) = key.parse::() { if n.to_string() == key { result.insert(next_int.to_string(), value); next_int += 1; continue; } } result.insert(key, value); } } _ => panic!("array_merge(): Argument must be of type array"), } } let is_list = result.keys().enumerate().all(|(i, k)| *k == i.to_string()); if is_list { PhpMixed::List(result.into_values().collect()) } else { PhpMixed::Array(result) } } /// PHP `array_merge` for a string-keyed map that MAY also contain integer-like /// keys. Typed counterpart of [`array_merge`] for `IndexMap` values /// (e.g. `Link` maps from `getProvides`/`getReplaces`). /// /// Must reproduce the same mixed-key semantics as [`array_merge`]: string keys /// overwrite in place (later wins), integer-like keys ("0","1",...) are appended /// and renumbered sequentially across both inputs. A naive `IndexMap::insert` /// per entry is INCORRECT because it would collide on shared integer keys. pub fn array_merge_map( array1: IndexMap, array2: IndexMap, ) -> IndexMap { let mut result: IndexMap = IndexMap::new(); let mut next_int: i64 = 0; for array in [array1, array2] { for (key, value) in array { if let Ok(n) = key.parse::() { if n.to_string() == key { result.insert(next_int.to_string(), value); next_int += 1; continue; } } result.insert(key, value); } } result } pub fn array_diff(_array1: &[String], _array2: &[String]) -> Vec { _array1 .iter() .filter(|&x| !_array2.contains(x)) .cloned() .collect() } pub fn array_unique(_array: &[T]) -> Vec { todo!() } pub fn array_intersect_key( _array1: &IndexMap, _array2: &IndexMap, ) -> IndexMap { _array1 .iter() .filter(|(k, _)| _array2.contains_key(k.as_str())) .map(|(k, v)| (k.clone(), v.clone())) .collect() } pub fn array_replace_recursive( mut base: IndexMap, replacement: IndexMap, ) -> IndexMap { for (key, replacement_value) in replacement { let merged = match base.get(&key) { Some(base_value) => { array_replace_recursive_value(base_value.clone(), replacement_value) } None => replacement_value, }; base.insert(key, merged); } base } // PHP recurses only when both the existing and the replacing value are arrays; // otherwise the replacing value wins outright. fn array_replace_recursive_value(base: PhpMixed, replacement: PhpMixed) -> PhpMixed { match (base, replacement) { (PhpMixed::Array(base), PhpMixed::Array(replacement)) => { PhpMixed::Array(array_replace_recursive_assoc(base, replacement)) } (PhpMixed::List(base), PhpMixed::List(replacement)) => { PhpMixed::List(array_replace_recursive_list(base, replacement)) } (_, replacement) => replacement, } } fn array_replace_recursive_assoc( mut base: IndexMap, replacement: IndexMap, ) -> IndexMap { for (key, replacement_value) in replacement { let merged = match base.get(&key) { Some(base_value) => { array_replace_recursive_value(base_value.clone(), replacement_value) } None => replacement_value, }; base.insert(key, merged); } base } fn array_replace_recursive_list( mut base: Vec, replacement: Vec, ) -> Vec { for (index, replacement_value) in replacement.into_iter().enumerate() { if index < base.len() { base[index] = array_replace_recursive_value(base[index].clone(), replacement_value); } else { base.push(replacement_value); } } base } pub fn array_search_mixed( needle: &PhpMixed, haystack: &PhpMixed, strict: bool, ) -> Option { if !strict { // TODO(phase-c): non-strict array_search needs PHP's loose `==` comparison // semantics. Only the strict path is implemented; loose comparison is // deferred rather than approximated. todo!("non-strict array_search (PHP loose comparison)"); } match haystack { PhpMixed::List(items) => items .iter() .position(|value| value == needle) .map(|i| PhpMixed::Int(i as i64)), PhpMixed::Array(map) => map .iter() .find(|(_, value)| *value == needle) .map(|(key, _)| php_key_to_mixed(key)), _ => None, } } pub fn array_search(needle: &str, haystack: &IndexMap) -> Option { haystack .iter() .find(|(_, value)| value.as_str() == needle) .map(|(key, _)| key.clone()) } pub fn array_shift(_array: &mut Vec) -> Option { if _array.is_empty() { None } else { Some(_array.remove(0)) } } pub fn array_pop(_array: &mut Vec) -> Option { _array.pop() } pub fn array_unshift(_array: &mut Vec, _value: T) { _array.insert(0, _value); } pub fn array_reverse(_array: &[T], _preserve_keys: bool) -> Vec { _array.iter().rev().cloned().collect() } pub fn array_filter(_array: &[T], _callback: F) -> Vec where F: Fn(&T) -> bool, { _array.iter().filter(|&x| _callback(x)).cloned().collect() } pub fn array_filter_map( _array: &IndexMap, _callback: F, ) -> IndexMap where F: Fn(&PhpMixed) -> bool, { _array .iter() .filter(|&(_, v)| _callback(v)) .map(|(k, v)| (k.clone(), v.clone())) .collect() } pub fn array_all(_array: &[T], _callback: F) -> bool where F: Fn(&T) -> bool, { _array.iter().all(_callback) } pub fn array_any(_array: &[T], _callback: F) -> bool where F: Fn(&T) -> bool, { _array.iter().any(_callback) } pub fn array_reduce(_array: &[T], _callback: F, _initial: U) -> U where F: Fn(U, &T) -> U, { _array.iter().fold(_initial, _callback) } pub fn array_intersect(_array1: &[T], _array2: &[T]) -> Vec { _array1 .iter() .filter(|&x| _array2.contains(x)) .cloned() .collect() } pub fn array_flip(array: &PhpMixed) -> PhpMixed { let mut result: IndexMap = IndexMap::new(); match array { PhpMixed::List(items) => { for (i, value) in items.iter().enumerate() { match value { PhpMixed::Int(n) => { result.insert(n.to_string(), PhpMixed::Int(i as i64)); } PhpMixed::String(s) => { result.insert(s.clone(), PhpMixed::Int(i as i64)); } // Non int/string values cannot be array keys and are skipped. _ => {} } } } PhpMixed::Array(map) => { for (key, value) in map { match value { PhpMixed::Int(n) => { result.insert(n.to_string(), php_key_to_mixed(key)); } PhpMixed::String(s) => { result.insert(s.clone(), php_key_to_mixed(key)); } _ => {} } } } _ => panic!("array_flip(): Argument #1 ($array) must be of type array"), } PhpMixed::Array(result) } pub fn array_flip_strings(_array: &[String]) -> IndexMap { _array .iter() .enumerate() .map(|(i, s)| (s.clone(), PhpMixed::Int(i as i64))) .collect() } pub fn array_key_exists(_key: &str, _array: &IndexMap) -> bool { _array.contains_key(_key) } pub fn array_is_list(array: &PhpMixed) -> bool { match array { PhpMixed::List(_) => true, PhpMixed::Array(map) => map.keys().enumerate().all(|(i, k)| *k == i.to_string()), _ => panic!("array_is_list(): Argument #1 ($array) must be of type array"), } } pub fn array_splice( _array: &mut Vec, _offset: i64, _length: Option, _replacement: Vec, ) -> Vec { todo!() } pub fn array_pop_first(array: &mut Vec) -> Option { if array.is_empty() { None } else { Some(array.remove(0)) } } pub fn array_merge_recursive(_arrays: Vec) -> PhpMixed { todo!() } pub fn array_slice( array: &IndexMap, offset: i64, length: Option, ) -> IndexMap { let (start, end) = php_slice_bounds(array.len() as i64, offset, length); array .iter() .skip(start) .take(end - start) .map(|(k, v)| (k.clone(), v.clone())) .collect() } pub fn array_map(_callback: F, _array: &[T]) -> Vec where F: Fn(&T) -> U, { _array.iter().map(_callback).collect() } pub fn array_filter_use_key( _array: &IndexMap, _callback: Box bool>, ) -> IndexMap { _array .iter() .filter(|(k, _)| _callback(k.as_str())) .map(|(k, v)| (k.clone(), v.clone())) .collect() } pub fn array_chunk(_array: &[T], _size: i64, _preserve_keys: bool) -> Vec> { _array.chunks(_size as usize).map(|c| c.to_vec()).collect() } pub fn array_diff_key( _array1: IndexMap, _array2: &IndexMap, ) -> IndexMap { _array1 .into_iter() .filter(|(k, _)| !_array2.contains_key(k.as_str())) .collect() } pub fn array_key_last(_array: &IndexMap) -> usize { todo!() } pub fn array_splice_mixed( _array: &mut Vec, _offset: i64, _length: i64, _replacement: Vec, ) { 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 { if let Ok(n) = key.parse::() { if n.to_string() == key { return PhpMixed::Int(n); } } PhpMixed::String(key.to_string()) } /// Resolve PHP array_slice/substr-style (offset, length) into a `[start, end)` /// pair of indices, honouring negative offsets and lengths. fn php_slice_bounds(len: i64, offset: i64, length: Option) -> (usize, usize) { let start = if offset < 0 { (len + offset).max(0) } else { offset.min(len) }; let end = match length { None => len, Some(l) if l < 0 => (len + l).max(start), Some(l) => (start + l).min(len), }; (start as usize, end as usize) } pub fn in_array(needle: PhpMixed, haystack: &PhpMixed, strict: bool) -> bool { let values: Vec<&PhpMixed> = match haystack { PhpMixed::List(items) => items.iter().collect(), PhpMixed::Array(map) => map.values().collect(), _ => return false, }; if !strict { // TODO(phase-c): non-strict in_array needs PHP's loose `==` comparison semantics. Only the // strict path is implemented; loose comparison is deferred rather than approximated. todo!("non-strict in_array (PHP loose comparison)"); } values.iter().any(|value| **value == needle) } pub fn krsort(_array: &mut IndexMap) { todo!() } pub fn uasort(array: &mut Vec, compare: F) where F: FnMut(&T, &T) -> i64, { let mut compare = compare; array.sort_by(|a, b| compare(a, b).cmp(&0)); } pub fn uasort_map(array: &mut IndexMap, compare: F) where F: FnMut(&V, &V) -> i64, { let mut compare = compare; array.sort_by(|_, v1, _, v2| compare(v1, v2).cmp(&0)); } pub fn sort(_array: &mut Vec) { _array.sort(); } pub fn sort_with_flags(_array: &mut Vec, _flags: i64) { todo!() } pub const SORT_REGULAR: i64 = 0; pub const SORT_NUMERIC: i64 = 1; pub const SORT_STRING: i64 = 2; pub const SORT_NATURAL: i64 = 6; pub const SORT_FLAG_CASE: i64 = 8; pub fn usort(_array: &mut Vec, _compare: F) where F: FnMut(&T, &T) -> i64, { let mut compare = _compare; _array.sort_by(|a, b| compare(a, b).cmp(&0)); } pub fn ksort(_array: &mut IndexMap) { todo!() } pub fn asort(_array: &mut IndexMap) { todo!() } pub fn uksort(array: &mut IndexMap, callback: F) where F: FnMut(&str, &str) -> i64, { let mut callback = callback; array.sort_by(|k1, _, k2, _| callback(k1, k2).cmp(&0)); } pub fn sort_natural_flag_case(_values: &mut Vec) { todo!() } pub fn count_mixed(value: &PhpMixed) -> i64 { count(value) as i64 } pub fn count(value: &PhpMixed) -> usize { match value { PhpMixed::List(items) => items.len(), PhpMixed::Array(entries) => entries.len(), PhpMixed::Object(object) => object.count(), // PHP 8 throws a `TypeError` for non-countable arguments. PhpMixed::Null | PhpMixed::Bool(_) | PhpMixed::Int(_) | PhpMixed::Float(_) | PhpMixed::String(_) => { panic!("count(): Argument #1 ($value) must be of type Countable|array") } } } pub fn current(_value: PhpMixed) -> PhpMixed { todo!() } pub fn key(_value: PhpMixed) -> Option { todo!() } pub fn reset(_array: &[T]) -> Option { _array.first().cloned() } pub fn reset_first(_array: &[T]) -> Option { _array.first().cloned() } pub fn end_arr(_array: &IndexMap) -> Option { _array.values().last().cloned() } pub fn iterator_to_array(iter: I) -> Vec where I: IntoIterator, { iter.into_iter().collect() } pub fn end(_array: &[V]) -> Option { _array.last().cloned() }