Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 7 additions & 14 deletions src/stdlib/arrays.zig
Original file line number Diff line number Diff line change
Expand Up @@ -1240,7 +1240,10 @@ const ArrayCmp = enum { values, keys, assoc };
fn arraySetOp(comptime cmp: ArrayCmp, comptime keep_matches: bool) fn (*NativeContext, []const Value) RuntimeError!NativeResult {
return struct {
fn f(ctx: *NativeContext, args: []const Value) RuntimeError!NativeResult {
if (args.len < 2 or args[0] != .array) return NativeResult.scalar(.null);
// php8 accepts a single array with no other arrays, intersect keeps
// every entry (matchesAll over none) and diff drops none (matchesAny
// over none) soo both return a key-preserving copy of $array
if (args.len == 0 or args[0] != .array) return NativeResult.scalar(.null);
const src = args[0].array;
var result = try ctx.createArray();
for (src.entries.items) |entry| {
Expand Down Expand Up @@ -1463,25 +1466,15 @@ fn array_rand(ctx: *NativeContext, args: []const Value) RuntimeError!NativeResul
try ctx.vm.setPendingException("ValueError", "array_rand(): Argument #2 ($num) must be between 1 and the number of elements in argument #1 ($array)");
return error.RuntimeError;
}
if (num == 1 and (args.len < 2 or Value.toInt(args[1]) == 1 and args.len == 1)) {
// single-arg form returns scalar
// php returns a scalar key whenever num is 1, whether $num was omitted or
// passed explicitly, an array of keys only for num > 1
if (num == 1) {
const idx = std.crypto.random.intRangeAtMost(usize, 0, arr.entries.items.len - 1);
return switch (arr.entries.items[idx].key) {
.int => |i| NativeResult.scalar(.{ .int = i }),
.string => |s| NativeResult.shareString(s),
};
}
// PHP returns scalar key when num=1 (default), array of keys otherwise.
if (num == 1) {
const out = try ctx.createArray();
const idx = std.crypto.random.intRangeAtMost(usize, 0, arr.entries.items.len - 1);
const v: Value = switch (arr.entries.items[idx].key) {
.int => |i| .{ .int = i },
.string => |s| .{ .string = s },
};
try out.append(ctx.allocator, v);
return NativeResult.borrowed(.{ .array = out });
}
// Fisher-Yates partial shuffle for `num` distinct picks
var pool = try ctx.allocator.alloc(usize, arr.entries.items.len);
defer ctx.allocator.free(pool);
Expand Down
1 change: 1 addition & 0 deletions src/stdlib/datetime.zig
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ pub const entries = .{
.{ "date_time_set", native_date_time_set },
.{ "date_parse", native_date_parse },
.{ "date_parse_from_format", native_date_parse_from_format },
.{ "date_get_last_errors", dtGetLastErrors },
.{ "mktime", native_mktime },
.{ "gmmktime", native_gmmktime },
.{ "strtotime", native_strtotime },
Expand Down
37 changes: 37 additions & 0 deletions tests/array_diff_intersect_single_array.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php
// array_diff/array_intersect and their key/assoc variants accept a
// single array (php8) and return that array with its keys preserved

$a = ["x" => 1, 5 => 2, "y" => "2", 0 => 3];

var_dump(array_diff($a) === $a);
var_dump(array_intersect($a) === $a);
var_dump(array_diff_key($a) === $a);
var_dump(array_intersect_key($a) === $a);
var_dump(array_diff_assoc($a) === $a);
var_dump(array_intersect_assoc($a) === $a);

// keys and their order survive exactly (=== above already implies it, but keep
// the shape visible in the output)
print_r(array_diff($a));
print_r(array_intersect($a));
var_dump(array_keys(array_diff_key($a)));

// duplicate values are not collapsed by the single-array form
$dupes = ["a" => 1, "b" => 1, "c" => "1"];
var_dump(array_diff($dupes) === $dupes);
var_dump(array_intersect($dupes) === $dupes);

// empty array stays empty
var_dump(array_diff([]) === []);
var_dump(array_intersect([]) === []);
var_dump(array_diff_assoc([]) === []);
var_dump(array_intersect_assoc([]) === []);

// two-array behavior is unchanged
print_r(array_diff($a, [2]));
print_r(array_intersect($a, [2]));
print_r(array_diff_key($a, ["x" => 0]));
print_r(array_intersect_key($a, ["x" => 0]));
print_r(array_diff_assoc($a, ["x" => 1]));
print_r(array_intersect_assoc($a, ["x" => 1]));
52 changes: 52 additions & 0 deletions tests/array_rand_num_one.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<?php
// array_rand($arr, 1) returns a scalar key just like array_rand($arr).
// only num > 1 returns an array of keys. no assertion depends on which key the
// random pick returned, only on its type and membership

$assoc = ["a" => 1, "b" => 2, "c" => 3];
var_dump(is_array(array_rand($assoc)));
var_dump(is_array(array_rand($assoc, 1)));
var_dump(is_array(array_rand($assoc, 2)));

// string keys
$k = array_rand($assoc);
echo gettype($k) . " " . (array_key_exists($k, $assoc) ? "in" : "missing") . "\n";
$k = array_rand($assoc, 1);
echo gettype($k) . " " . (array_key_exists($k, $assoc) ? "in" : "missing") . "\n";

// numeric keys
$list = [10, 20, 30, 40];
$k = array_rand($list);
echo gettype($k) . " " . (array_key_exists($k, $list) ? "in" : "missing") . "\n";
$k = array_rand($list, 1);
echo gettype($k) . " " . (array_key_exists($k, $list) ? "in" : "missing") . "\n";

// num > 1 keeps the array-of-keys form distinct keys of the source array
$picks = array_rand($assoc, 2);
echo gettype($picks) . " " . count($picks) . "\n";
echo count(array_unique($picks)) === 2 ? "distinct\n" : "dupes\n";
foreach ($picks as $p) {
echo (array_key_exists($p, $assoc) ? "in " : "missing ");
}
echo "\n";

// asking for every key is deterministic, all keys in their original order
var_dump(array_rand($list, 4) === [0, 1, 2, 3]);
var_dump(array_rand($assoc, 3) === ["a", "b", "c"]);

// single-element array
$one = ["only" => 1];
var_dump(array_rand($one));
var_dump(array_rand($one, 1));

// out of range num still raises the ValueError
try {
array_rand($assoc, 4);
} catch (ValueError $e) {
echo get_class($e) . ": " . $e->getMessage() . "\n";
}
try {
array_rand($assoc, 0);
} catch (ValueError $e) {
echo get_class($e) . ": " . $e->getMessage() . "\n";
}
27 changes: 27 additions & 0 deletions tests/date_get_last_errors.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php
// the procedural date_get_last_errors() is registered and reports
// the same parse-error state as DateTime::getLastErrors()

var_dump(function_exists('date_get_last_errors'));

// succesful parse
DateTime::createFromFormat("Y-m-d", "2024-03-15");
var_dump(date_get_last_errors());
var_dump(date_get_last_errors() === DateTime::getLastErrors());

// failed parse
DateTime::createFromFormat("Y-m-d", "not a date");
$err = date_get_last_errors();
echo gettype($err) . "\n";
var_dump($err === DateTime::getLastErrors());
echo "ec=" . $err['error_count'] . " wc=" . $err['warning_count'] . "\n";
echo "has-errors-array: " . (is_array($err['errors']) ? 'y' : 'n') . "\n";
echo "has-warnings-array: " . (is_array($err['warnings']) ? 'y' : 'n') . "\n";

// a later successful parse resets both to false
DateTime::createFromFormat("Y-m-d", "2024-12-31");
var_dump(date_get_last_errors());

// DateTimeImmutable shares the same state as the procedural function
DateTimeImmutable::createFromFormat("Y-m-d", "garbage");
var_dump(date_get_last_errors() === DateTimeImmutable::getLastErrors());
Loading