From Code to Community: Sponsoring The Perl and Raku Conference 2025 Learn more

use strict;
# $SIG{__DIE__ } = \&Graph::__carp_confess;
# $SIG{__WARN__} = \&Graph::__carp_confess;
my $empty = {};
sub _empty () { $empty }
my (@FLAGS, %FLAG_COMBOS, %FLAG2I, @FIELDS);
BEGIN {
@FLAGS = qw(_COUNT _MULTI _UNORD _REF _UNIONFIND _LIGHT _STR);
%FLAG_COMBOS = (
_COUNTMULTI => [qw(_COUNT _MULTI)],
_REFSTR => [qw(_REF _STR)],
);
# Next id, Flags, Arity, Index to path, path to index,
# successors, predecessors: 2-level hashes to array-ref of path IDs
# attributes - two-level for MULTI, node/multi count
@FIELDS = qw(_n _f _arity _i _pi _s _p _attr _count);
for my $i (0..$#FLAGS) {
my $n = $FLAGS[$i];
my $f = 1 << $i;
$FLAG2I{$n} = $f;
no strict 'refs';
*$n = sub () { $f };
*{"_is$n"} = sub { $_[0]->[ 1 ] & $f }; # 1 = _f
}
for my $k (keys %FLAG_COMBOS) {
my $f = 0;
$f |= $_ for map $FLAG2I{$_}, @{ $FLAG_COMBOS{$k} };
no strict 'refs';
*$k = sub () { return $f }; # return to dodge pointless 5.22 stricture
*{"_is$k"} = sub { $_[0]->[ 1 ] & $f }; # 1 = _f
}
for my $i (0..$#FIELDS) {
no strict 'refs';
*{ $FIELDS[$i] }= sub () { $i };
}
}
sub _new {
my ($class, $flags, $arity) = @_;
my $hyper = !$arity;
my $need_s = $arity != 1;
my $need_p = $need_s && !($flags & _UNORD);
bless [
0, $flags, $arity, [], {},
($need_s ? {} : undef), ($need_p ? {} : undef),
[], [],
], $class;
}
require Exporter;
use vars qw(@ISA @EXPORT_OK %EXPORT_TAGS);
@ISA = qw(Exporter);
%EXPORT_TAGS =
(flags => [@FLAGS, keys %FLAG_COMBOS, qw(_GEN_ID)],
fields => \@FIELDS);
@EXPORT_OK = map @$_, values %EXPORT_TAGS;
my $_GEN_ID = 0;
sub _GEN_ID () { \$_GEN_ID }
sub stringify {
my ($f, $arity, $m) = (@{ $_[0] }[ _f, _arity ], $_[0]);
my ($multi, @rows) = $f & _MULTI;
my @p = $m->paths;
@p = $arity == 1 ? sort @p :
map $_->[0], sort { $a->[1] cmp $b->[1] }
($arity == 0 && !($f & _UNORD))
? map [$_, join '|', map "@$_", @$_], @p
: map [$_,"@$_"], @p; # use the Schwartz
if ($arity == 2) {
require Set::Object;
my ($pre, $suc, @s) = (Set::Object->new(map $_->[0], @p), Set::Object->new(map $_->[1], @p));
@rows = ([ 'to:', @s = sort $suc->members ], map {
my $p = $_;
[ $p, map {
my $text = defined(my $id = $m->has_path([$p, $_])) ? 1 : '';
my $attrs = !$text ? undef :
$multi ? $m->[ _attr ][$id] : $m->_get_path_attrs([$p, $_]);
defined $attrs ? $m->_dumper($attrs) : $text;
} @s ];
} sort $pre->members);
} else {
@rows = map {
my $attrs = $multi
? $m->[ _attr ][ $m->has_path($_) ] : $m->_get_path_attrs($_);
[ $m->_dumper($_),
($m->get_ids_by_paths([ $_ ], 0))[0].
(!defined $attrs ? '' : ",".$m->_dumper($attrs)) ];
} @p;
}
join '',
map "$_\n",
"@{[ref $m]} arity=$arity flags: @{[_stringify_fields($m->[ _f ])]}",
map join(' ', map sprintf('%4s', $_), @$_),
@rows;
}
sub _stringify_fields {
return '0' if !$_[0];
join '|', grep $_[0] & $FLAG2I{$_}, @FLAGS;
}
sub _dumper {
my (undef, $got) = @_;
return $got if defined $got and !ref $got;
require Data::Dumper;
my $dumper = Data::Dumper->new([$got]);
$dumper->Indent(0)->Terse(1);
$dumper->Sortkeys(1) if $dumper->can("Sortkeys");
$dumper->Dump;
}
sub has_any_paths {
scalar keys %{ $_[0]->[ _pi ] };
}
sub _set_path_attr_common {
push @_, 0;
my ($i) = &__set_path;
my $attr = (my $m = $_[0])->[ _attr ];
($m->[ _f ] & _MULTI) ? \$attr->[ $i ]{ $_[2] } : \$attr->[ $i ];
}
sub _set_path_attrs {
${ &{ $_[0]->can('_set_path_attr_common') } } = $_[-1];
}
sub _set_path_attr {
${ &{ $_[0]->can('_set_path_attr_common') } }->{ $_[-2] } = $_[-1];
}
sub set_paths {
map +($_[0]->__set_path($_, 1))[0], @_[1..$#_];
}
sub set_path_by_multi_id {
push @_, 1;
goto &__set_path;
}
sub __set_path {
my $inc_if_exists = pop;
&__arg;
my ($f, $a, $map_i, $pi, $map_s, $map_p, $m, $k, $id) = (@{ $_[0] }[ _f, _arity, _i, _pi, _s, _p ], @_);
my $is_multi = $f & _MULTI;
my $k_orig = $k;
$k = __strval($k, $f) if $a == 1 && ($f & _REF) && ref($k);
my $l = ($a == 0 && !($f & _UNORD)) ? join '|', map join(' ', sort @$_), @$k : $a == 1 ? "$k" : "@$k";
if (exists $pi->{ $l }) {
return ($pi->{ $l }) if !($inc_if_exists and ($f & _COUNTMULTI));
my $nc = \$m->[ _count ][ my $i = $pi->{ $l } ];
$$nc++, return ($i) if !$is_multi;
my $na = $m->[ _attr ][ $i ];
if ($id eq _GEN_ID) {
$$nc++ while exists $na->{ $$nc };
$id = $$nc;
}
$na->{ $id } = { };
return ($i, $id);
}
$map_i->[ $pi->{ $l } = my $i = $m->[ _n ]++ ] = $k_orig;
$m->[ _attr ][ $i ] = { ($id = ($id eq _GEN_ID) ? 0 : $id) => {} } if $is_multi;
$m->[ _count ][ $i ] = $is_multi ? 0 : 1 if ($f & _COUNTMULTI);
_successors_add($f, $a, $map_s, $map_p, $i, $k) if $map_s; # dereffed
($i, $id);
}
sub _successors_add {
my ($f, $a, $map_s, $map_p, $id, $path) = @_;
my $pairs = _successors_cartesian(($f & _UNORD), $a == 0, $path);
push @{ $map_s->{ $_->[0] }{ $_->[1] } }, $id for @$pairs;
return if !$map_p;
push @{ $map_p->{ $_->[1] }{ $_->[0] } }, $id for @$pairs;
}
sub _successors_del {
my ($f, $a, $map_s, $map_p, $id, $path) = @_;
my $pairs = _successors_cartesian(($f & _UNORD), $a == 0, $path);
for (@$pairs) {
my ($p, $s) = @$_;
my @new = grep $_ != $id, @{ $map_s->{ $p }{ $s } };
if (@new) {
$map_s->{ $p }{ $s } = \@new;
$map_p->{ $s }{ $p } = \@new if $map_p;
next;
}
delete $map_s->{ $p }{ $s };
delete $map_s->{ $p } if !keys %{ $map_s->{ $p } };
next if !$map_p;
delete $map_p->{ $s }{ $p };
delete $map_p->{ $s } if !keys %{ $map_p->{ $s } };
}
}
sub _successors_cartesian {
my ($unord, $hyper, $seq) = @_;
return [ $seq ] if !$unord and !$hyper;
return [] if $unord and $hyper and !@$seq;
my ($allow_self, $p_s, $s_s, @pairs);
if ($unord) {
require Set::Object;
my @a = Set::Object->new(@$seq)->members;
($allow_self, $p_s, $s_s) = (@a < 2, \@a, \@a);
} else {
($allow_self, $p_s, $s_s) = (1, @$seq);
}
for my $p (@$p_s) {
push @pairs, map [$p, $_], $allow_self ? @$s_s : grep $p != $_, @$s_s;
}
\@pairs;
}
sub _get_path_count {
return 0 unless my ($i) = &__has_path;
my $f = (my $m = $_[0])->[ _f ];
return
($f & _COUNT) ? $m->[ _count ][ $i ] :
($f & _MULTI) ? scalar keys %{ $m->[ _attr ][ $i ] } : 1;
}
sub has_path {
( &__has_path )[0];
}
sub has_path_by_multi_id {
return undef unless my ($i) = &__has_path;
return exists $_[0]->[ _attr ][ $i ]{ $_[2] };
}
sub del_path {
return unless my ($i, $l) = &__has_path;
return 1 if &_is_COUNT and --$_[0][ _count ][ $i ] > 0;
$_[0]->_sequence_del($i, $l);
1;
}
sub del_path_by_multi_id {
return unless my ($i, $l) = &__has_path;
delete((my $attrs = (my $m = $_[0])->[ _attr ][ $i ])->{ $_[2] });
return 1 if keys %$attrs;
$m->_sequence_del($i, $l);
1;
}
sub get_multi_ids {
return unless ((my $m = $_[0])->[ _f ] & _MULTI) and my ($i) = &__has_path;
keys %{ $m->[ _attr ][ $i ] };
}
sub rename_path {
my ($m, $from, $to) = @_;
return 1 if $m->[ _arity ] != 1; # all integers, no names
return unless my ($i, $l) = $m->__has_path($from);
$m->[ _i ][ $i ] = $to;
$to = __strval($to, $m->[ _f ]) if ref($to) and ($m->[ _f ] & _REF);
$m->[ _pi ]{ $to } = delete $m->[ _pi ]{ $l };
return 1;
}
sub _del_path_attrs {
return unless my ($i) = &__has_path;
my $attr = (my $m = $_[0])->[ _attr ];
return $attr->[ $i ]{ $_[2] } = undef, 1 if ($m->[ _f ] & _MULTI);
delete $attr->[ $i ];
}
sub __has_path {
&__arg;
my ($f, $a, $pi, $k) = (@{ $_[0] }[ _f, _arity, _pi ], $_[1]);
$k = __strval($k, $f) if $a == 1 && ($f & _REF) && ref($k);
my $l = ($a == 0 && !($f & _UNORD)) ? join '|', map join(' ', sort @$_), @$k : $a == 1 ? "$k" : "@$k";
my $id = $pi->{ $l };
(defined $id ? $id : return, $l);
}
sub _get_path_attrs {
return unless my ($i) = &__has_path;
my $attrs = (my $m = $_[0])->[ _attr ][ $i ];
($m->[ _f ] & _MULTI) ? $attrs->{ $_[2] } : $attrs;
}
sub _has_path_attrs {
keys %{ &{ $_[0]->can('_get_path_attrs') } || return undef } ? 1 : 0;
}
sub _has_path_attr {
exists(( &{ $_[0]->can('_get_path_attrs') } || return )->{ $_[-1] });
}
sub _get_path_attr {
( &{ $_[0]->can('_get_path_attrs') } || return )->{ $_[-1] };
}
sub _get_path_attr_names {
keys %{ &{ $_[0]->can('_get_path_attrs') } || return };
}
sub _get_path_attr_values {
values %{ &{ $_[0]->can('_get_path_attrs') } || return };
}
sub _del_path_attr {
return unless my $attrs = &{ $_[0]->can('_get_path_attrs') };
return 0 unless exists $attrs->{ my $attr = $_[-1] };
delete $attrs->{$attr};
return 1 if keys %$attrs;
&{ $_[0]->can('_del_path_attrs') };
1;
}
sub _sequence_del {
my ($m, $id, $l) = @_;
my ($f, $a, $map_i, $pi, $map_s, $map_p) = @$m[ _f, _arity, _i, _pi, _s, _p ];
delete $pi->{ $l };
delete $m->[ $_ ][ $id ] for _count, _attr;
my $path = delete $map_i->[ $id ];
_successors_del($f, $a, $map_s, $map_p, $id, $path) if $map_s;
return 1;
}
sub get_paths_by_ids {
my ($i, undef, $list, $deep) = ( @{ $_[0] }[ _i ], @_ );
$deep ? map [ map [ @$i[ @$_ ] ], @$_ ], @$list : map [ @$i[ @$_ ] ], @$list;
}
sub paths {
grep defined, @{ $_[0]->[ _i ] || Graph::_empty_array() };
}
sub ids {
values %{ $_[0]->[ _pi ] || Graph::_empty_array() };
}
sub get_ids_by_paths {
my ($f, $a, $pi, $m, $list, $ensure, $deep) = ( @{ $_[0] }[ _f, _arity, _pi ], @_ );
$deep ||= 0;
my ($is_multi, $is_ref, $is_unord) = (map $f & $_, _MULTI, _REF, _UNORD);
return map { # Fast path
my @ret = map {
my $id = $pi->{ $a != 1 ? "@$_" : $_ };
defined $id ? $id :
!$ensure ? return :
($is_multi ? $m->set_path_by_multi_id($_, _GEN_ID) : $m->set_paths($_))[0];
} $deep ? @$_ : $_;
$deep ? \@ret : @ret;
} @$list if $a and !$is_ref and $deep < 2;
map {
my @ret = map {
my @ret2 = map {
my $k = $_;
$k = __strval($k, $f) if $a == 1 && $is_ref && ref($k);
my $l = ($a == 0 && !$is_unord) ? join '|', map join(' ', sort @$_), @$k : $a == 1 ? "$k" : "@$k";
my $id = $pi->{ $l };
defined $id ? $id :
!$ensure ? return :
($is_multi ? $m->set_path_by_multi_id($_, _GEN_ID) : $m->set_paths($_))[0];
} $deep > 1 ? @$_ : $_;
$deep > 1 ? \@ret2 : @ret2;
} $deep ? @$_ : $_;
$deep ? \@ret : @ret;
} @$list;
}
sub _paths_fromto {
my $offset = pop;
my ($i, $map_x, @v) = ( @{ $_[0] }[ _i, $offset ], @_[1..$#_] );
Graph::__carp_confess("undefined vertex") if grep !defined, @v;
require Set::Object;
map $i->[ $_ ], Set::Object->new(map @$_, map values %{ $map_x->{ $_ } || _empty }, @v)->members;
}
sub paths_from { push @_, _s; goto &_paths_fromto }
sub paths_to { push @_, _p; goto &_paths_fromto }
sub _cessors {
my $offset = pop;
my ($map_x, @v) = ( @{ $_[0] }[ $offset ], @_[1..$#_] );
Graph::__carp_confess("undefined vertex") if grep !defined, @v;
require Set::Object;
Set::Object->new(map keys %{ $map_x->{ $_ } || _empty }, @v)->members;
}
sub successors { push @_, _s; goto &_cessors }
sub predecessors { push @_, _p; goto &_cessors }
sub has_successor {
my ($map_s, $u, $v) = ( @{ $_[0] }[ _s ], @_[1, 2] );
Graph::__carp_confess("undefined vertex") if grep !defined, $u, $v;
exists ${ $map_s->{ $u } || _empty }{ $v };
}
sub __strval {
my ($k, $f) = @_;
return $k unless ref $k && ($f & _REF);
return "$k" if ($f & _STR);
require Scalar::Util;
Scalar::Util::refaddr($k);
}
sub __arg {
my ($f, $a, $m, $k) = (@{ $_[0] }[ _f, _arity ], @_[0, 1]);
Graph::__carp_confess(sprintf "arguments %d (%s) expected %d for\n".$m->stringify,
scalar @$k, "@$k", $a)
if $a > 1 and @$k != $a;
}
sub reindex {
my ($f, $a, $i2p, $m) = (@{ $_[0] }[ _f, _arity, _i ], $_[0]);
my $is_ref = $a == 1 && ($f & _REF);
my $pi = $m->[ _pi ] = {};
for my $i ( 0..$#{ $i2p } ) {
next if !defined(my $k = $i2p->[ $i ]); # deleted
$k = __strval($k, $f) if $is_ref && ref($k);
$pi->{ $k } = $i;
}
}
1;
__END__
=pod
=head1 NAME
Graph::AdjacencyMap - map of graph vertices or edges
=head1 SYNOPSIS
Internal.
=head1 DESCRIPTION
B<This module is meant for internal use by the Graph module.>
=head1 OBJECT METHODS
=head2 del_path(\@seq)
Delete a Map path.
=head2 del_path_by_multi_id(\@seq, $id)
Delete a Map path by a multi(vertex) id.
=head2 get_multi_ids(\@seq)
Return the multi ids.
=head2 has_path(\@seq)
Returns the integer ID of the path, or undef if Map doesn't have it.
=head2 has_any_paths
Return true if the Map has any paths, false if not.
=head2 has_path_by_multi_id(\@seq, $id)
Return true if the Map has the path by a multi(vertex) id, false if not.
=head2 paths
Return all the paths (left-hand sides) of the Map.
=head2 ids
Return all the right-hand sides of the Map, unsorted.
=head2 set_paths(\@seq1, \@seq2, ...)
@ids = set_paths($seq1, $seq2, ...)
Create/identify the path of C<$seq*>. Returns the integer ID of each path.
For arity other than 1, the sequence items must be integers.
For arity 1, do not wrap the item in an array.
For C<_UNORD>, you must give the sequence already sorted.
=head2 set_path_by_multi_id(\@seq, $id)
($integer_ID, $multi_ID) = $m->set_path_by_multi_id(\@seq, $id)
Set the path in the Map by the multi id.
=head2 get_paths_by_ids([ \@idlist1, \@idlist2... ], $deep)
Given an array-ref of array-refs of vertex IDs, returns a list of
array-refs of vertex-names.
This is to look up vertex paths for use in edges. Only useful for arity 1.
The C<$deep> option is useful with directed hyperedges.
=head2 get_ids_by_paths
@ids = $m->get_ids_by_paths([ \@seq1, \@seq2... ], $ensure, 0);
@id_lists = $m->get_ids_by_paths([ \@seq1, \@seq2... ], $ensure, 1);
This is to look up vertex IDs for use in edges. Only useful for arity 1.
Given an array-ref of array-refs with paths, returns a list of IDs of
existing paths.
If C<$ensure> is true, will first create paths that do not already exist.
If it is not, any non-existing paths will cause an empty list to be returned.
If $deep is true, each sequence will be treated as a list of paths,
and IDs filled in for the return values. This can have a value up to 2.
=head2 rename_path($from, $to)
Rename the path.
=head2 stringify
Return a string describing the object in a human-friendly(ish) way.
=head2 successors
@successors = $m->successors(@v)
Only valid for a map of arity other than 1.
=head2 predecessors
@predecessors = $m->predecessors($v)
Only valid for a non-C<_UNORD> map of arity other than 1.
=head2 paths_from
@paths = $m->paths_from(@v)
Only valid for a map of arity other than 1.
=head2 paths_to
@paths = $m->paths_to($v)
Only valid for a non-C<_UNORD> map of arity other than 1.
=head2 has_successor
$bool = $m->has_successor($u, $v)
Only valid for a map of arity other than 1.
=head2 reindex
Will recreate the mapping from paths to indexes. Intended for use after
a deep copy.
=head1 AUTHOR AND COPYRIGHT
Jarkko Hietaniemi F<jhi@iki.fi>
=head1 LICENSE
This module is licensed under the same terms as Perl itself.
=cut