The Perl and Raku Conference 2025: Greenville, South Carolina - June 27-29 Learn more

# Copyright (c) 2024 Yuki Kimoto
# MIT License
class R::NDArray {
version_from R;
use Hash;
use IntList;
use Native::MethodCall;
use StringBuffer;
use R::Util;
use R::OP;
use Sort;
use R::NDArray::String;
use Comparable;
# Fields
has data : protected ro object;
method set_data : void ($data : object) {
unless ($data) {
die "The \$data must be defined.";
}
unless (Fn->is_array($data)) {
die "The type of the \$data must be an array type.";
}
my $type_name = type_name $data;
unless ($type_name eq type_name $self->{data}) {
die "The type name of the \$data must be same as the type name of the data of this instance.";
}
my $array_length = Fn->array_length($data);
unless ($array_length == Fn->array_length($self->{data})) {
die "The array lenght of the \$data must be equal to the array legnth of the array length of the data of this instance.";
}
if (Fn->is_object_array($data)) {
Array->memcpy_object_address((object[])$self->{data}, 0, (object[])$data, 0, $array_length);
}
else {
my $elem_size = Fn->get_elem_size($data);
Fn->memcpy($self->{data}, 0, $data, 0, $elem_size * $array_length);
}
}
has dim : int[];
method dim : int[] () {
my $dim = $self->{dim};
my $dim_clone = copy $dim;
return $dim_clone;
}
method set_dim : void ($dim : int[]) {
my $is_dim_read_only = $self->{is_dim_read_only};
if ($is_dim_read_only) {
die "dim field is read only.";
}
my $nomalized_dim = R::Util->normalize_dim($dim);
my $data = $self->{data};
R::Util->check_length($data, $nomalized_dim);
$self->{dim} = $nomalized_dim;
}
has is_dim_read_only : ro byte;
# Class Methods
static method normalize_indexes_product : R::NDArray::Int[] ($indexes_product : R::NDArray::Int[], $dim : int[]) {
unless ($indexes_product) {
die "The cartesian product of the indexes \$indexes_product must be defined.";
}
unless ($dim) {
die "The dimensions \$dim muest be defined.";
}
my $indexes_product_length = @$indexes_product;
my $dim_length = @$dim;
unless ($indexes_product_length <= $dim_length) {
die "The length of the cartesian product of the indexes \$indexes_product must be less than or equal to the length of the dimensions \$dim.";
}
my $normalized_indexes_product = new R::NDArray::Int[$dim_length];
for (my $indexes_product_index = 0; $indexes_product_index < $dim_length; $indexes_product_index++) {
my $indexes = (R::NDArray::Int)undef;
if ($indexes_product_index < $indexes_product_length) {
$indexes = $indexes_product->[$indexes_product_index];
}
unless (!$indexes || $indexes->is_vector) {
die "The ${indexes_product_index}th element of the cartesian product of the indexes \$indexes_product must be a vector or undef.";
}
my $dim_elem = $dim->[$indexes_product_index];
if ($indexes) {
my $indexes_data = $indexes->data;
for (my $i = 0; $i < @$indexes_data; $i++) {
unless ($indexes_data->[$i] < $dim_elem) {
die "The ${i}th element of the data of the ${indexes_product_index}th element of the cartesian product of the indexes \$indexes_product must be less than the ${dim_elem}th element of the dimensions \$dim.";
}
}
}
else {
my $indexes_data = new int[$dim->[$indexes_product_index]];
for (my $i = 0; $i < $dim_elem; $i++) {
$indexes_data->[$i] = $i;
}
$indexes = R::NDArray::Int->new({data => $indexes_data});
}
$normalized_indexes_product->[$indexes_product_index] = $indexes;
}
return $normalized_indexes_product;
}
# Instance Methods
protected method init : void ($options : object[] = undef) {
my $options_h = Hash->new($options);
# data option
my $data = $options_h->get_or_default("data", undef);
if ($data) {
my $default_data = $self->create_default_data;
unless (type_name $data eq type_name $default_data) {
my $type_name = type_name $default_data;
die "The type of the data \$data must be $type_name type.";
}
}
# dim option
my $dim = (int[])$options_h->get_or_default("dim", undef);
unless ($dim) {
if ($data) {
my $data_length = Fn->array_length($data);
$dim = [$data_length];
}
else {
$dim = new int[0];
}
}
$dim = R::Util->normalize_dim($dim);
unless ($data) {
my $data_length = R::Util->calc_data_length($dim);
$data = $self->create_default_data($data_length);
}
R::Util->check_length($data, $dim);
$self->{data} = $data;
$self->{dim} = $dim;
}
method make_dim_read_only : void () {
$self->{is_dim_read_only} = 1;
}
method nrow : int () {
unless ($self->is_matrix) {
die "This instance must be a matrix.";
}
my $dim = $self->{dim};
my $nrow = $dim->[0];
return $nrow;
}
method ncol : int () {
unless ($self->is_matrix) {
die "This instance must be a matrix.";
}
my $dim = $self->{dim};
my $ncol = 1;
if (@$dim >= 2) {
$ncol = $dim->[1];
}
return $ncol;
}
method length : int () {
my $dim = $self->{dim};
my $length = R::Util->calc_data_length($dim);
return $length;
}
method is_empty : int () {
my $dim = $self->{dim};
my $is_empty = 0;
if (@$dim == 0) {
$is_empty = 1;
}
return $is_empty;
}
method is_scalar : int () {
my $dim = $self->{dim};
my $length = $self->length;
my $is_scalar = 0;
if (@$dim >= 1 && $length == 1) {
$is_scalar = 1;
}
return $is_scalar;
}
method is_vector : int () {
my $dim = $self->{dim};
my $dim_length = @$dim;
my $length = $self->length;
my $is_vector = 0;
if ($dim_length >= 1 && $dim->[0] == $length) {
$is_vector = 1;
}
return $is_vector;
}
method is_matrix : int () {
my $dim = $self->{dim};
my $dim_length = @$dim;
my $length = $self->length;
my $is_matrix = 0;
if (@$dim >= 1) {
my $nrow = -1;
my $ncol = -1;
if ($dim_length == 1) {
$nrow = $dim->[0];
$ncol = 1;
}
else {
$nrow = $dim->[0];
$ncol = $dim->[1];
}
if ($nrow * $ncol == $length) {
$is_matrix = 1;
}
}
return $is_matrix;
}
method is_square_matrix : int () {
my $dim = $self->{dim};
my $is_square_matrix = 0;
if ($self->is_matrix && $self->nrow == $self->ncol) {
$is_square_matrix = 1;
}
return $is_square_matrix;
}
method drop_dim : void ($index : int = -1) {
my $dim = $self->{dim};
my $dropped_dim = R::Util->drop_dim($dim, $index);
$self->set_dim($dropped_dim);
}
method expand_dim : void ($index : int = -1) {
my $dim = $self->{dim};
my $expandped_dim = R::Util->expand_dim($dim, $index);
$self->set_dim($expandped_dim);
}
method create_default_data : object ($length : int = 0) { die "Not implemented."; }
method elem_to_string : string ($data : object, $data_index : int) { die "Not impelmented."; }
method elem_assign : void ($dist_data : object, $dist_data_index : int, $src_data : object, $src_data_index : int) { die "Not impelmented."; }
method elem_clone : void ($dist_data : object, $dist_data_index : int, $src_data : object, $src_data_index : int) { die "Not impelmented."; }
method elem_cmp : int ($a_data : object, $a_data_index : int, $b_data : object, $b_data_index : int) { die "Not implemented."; }
method elem_is_na : int ($data : object, $data_index : int) { die "Not implemented."; }
precompile method to_string_ndarray : R::NDArray::String () {
unless ($self) {
die "This instance must be defined.";
}
my $length = $self->length;
my $dim = $self->dim;
my $ret_ndarray = R::OP::String->c(undef, $dim);
my $data = $self->data;
my $ret_data = $ret_ndarray->data;
for (my $i = 0; $i < $length; $i++) {
$ret_data->[$i] = $self->elem_to_string($data, $i);
}
return $ret_ndarray;
}
method elem_size : int () {
my $data = $self->{data};
my $elem_size = Fn->get_elem_size($data);
return $elem_size;
}
method elem_type_name : string () {
my $data = $self->{data};
my $elem_type_name = Fn->get_elem_type_name($data);
return $elem_type_name;
}
method is_numeric_ndarray : int () {
my $data = $self->{data};
my $is_numeric_ndarray = 0;
if (Fn->is_numeric_array($data)) {
$is_numeric_ndarray = 1;
}
return $is_numeric_ndarray;
}
method is_mulnum_ndarray : int () {
my $data = $self->{data};
my $is_mulnum_ndarray = 0;
if (Fn->is_mulnum_array($data)) {
$is_mulnum_ndarray = 1;
}
return $is_mulnum_ndarray;
}
method is_any_numeric_ndarray : int () {
my $any_numeric_ndarray = $self->is_numeric_ndarray || $self->is_mulnum_ndarray;
return $any_numeric_ndarray;
}
method is_object_ndarray : int () {
my $data = $self->{data};
my $is_object_ndarray = 0;
if (Fn->is_object_array($data)) {
$is_object_ndarray = 1;
}
return $is_object_ndarray;
}
method clone : R::NDArray ($shallow : int = 0) {
my $elem_size = $self->elem_size;
my $length = $self->length;
my $clone_data = $self->create_default_data($length);
my $data = $self->{data};
for (my $i = 0; $i < $length; $i++) {
if ($shallow) {
$self->elem_assign($clone_data, $i, $data, $i);
}
else {
$self->elem_clone($clone_data, $i, $data, $i);
}
}
my $clone_dim = $self->dim;
my $clone = (R::NDArray)Native::MethodCall->new_proto($self, [(object){data => $clone_data, dim => $clone_dim}]);
return $clone;
}
method slice : R::NDArray ($indexes_product : R::NDArray::Int[]) {
my $dim = $self->{dim};
$indexes_product = &normalize_indexes_product($indexes_product, $dim);
my $ret_dim = &create_ret_dim($indexes_product);
my $data_indexes = $self->create_data_indexes($indexes_product, $dim);
my $ret_data = $self->slice_data($data_indexes);
my $ret_ndarray = (R::NDArray)Native::MethodCall->new_proto($self, [(object){data => $ret_data, dim => $ret_dim}]);
return $ret_ndarray;
}
method slice_set : void ($indexes_product : R::NDArray::Int[], $ndarray : R::NDArray) {
my $dim = $self->{dim};
$indexes_product = &normalize_indexes_product($indexes_product, $dim);
my $ret_dim = &create_ret_dim($indexes_product);
my $data_indexes = $self->create_data_indexes($indexes_product, $dim);
my $equals_dropped_dim = R::Util->equals_dropped_dim($ndarray->dim, $ret_dim);
unless ($equals_dropped_dim) {
die "The dimensions of \$ndarray must be compatible with dim created by \$indexes_product.";
}
$self->slice_set_data($data_indexes, $ndarray->data);
}
method to_string : string () {
my $dim = $self->{dim};
my $dim_length = @$dim;
my $data = $self->{data};
my $string_buffer = StringBuffer->new;
if ($dim_length == 0) {
$string_buffer->push("Empty");
}
elsif ($dim_length == 1) {
my $data_length = $self->length;
for (my $i = 0; $i < $data_length; $i++) {
$string_buffer->push("[$i]\t");
my $elem_string = $self->elem_to_string($data, $i);
unless ($elem_string) {
$elem_string = "undef";
}
$string_buffer->push($elem_string);
unless ($i == $data_length - 1) {
$string_buffer->push("\n");
}
}
}
elsif ($dim_length == 2) {
my $nrow = $self->nrow;
$string_buffer->push("\t");
for (my $row = 0; $row < $dim->[1]; $row++) {
$string_buffer->push("[,$row]");
unless ($row == $nrow - 1) {
$string_buffer->push("\t");
}
}
$string_buffer->push("\n");
my $ncol = $self->ncol;
for (my $row = 0; $row < $nrow; $row++) {
$string_buffer->push("[$row]\t");
for (my $col = 0; $col < $ncol; $col++) {
my $coordinate = [$row, $col];
my $data_index = &convert_coordinate_to_data_index($coordinate, $dim);
my $elem_string = $self->elem_to_string($data, $data_index);
unless ($elem_string) {
$elem_string = "undef";
}
$string_buffer->push($elem_string);
unless ($col == $ncol - 1) {
$string_buffer->push("\t");
}
}
unless ($row == $nrow - 1) {
$string_buffer->push("\n");
}
}
}
else {
my $dim_length = @$dim;
my $dim_num = $dim_length - 1;
my $dim_list = IntList->new($dim);
my $coordinate_list = IntList->new;
&recursive_to_string($self, $dim_list, $string_buffer, \$dim_num, $coordinate_list);
}
my $string = (mutable string)$string_buffer->to_string;
while (length $string > 0 && $string->[length $string - 1] == '\n') {
Fn->chomp($string);
}
return $string;
}
method order : R::NDArray::Int () {
my $length = $self->length;
my $order_data_indexes_ndarray = R::OP::Int->seq(0, $length - 1);
my $order_data_indexes = $order_data_indexes_ndarray->data;
Sort->sort_int($order_data_indexes, [has that : R::NDArray = $self] method : int ($a_data_index : int, $b_data_index : int) {
return $self->{that}->elem_cmp($self->{that}->data, $a_data_index, $self->{that}->data, $b_data_index);
});
return $order_data_indexes_ndarray;
}
method set_order : void ($data_indexes_ndarray : R::NDArray::Int) {
$self->check_set_order_args($data_indexes_ndarray);
my $data_indexes = $data_indexes_ndarray->data;
my $data = $self->{data};
my $tmp = $self->clone;
my $tmp_data = $tmp->data;
my $length = $self->length;
for (my $i = 0; $i < $length; $i++) {
my $order_data_index = $data_indexes->[$i];
$self->elem_assign($data, $i, $tmp_data, $order_data_index);
}
}
method sort_asc : void () {
my $order_ndarray = $self->order;
$self->set_order($order_ndarray);
}
method sort_desc : void () {
my $order_ndarray = $self->order;
&reverse_int($order_ndarray->data);
$self->set_order($order_ndarray);
}
# Private Class Methods
private static method reverse_int : void ($array : int[]) {
unless ($array) {
die "The array \$array must be defined.";
}
my $length = @$array;
for (my $i = 0; $i < $length / 2; $i++) {
my $temp = $array->[$i];
$array->[$i] = $array->[$length - $i - 1];
$array->[$length - $i - 1] = $temp;
}
}
private static method create_ret_dim : int[] ($indexes_product : R::NDArray::Int[]) {
my $indexes_product_length = @$indexes_product;
my $ret_dim = new int[$indexes_product_length];
for (my $i = 0; $i < $indexes_product_length; $i++) {
my $indexes = $indexes_product->[$i];
if ($indexes) {
$ret_dim->[$i] = $indexes->length;
}
else {
die "[Unexpected]\$indexes must be defined.";
}
}
return $ret_dim;
}
precompile private static method convert_coordinate_to_data_index : int ($coordinate : int[], $dim : int[]) {
my $data_index = 0;
my $dim_length = @$dim;
for (my $i = 0; $i < $dim_length; $i++) {
if ($i > 0) {
my $tmp = 1;
for (my $k = 0; $k < $i; $k++) {
$tmp *= $dim->[$k];
}
$data_index += $tmp * $coordinate->[$i];
}
else {
$data_index += $coordinate->[$i];
}
}
return $data_index;
}
precompile static private method convert_data_index_to_coordinate : int[] ($data_index : int, $dim : int[]) {
my $dim_length = @$dim;
my $coordinate = new int[$dim_length];
my $before_dim_product = 1;
for (my $i = 0; $i < $dim_length; $i++) {
$before_dim_product *= $dim->[$i];
}
for (my $i = $dim_length - 1; $i >= 0; $i--) {
my $dim_product = 1;
for (my $k = 0; $k < $i; $k++) {
$dim_product *= $dim->[$k];
}
my $reminder = $data_index % $before_dim_product;
my $quotient = $reminder / $dim_product;
$coordinate->[$i] = $quotient;
$before_dim_product = $dim_product;
}
return $coordinate;
}
private static method recursive_to_string : void ($self : R::NDArray, $dim_list : IntList, $string_buffer : StringBuffer, $dim_num_ref : int*, $coordinate_list : IntList) {
my $data = $self->{data};
my $dim_elem = $dim_list->pop;
for (my $i = 0; $i < $dim_elem; $i++) {
$string_buffer->push("[");
$string_buffer->push(Fn->repeat(",", $$dim_num_ref) . "$i");
$string_buffer->push("]\n");
$coordinate_list->unshift($i);
if ($dim_list->length > 2) {
$$dim_num_ref--;
&recursive_to_string($self, $dim_list, $string_buffer, $dim_num_ref, $coordinate_list);
$$dim_num_ref++;
}
else {
$string_buffer->push("\t");
for (my $dim1 = 0; $dim1 < $dim_list->get(1); $dim1++) {
if ($dim1 == $dim_list->get(1)) {
$string_buffer->push("[,$dim1]\n");
}
else {
$string_buffer->push("[,$dim1]\t");
}
}
$string_buffer->push("\n");
for (my $dim0 = 0; $dim0 < $dim_list->get(0); $dim0++) {
$string_buffer->push("[$dim0]\t");
my $elem_strings_list = StringList->new;
for (my $dim1 = 0; $dim1 < $dim_list->get(1); $dim1++) {
my $coordinate_list_clone = IntList->new;
$coordinate_list_clone->push($dim0);
$coordinate_list_clone->push($dim1);
for (my $coordinate_index = 0; $coordinate_index < $coordinate_list->length; $coordinate_index++) {
$coordinate_list_clone->push($coordinate_list->get($coordinate_index));
}
my $dim = $self->dim;
for (my $tmp_dim_index = $coordinate_list_clone->length - 1; $tmp_dim_index < @$dim; $tmp_dim_index++) {
$coordinate_list_clone->push(0);
}
my $coordinate = $coordinate_list_clone->to_array;
my $data_index = &convert_coordinate_to_data_index($coordinate, $dim);
my $elem_string = $self->elem_to_string($data, $data_index);
unless ($elem_string) {
$elem_string = "undef";
}
$elem_strings_list->push($elem_string);
}
$string_buffer->push(Fn->join("\t", $elem_strings_list->to_array) . "\n");
}
}
$coordinate_list->shift;
$string_buffer->push("\n");
}
$dim_list->push($dim_elem);
}
# Private Instance Methods
private precompile method slice_data : object ($data_indexes : int[]) {
unless ($data_indexes) {
die "The data indexes \$data_indexes must be defined.";
}
my $data_indexes_length = @$data_indexes;
my $data = $self->data;
my $ret_data = $self->create_default_data($data_indexes_length);
for (my $i = 0; $i < $data_indexes_length; $i++) {
my $data_index = $data_indexes->[$i];
$self->elem_assign($ret_data, $i, $data, $data_index);
}
return $ret_data;
}
private precompile method slice_set_data : void ($data_indexes : int[], $data : object) {
my $self_data = $self->data;
unless ($data_indexes) {
die "The data indexes \$data_indexes must be defined.";
}
my $data_length = Fn->array_length($data);
for (my $i = 0; $i < $data_length; $i++) {
my $data_index = $data_indexes->[$i];
$self->elem_assign($self_data, $data_index, $data, $i);
}
}
private precompile method create_data_indexes : int[] ($indexes_product : R::NDArray::Int[], $dim : int[]) {
my $ret_dim = &create_ret_dim($indexes_product);
my $ret_dim_length = @$ret_dim;
my $ret_length = R::Util->calc_data_length($ret_dim);
my $ret_data = $self->create_default_data($ret_length);
my $data_indexes = new int[$ret_length];
for (my $ret_data_index = 0; $ret_data_index < $ret_length; $ret_data_index++) {
my $ret_coordinate = &convert_data_index_to_coordinate($ret_data_index, $ret_dim);
my $coordinate = new int[$ret_dim_length];
for (my $coordinate_index = 0; $coordinate_index < @$coordinate; $coordinate_index++) {
my $ret_coordinate_x = $ret_coordinate->[$coordinate_index];
$coordinate->[$coordinate_index] = $indexes_product->[$coordinate_index]->data->[$ret_coordinate_x];
if ($coordinate->[$coordinate_index] >= $dim->[$coordinate_index]) {
die "Invalid coordinate.";
}
}
my $data_index = &convert_coordinate_to_data_index($coordinate, $dim);
$data_indexes->[$ret_data_index] = $data_index;
}
return $data_indexes;
}
private method check_set_order_args : void ($indexes_ndarray : R::NDArray::Int) {
unless ($indexes_ndarray) {
die "The indexes \$indexes_ndarray must be defined.";
}
my $length = $self->length;
my $indexes_length = $indexes_ndarray->length;
unless ($indexes_length == $length) {
die "The length of the indexes \$indexes_ndarray must be the length of this vector.";
}
}
}