diff --git a/src/collections_me.c b/src/collections_me.c index 94ad376..9bb2430 100644 --- a/src/collections_me.c +++ b/src/collections_me.c @@ -97,21 +97,21 @@ ZEND_BEGIN_ARG_INFO(copy_of_arginfo, 0) ZEND_END_ARG_INFO() ZEND_BEGIN_ARG_INFO(copy_of_range_arginfo, 0) - ZEND_ARG_TYPE_INFO(0, from_idx, IS_LONG, 0) - ZEND_ARG_TYPE_INFO(0, num_elements, IS_LONG, 0) + ZEND_ARG_TYPE_INFO(0, from_index, IS_LONG, 0) + ZEND_ARG_TYPE_INFO(0, to_index, IS_LONG, 0) ZEND_END_ARG_INFO() ZEND_BEGIN_ARG_INFO(fill_arginfo, 0) ZEND_ARG_INFO(0, element) ZEND_ARG_TYPE_INFO(0, from_index, IS_LONG, 0) - ZEND_ARG_TYPE_INFO(0, num_elements, IS_LONG, 0) + ZEND_ARG_TYPE_INFO(0, to_index, IS_LONG, 0) ZEND_END_ARG_INFO() ZEND_BEGIN_ARG_INFO(binary_search_arginfo, 0) ZEND_ARG_INFO(0, element) ZEND_ARG_TYPE_INFO(0, flags, IS_LONG, 0) ZEND_ARG_TYPE_INFO(0, from_index, IS_LONG, 0) - ZEND_ARG_TYPE_INFO(0, num_elements, IS_LONG, 0) + ZEND_ARG_TYPE_INFO(0, to_index, IS_LONG, 0) ZEND_END_ARG_INFO() ZEND_BEGIN_ARG_INFO(binary_search_by_arginfo, 0) @@ -119,7 +119,7 @@ ZEND_BEGIN_ARG_INFO(binary_search_by_arginfo, 0) ZEND_ARG_CALLABLE_INFO(0, selector, 0) ZEND_ARG_TYPE_INFO(0, flags, IS_LONG, 0) ZEND_ARG_TYPE_INFO(0, from_index, IS_LONG, 0) - ZEND_ARG_TYPE_INFO(0, num_elements, IS_LONG, 0) + ZEND_ARG_TYPE_INFO(0, to_index, IS_LONG, 0) ZEND_END_ARG_INFO() ZEND_BEGIN_ARG_INFO(chuncked_arginfo, 0) diff --git a/src/collections_methods.c b/src/collections_methods.c index 13882cc..2699655 100644 --- a/src/collections_methods.c +++ b/src/collections_methods.c @@ -75,6 +75,8 @@ PHP_COLLECTIONS_ERROR(E_WARNING, "Elements should be int or double") #define ERR_BAD_GROUP() \ PHP_COLLECTIONS_ERROR(E_WARNING, "Group value must be array") +#define ERR_NOT_PACKED() \ + PHP_COLLECTIONS_ERROR(E_WARNING, "The array should be packed") #define ERR_SILENCED() #define ELEMENTS_VALIDATE(elements, err, err_then) \ @@ -147,6 +149,15 @@ static zend_always_inline zend_object* create_pair_obj() return create_object(collections_pair_ce, &std_object_handlers); } +static zend_always_inline void array_release(zend_array* ht) +{ + if (GC_REFCOUNT(ht) > 1) { + GC_DELREF(ht); + } else { + zend_array_destroy(ht); + } +} + static zend_always_inline zend_array* array_group_fetch(zend_array* ht, zval* key) { zend_array* group = NULL; @@ -351,6 +362,25 @@ static zend_always_inline compare_func_t compare_func_init( return reverse ? bucket_reverse_compare_regular : bucket_compare_regular; } +static zend_always_inline zend_long binary_search(Bucket* ref, zval* val, + zend_long from_idx, zend_long to_idx, compare_func_t cmp) +{ + zend_long low = from_idx; + zend_long high = to_idx - 1; + while (low <= high) { + zend_long mid = (low + high) >> 1; + int result = cmp(&ref[mid].val, val); + if (result == 1) { + high = mid - 1; + } else if (result == -1) { + low = mid + 1; + } else { + return mid; + } + } + return -(low + 1); +} + static zend_always_inline void array_sort_by(zend_array* ht) { uint32_t i; @@ -879,12 +909,95 @@ PHP_METHOD(Collection, average) PHP_METHOD(Collection, binarySearch) { - + zval* element; + zend_long flags = 0; + zend_long from_idx = 0; + zend_array* current = THIS_COLLECTION; + uint32_t num_elements = zend_hash_num_elements(current); + zend_long to_idx = num_elements; + ZEND_PARSE_PARAMETERS_START(1, 4) + Z_PARAM_ZVAL(element) + Z_PARAM_OPTIONAL + Z_PARAM_LONG(flags) + Z_PARAM_LONG(from_idx) + Z_PARAM_LONG(to_idx) + ZEND_PARSE_PARAMETERS_END(); + if (UNEXPECTED(!HT_IS_PACKED(current))) { + ERR_NOT_PACKED(); + RETURN_NULL(); + } + if (UNEXPECTED(from_idx < 0)) { + ERR_BAD_INDEX(); + RETURN_NULL(); + } + if (to_idx > num_elements) { + to_idx = num_elements; + } + if (UNEXPECTED(to_idx < from_idx)) { + ERR_BAD_SIZE(); + RETURN_NULL(); + } + compare_func_t cmp; + ZEND_HASH_FOREACH_VAL(current, zval* val) + cmp = compare_func_init(val, 0, flags); + break; + ZEND_HASH_FOREACH_END(); + RETVAL_LONG(binary_search(current->arData, element, from_idx, to_idx, cmp)); } PHP_METHOD(Collection, binarySearchBy) { - + zval* element; + zend_fcall_info fci; + zend_fcall_info_cache fcc; + zend_long flags = 0; + zend_long from_idx = 0; + zend_array* current = THIS_COLLECTION; + uint32_t num_elements = zend_hash_num_elements(current); + zend_long to_idx = num_elements; + ZEND_PARSE_PARAMETERS_START(2, 5) + Z_PARAM_ZVAL(element) + Z_PARAM_FUNC(fci, fcc) + Z_PARAM_OPTIONAL + Z_PARAM_LONG(flags) + Z_PARAM_LONG(from_idx) + Z_PARAM_LONG(to_idx) + ZEND_PARSE_PARAMETERS_END(); + if (UNEXPECTED(!HT_IS_PACKED(current))) { + ERR_NOT_PACKED(); + RETURN_NULL(); + } + if (UNEXPECTED(from_idx < 0)) { + ERR_BAD_INDEX(); + RETURN_NULL(); + } + if (to_idx > num_elements) { + to_idx = num_elements; + } + if (UNEXPECTED(to_idx < from_idx)) { + ERR_BAD_SIZE(); + RETURN_NULL(); + } + INIT_FCI(&fci, 2); + uint32_t range = to_idx - from_idx; + Bucket* ref = (Bucket*)malloc(range * sizeof(Bucket)); + compare_func_t cmp = NULL; + uint32_t idx = 0; + Bucket* bucket = current->arData + from_idx; + Bucket* end = current->arData + to_idx; + for (; bucket < end; ++bucket) { + CALLBACK_KEYVAL_INVOKE(params, bucket); + memcpy(&ref[idx++].val, &bucket->val, sizeof(zval)); + if (UNEXPECTED(cmp == NULL)) { + cmp = compare_func_init(&retval, 0, flags); + } + } + zend_long result = binary_search(ref, element, 0, range, cmp); + RETVAL_LONG(result < 0 ? result - from_idx : result + from_idx); + for (idx = 0; idx < range; ++idx) { + zval_ptr_dtor(&ref[idx].val); + } + free(ref); } PHP_METHOD(Collection, chunked) @@ -928,6 +1041,7 @@ PHP_METHOD(Collection, chunked) if (transform) { ZVAL_LONG(¶ms[1], num_chunks++); zend_call_function(&fci, &fcc); + array_release(chunk); zend_hash_next_index_insert(chunked, &retval); } else { zend_hash_next_index_insert(chunked, ¶ms[0]); @@ -939,6 +1053,7 @@ PHP_METHOD(Collection, chunked) if (transform) { ZVAL_LONG(¶ms[1], num_chunks++); zend_call_function(&fci, &fcc); + array_release(chunk); zend_hash_next_index_insert(chunked, &retval); } else { zend_hash_next_index_insert(chunked, ¶ms[0]); @@ -1115,20 +1230,21 @@ PHP_METHOD(Collection, copyOf) PHP_METHOD(Collection, copyOfRange) { - zend_long from_idx, num_elements; + zend_long from_idx, to_idx; ZEND_PARSE_PARAMETERS_START(2, 2) Z_PARAM_LONG(from_idx) - Z_PARAM_LONG(num_elements) + Z_PARAM_LONG(to_idx) ZEND_PARSE_PARAMETERS_END(); - if (from_idx < 0) { + if (UNEXPECTED(from_idx < 0)) { ERR_BAD_INDEX(); RETURN_NULL(); } - if (num_elements < 0) { + if (UNEXPECTED(to_idx < from_idx)) { ERR_BAD_SIZE(); RETURN_NULL(); } zend_array* current = THIS_COLLECTION; + uint32_t num_elements = to_idx - from_idx; ARRAY_NEW(new_collection, num_elements); zend_bool packed = HT_IS_PACKED(current); Bucket* bucket = current->arData; @@ -1316,14 +1432,23 @@ PHP_METHOD(Collection, fill) zval* element; zend_long from_idx = 0; zend_array* current = THIS_COLLECTION; - SEPARATE_CURRENT_COLLECTION(current); - zend_long num_elements = zend_hash_num_elements(current - from_idx); + zend_long to_idx = zend_hash_num_elements(current); ZEND_PARSE_PARAMETERS_START(1, 3) Z_PARAM_ZVAL(element) Z_PARAM_OPTIONAL Z_PARAM_LONG(from_idx) - Z_PARAM_LONG(num_elements) + Z_PARAM_LONG(to_idx) ZEND_PARSE_PARAMETERS_END(); + if (UNEXPECTED(from_idx < 0)) { + ERR_BAD_INDEX(); + RETURN_NULL(); + } + if (UNEXPECTED(to_idx < from_idx)) { + ERR_BAD_SIZE(); + RETURN_NULL(); + } + SEPARATE_CURRENT_COLLECTION(current); + uint32_t num_elements = to_idx - from_idx; Bucket* bucket = current->arData + from_idx; Bucket* end = bucket + current->nNumUsed; for (; num_elements > 0 && bucket < end; ++bucket, --num_elements) { diff --git a/stubs/Collection.php b/stubs/Collection.php index ea3020a..3dca4fa 100644 --- a/stubs/Collection.php +++ b/stubs/Collection.php @@ -102,10 +102,10 @@ class Collection implements ArrayAccess, Countable * @param mixed $element * @param int $flags[optional] * @param int $from_index[optional] - * @param int $num_elements[optional] + * @param int $to_index[optional] * @return int|null */ - function binarySearch($element, $flags, $from_index, $num_elements) {} + function binarySearch($element, $flags, $from_index, $to_index) {} /** * Searches the array or the range of the array for the provided element using the @@ -119,10 +119,25 @@ class Collection implements ArrayAccess, Countable * @param callable $selector ($value, $key) -> $new_value * @param int $flags[optional] * @param int $from_index[optional] - * @param int $num_elements[optional] + * @param int $to_index[optional] * @return int|null */ - function binarySearchBy($element, $selector, $flags, $from_index, $num_elements) {} + function binarySearchBy($element, $selector, $flags, $from_index, $to_index) {} + + /** + * Searches the array or the range of the array for the provided element using the + * binary search algorithm. + * + * The array is expected to be packed and sorted into ascending order according to + * the specified comparator, otherwise the result is undefined. + * + * @param mixed $element + * @param callable $comparator (Pair($key, $value), Pair($key, $value)) -> int + * @param int $from_index[optional] + * @param int $to_index[optional] + * @return int|null + */ + function binarySearchWith($element, $comparator, $from_index, $to_index) {} /** * Splits this collection into a collection of arrays each not exceeding the given size. @@ -187,10 +202,10 @@ class Collection implements ArrayAccess, Countable * Returns new collection which is a copy of range of original collection. * * @param int $from_index - * @param int $num_elements + * @param int $to_index * @return Collection */ - function copyOfRange($from_index, $num_elements) {} + function copyOfRange($from_index, $to_index) {} /** * Returns the number of elements in this collection. @@ -260,10 +275,10 @@ class Collection implements ArrayAccess, Countable * * @param mixed $element * @param int $from_index[optional] - * @param int $num_elements[optional] + * @param int $to_index[optional] * @return void */ - function fill($element, $from_index, $num_elements) {} + function fill($element, $from_index, $to_index) {} /** * Returns a collection containing only elements matching the given predicate. diff --git a/tests/011-copy-of-range.phpt b/tests/011-copy-of-range.phpt index df6d81a..5386163 100644 --- a/tests/011-copy-of-range.phpt +++ b/tests/011-copy-of-range.phpt @@ -7,8 +7,8 @@ $array = [3, 7, 6, 9, 2]; // An associative array, however, Collection::copyOfRange() still works, // and string keys will be preserved. $array1 = ['a' => 'b', 'c', 'd' => 'e']; -$array2 = Collection::init($array)->copyOfRange(2, 4)->toArray(); -$array3 = Collection::init($array1)->copyOfRange(1, 2)->toArray(); +$array2 = Collection::init($array)->copyOfRange(2, 6)->toArray(); +$array3 = Collection::init($array1)->copyOfRange(1, 3)->toArray(); if ($array2 != array_slice($array, 2, 4) || $array3 != array_slice($array1, 1, 2)) { echo 'Collection::copyOfRange() failed.', PHP_EOL; } diff --git a/tests/016-fill.phpt b/tests/016-fill.phpt index 1fb5d7c..dde288b 100644 --- a/tests/016-fill.phpt +++ b/tests/016-fill.phpt @@ -4,7 +4,7 @@ Test Collection::fill(). 'baz', 1, 2, 3]; $collection = Collection::init($array); -$collection->fill('t', 1, 3); +$collection->fill('t', 1, 4); $array1 = ['foo', 'bar' => 't', 't', 't', 3]; $collection1 = Collection::init($array); $collection1->fill(0); diff --git a/tests/058-binary-search-by.phpt b/tests/058-binary-search-by.phpt new file mode 100644 index 0000000..07830ff --- /dev/null +++ b/tests/058-binary-search-by.phpt @@ -0,0 +1,30 @@ +--TEST-- +Test Collection::binarySearch(). +--FILE-- +binarySearch($which, 0, $from, $to) != $idx) { + echo 'Collection::binarySearch() failed.', PHP_EOL; +} +$array = array_map(function ($value) { + return [$value]; +}, $array); +$selector = function ($value) { + return $value[0]; +}; +if ($collection->binarySearchBy($which, $selector, 0, $from, $to) != $idx) { + echo 'Collection::binarySearchBy() failed.', PHP_EOL; +} +?> +--EXPECT--