Group
Extension

PDK-Utils/lib/PDK/Utils/Set.pm

package PDK::Utils::Set;

use utf8;
use v5.30;
use Moose;
use namespace::autoclean;
use Carp;
use POSIX;
use Data::Dumper;
use experimental 'smartmatch';

our $VERSION = '0.005';

# 最小值数组属性,存储每个区间的最小值
has mins => (
    is      => 'rw',            # 读写属性
    isa     => 'ArrayRef[Int]', # 整数数组引用类型
    default => sub {[]},        # 默认值为空数组
);

# 最大值数组属性,存储每个区间的最大值
has maxs => (
    is      => 'rw',            # 读写属性
    isa     => 'ArrayRef[Int]', # 整数数组引用类型
    default => sub {[]},        # 默认值为空数组
);

# 构建参数处理
around BUILDARGS => sub {
    my $orig = shift;
    my $class = shift;

    if (@_ == 0) {
        # 无参数调用,使用默认值
        return $class->$orig();
    }
    elsif (@_ == 1 and ref($_[0]) eq __PACKAGE__) {
        # 单个参数且为同类对象,复制其区间
        my $setObj = $_[0];
        return $class->$orig(mins => [ @{$setObj->mins} ], maxs => [ @{$setObj->maxs} ]);
    }
    elsif (@_ == 2 and defined $_[0] and defined $_[1] and $_[0] =~ /^\d+$/o and $_[1] =~ /^\d+$/o) {
        # 两个数字参数,创建一个区间
        my ($MIN, $MAX) = $_[0] < $_[1] ? ($_[0], $_[1]) : ($_[1], $_[0]);
        return $class->$orig(mins => [ $MIN ], maxs => [ $MAX ]);
    }
    else {
        # 其他情况,直接传递参数
        return $class->$orig(@_);
    }
};

# 对象构建验证
sub BUILD {
    my $self = shift;
    my @ERROR;
    my $lengthOfMin = @{$self->mins};
    my $lengthOfMax = @{$self->maxs};

    # 检查最小值和最大值数组长度是否一致
    if ($lengthOfMin != $lengthOfMax) {
        push(@ERROR, '属性 (mins) 和 (maxs) 在构造函数 ' . __PACKAGE__ . ' 中必须保持相同长度');
    }

    # 检查每个区间的最小值是否不大于最大值
    for (my $i = 0; $i < $lengthOfMin; $i++) {
        if ($self->mins->[$i] > $self->maxs->[$i]) {
            push(@ERROR, '在构造函数 ' . __PACKAGE__ . ' 中,同一索引位置的 (mins) 不能大于 (maxs)');
            last;
        }
    }

    # 如果有错误,抛出异常
    if (@ERROR > 0) {
        confess(join(', ', @ERROR));
    }
}

# 获取区间数量
sub length {
    my $self = shift;
    my $lengthOfMin = @{$self->mins};
    my $lengthOfMax = @{$self->maxs};

    # 验证最小值和最大值数组长度一致
    confess("错误: 属性 (mins) 的长度($lengthOfMin) 与 (maxs) 的长度($lengthOfMax) 不一致") if $lengthOfMin != $lengthOfMax;

    return $lengthOfMin;
}

# 获取整体最小值
sub min {
    my $self = shift;
    return ($self->length > 0 ? $self->mins->[0] : undef);
}

# 获取整体最大值
sub max {
    my $self = shift;
    return ($self->length > 0 ? $self->maxs->[-1] : undef);
}

# 输出所有区间
sub dump {
    my $self = shift;
    my $length = $self->length;
    for (my $i = 0; $i < $length; $i++) {
        say $self->mins->[$i] . "  " . $self->maxs->[$i]
    }
}

# 向集合中添加区间(不检查重叠)
# 注意:使用该方法时需特别谨慎,只有在确定输入与现有区间不重叠时才能使用,否则可能导致错误。
sub addToSet {
    my ($self, $MIN, $MAX) = @_;

    # 确保最小值不大于最大值
    if ($MIN > $MAX) {
        ($MAX, $MIN) = ($MIN, $MAX)
    }

    my $length = $self->length;

    # 如果集合为空,直接添加
    if ($length == 0) {
        $self->mins([ $MIN ]);
        $self->maxs([ $MAX ]);
        return;
    }

    my $minArray = $self->mins;
    my $maxArray = $self->maxs;
    my $index;

    # 查找插入位置
    for (my $i = 0; $i < $length; $i++) {
        if ($MIN < $minArray->[$i]) {
            $index = $i;
            last;
        }
    }

    # 如果未找到插入位置,说明应插入到末尾
    $index = $length if not defined $index;

    # 构建新的区间数组
    my (@min, @max);
    push(@min, @{$minArray}[0 .. $index - 1]);
    push(@max, @{$maxArray}[0 .. $index - 1]);
    push(@min, $MIN);
    push(@max, $MAX);
    push(@min, @{$minArray}[$index .. $length - 1]);
    push(@max, @{$maxArray}[$index .. $length - 1]);

    $self->mins(\@min);
    $self->maxs(\@max);
}

# 合并区间到集合(检查并合并重叠区间)
sub mergeToSet {
    my $self = shift;

    # 处理单个Set对象参数
    if (@_ == 1 and ref($_[0]) eq __PACKAGE__) {
        my $setObj = $_[0];
        my $length = $setObj->length;
        for (my $i = 0; $i < $length; $i++) {
            $self->_mergeToSet($setObj->mins->[$i], $setObj->maxs->[$i]);
        }
    }
    else {
        # 处理两个数字参数
        $self->_mergeToSet(@_);
    }
}

# 内部方法:合并单个区间到集合
sub _mergeToSet {
    my ($self, $MIN, $MAX) = @_;

    # 确保最小值不大于最大值
    if ($MIN > $MAX) {
        ($MAX, $MIN) = ($MIN, $MAX)
    }

    my $length = $self->length;

    # 如果集合为空,直接添加
    if ($length == 0) {
        $self->mins([ $MIN ]);
        $self->maxs([ $MAX ]);
        return;
    }

    my $minArray = $self->mins;
    my $maxArray = $self->maxs;
    my ($minIndex, $maxIndex) = (-1, $length);

    # 查找新区间的最小值位置
    MIN: {
        for (my $i = 0; $i < $length; $i++) {
            if ($MIN >= $minArray->[$i] and $MIN <= $maxArray->[$i] + 1) {
                $minIndex = $i;
                last MIN;
            }
            elsif ($MIN < $minArray->[$i]) {
                $minIndex += 0.5;
                last MIN;
            }
            else {
                $minIndex++;
            }
        }
        $minIndex += 0.5;
    }

    # 查找新区间的最大值位置
    MAX: {
        for (my $j = $length - 1; $j >= $minIndex; $j--) {
            if ($MAX >= $minArray->[$j] - 1 and $MAX <= $maxArray->[$j]) {
                $maxIndex = $j;
                last MAX;
            }
            elsif ($MAX > $maxArray->[$j]) {
                $maxIndex -= 0.5;
                last MAX;
            }
            else {
                $maxIndex--;
            }
        }
        $maxIndex -= 0.5;
    }

    # 计算整数索引位置
    # min使用向上取整,即 POSIX::ceil(0.5) == 1 ,  POSIX::ceil(1) == 1
    # max使用向下取整,即 POSIX::floor(-0.5) == -1 ,  POSIX::floor(-1) == -1
    my $minIndexInt = POSIX::ceil($minIndex);
    my $maxIndexInt = POSIX::floor($maxIndex);
    my $isMinIndexInSet = $minIndex == $minIndexInt ? 1 : 0;
    my $isMaxIndexInSet = $maxIndex == $maxIndexInt ? 1 : 0;

    # 构建合并后的区间数组
    my (@min, @max);
    push(@min, @{$minArray}[0 .. $minIndexInt - 1]);
    push(@max, @{$maxArray}[0 .. $minIndexInt - 1]);
    push(@min, $isMinIndexInSet ? $minArray->[$minIndexInt] : $MIN);
    push(@max, $isMaxIndexInSet ? $maxArray->[$maxIndexInt] : $MAX);
    push(@min, @{$minArray}[$maxIndexInt + 1 .. $length - 1]);
    push(@max, @{$maxArray}[$maxIndexInt + 1 .. $length - 1]);

    $self->mins(\@min);
    $self->maxs(\@max);
}

# 比较两个集合的关系
sub compare {
    my ($self, $setObj) = @_;
    if ($self->isEqual($setObj)) {
        return 'equal'; # 相等
    }
    elsif ($self->_isContain($setObj)) {
        return 'containButNotEqual'; # 包含但不相等
    }
    elsif ($self->_isBelong($setObj)) {
        return 'belongButNotEqual'; # 属于但不相等
    }
    else {
        return 'other'; # 其他关系
    }
}

# 判断两个集合是否相等
sub isEqual {
    my ($self, $setObj) = @_;
    return (@{$self->mins} ~~ @{$setObj->mins} and @{$self->maxs} ~~ @{$setObj->maxs});
}

# 判断两个集合是否不相等
sub notEqual {
    my ($self, $setObj) = @_;
    return !(@{$self->mins} ~~ @{$setObj->mins} and @{$self->maxs} ~~ @{$setObj->maxs});
}

# 判断当前集合是否包含另一个集合(包含相等情况)
sub isContain {
    my ($self, $setObj) = @_;
    if ($self->isEqual($setObj)) {
        return 1;
    }
    else {
        return $self->_isContain($setObj);
    }
}

# 内部方法:判断当前集合是否包含另一个集合(不包含相等情况)
sub _isContain {
    my ($self, $setObj) = @_;
    my $copyOfSelf = PDK::Utils::Set->new($self);
    $copyOfSelf->mergeToSet($setObj);
    return $self->isEqual($copyOfSelf);
}

# 判断当前集合是否包含另一个集合但不相等
sub isContainButNotEqual {
    my ($self, $setObj) = @_;
    if ($self->isEqual($setObj)) {
        return 0;
    }
    else {
        return $self->_isContain($setObj);
    }
}

# 判断当前集合是否属于另一个集合(包含相等情况)
sub isBelong {
    my ($self, $setObj) = @_;
    if ($self->isEqual($setObj)) {
        return 1;
    }
    else {
        return $self->_isBelong($setObj);
    }
}

# 内部方法:判断当前集合是否属于另一个集合(不包含相等情况)
sub _isBelong {
    my ($self, $setObj) = @_;
    my $copyOfSetObj = PDK::Utils::Set->new($setObj);
    $copyOfSetObj->mergeToSet($self);
    return $setObj->isEqual($copyOfSetObj);
}

# 判断当前集合是否属于另一个集合但不相等
sub isBelongButNotEqual {
    my ($self, $setObj) = @_;
    if ($self->isEqual($setObj)) {
        return 0;
    }
    else {
        return $self->_isBelong($setObj);
    }
}

# 计算两个集合的交集
sub interSet {
    my $result = PDK::Utils::Set->new;
    my ($self, $setObj) = @_;

    # 如果任一集合为空,返回空集合
    if ($self->length == 0) {
        return $self;
    }
    if ($setObj->length == 0) {
        return $setObj;
    }

    # 使用双指针法计算交集
    my $i = 0;
    my $j = 0;

    while ($i < $self->length and $j < $setObj->length) {
        my @rangeSet1 = ($self->mins->[$i], $self->maxs->[$i]);
        my @rangeSet2 = ($setObj->mins->[$j], $setObj->maxs->[$j]);
        my ($min, $max) = $self->interRange(\@rangeSet1, \@rangeSet2);
        $result->_mergeToSet($min, $max) if defined $min;

        # 移动指针
        if ($setObj->maxs->[$j] > $self->maxs->[$i]) {
            $i++;
        }
        elsif ($setObj->maxs->[$j] == $self->maxs->[$i]) {
            $i++;
            $j++;
        }
        else {
            $j++;
        }
    }
    return $result;
}

# 计算两个区间的交集
sub interRange {
    my ($self, $rangeSet1, $rangeSet2) = @_;
    my ($min, $max);

    # 计算交集的最小值和最大值
    $min = $rangeSet1->[0] > $rangeSet2->[0] ? $rangeSet1->[0] : $rangeSet2->[0];
    $max = $rangeSet1->[1] < $rangeSet2->[1] ? $rangeSet1->[1] : $rangeSet2->[1];

    # 如果没有交集,返回空
    if ($min > $max) {
        return;
    }
    else {
        return ($min, $max);
    }
}

# 为方法添加参数验证
for my $func (qw(addToSet _mergeToSet)) {
    before $func => sub {
        my $self = shift;
        unless (@_ == 2 and $_[0] =~ /^\d+$/o and $_[1] =~ /^\d+$/o) {
            confess("错误: 方法 $func 只能接收两个数字参数");
        }
    }
}

# 为比较方法添加参数验证
for my $func (qw(compare isEqual isContain _isContain isContainButNotEqual isBelong _isBelong isBelongButNotEqual)) {
    before $func => sub {
        my $self = shift;
        confess("错误: 方法($func) 的第一个参数必须是 PDK::Utils::Set 对象") if ref($_[0]) ne 'PDK::Utils::Set';
    }
};

__PACKAGE__->meta->make_immutable;
1;

=encoding utf8
=head1 NAME

PDK::Utils::Set - 区间集合操作工具类

=head1 SYNOPSIS

    use PDK::Utils::Set;

    # 创建区间集合
    my $set = PDK::Utils::Set->new(1, 5);
    $set->addToSet(10, 20);

    # 合并区间
    $set->mergeToSet(6, 9);

    # 打印集合
    $set->dump;

    # 交集
    my $set2 = PDK::Utils::Set->new(4, 15);
    my $inter = $set->interSet($set2);
    $inter->dump;

    # 比较关系
    say $set->compare($set2);   # equal / containButNotEqual / belongButNotEqual / other

=head1 DESCRIPTION

该模块提供了一个基于区间的集合类,实现了区间的添加、合并、比较、交集等常用操作。

=head1 ATTRIBUTES

=over 4

=item mins

整数数组引用,存储区间集合中每个区间的最小值。

=item maxs

整数数组引用,存储区间集合中每个区间的最大值。

=back

=head1 CONSTRUCTOR

=over 4

=item new()

无参数构造,创建一个空集合。

=item new($setObj)

传入同类对象,复制其区间。

=item new($MIN, $MAX)

传入两个整数,创建一个单区间集合。

=item new(%args)

传入参数哈希,手动指定 C<mins> 与 C<maxs>。

=back

=head1 METHODS

=head2 基础操作

=over 4

=item length()

返回集合中的区间数量。

=item min()

返回集合整体的最小值(第一个区间的最小值),若集合为空返回 undef。

=item max()

返回集合整体的最大值(最后一个区间的最大值),若集合为空返回 undef。

=item dump()

打印所有区间,格式为 "MIN  MAX"。

=back

=head2 集合构造与合并

=over 4

=item addToSet($MIN, $MAX)

向集合中插入区间(不检查重叠,需保证输入与现有区间不冲突)。

=item mergeToSet($MIN, $MAX | $setObj)

向集合中合并区间,若重叠或相邻则会自动合并。

=back

=head2 集合比较

=over 4

=item compare($setObj)

比较两个集合的关系,返回以下字符串之一:

=over 8

=item * equal - 两个集合完全相等

=item * containButNotEqual - 当前集合包含对方但不相等

=item * belongButNotEqual - 当前集合属于对方但不相等

=item * other - 其他关系

=back

=item isEqual($setObj)

判断集合是否相等。

=item notEqual($setObj)

判断集合是否不相等。

=item isContain($setObj)

判断当前集合是否包含另一个集合(包含相等情况)。

=item isContainButNotEqual($setObj)

判断当前集合是否包含另一个集合但不相等。

=item isBelong($setObj)

判断当前集合是否属于另一个集合(包含相等情况)。

=item isBelongButNotEqual($setObj)

判断当前集合是否属于另一个集合但不相等。

=back

=head2 集合运算

=over 4

=item interSet($setObj)

返回两个集合的交集,结果为新的集合对象。

=item interRange(\@rangeSet1, \@rangeSet2)

计算两个区间的交集,返回 (MIN, MAX),若无交集则返回空。

=back

=head1 ERROR HANDLING

=over 4

=item *

如果 C<mins> 与 C<maxs> 长度不一致,构造函数会抛出异常。

=item *

如果同一索引的最小值大于最大值,构造函数会抛出异常。

=item *

C<addToSet> 与 C<mergeToSet> 方法只允许传入两个整数参数,否则抛出异常。

=item *

比较方法的参数必须是 C<PDK::Utils::Set> 对象,否则抛出异常。

=back

=head1 EXAMPLES

    use PDK::Utils::Set;

    # 创建区间集合
    my $set = PDK::Utils::Set->new(1, 5);
    $set->addToSet(10, 20);

    # 合并区间
    $set->mergeToSet(6, 9);

    # 打印集合
    $set->dump;

    # 交集
    my $set2 = PDK::Utils::Set->new(4, 15);
    my $inter = $set->interSet($set2);
    $inter->dump;

    # 比较关系
    say $set->compare($set2);   # equal / containButNotEqual / belongButNotEqual / other

=head1 AUTHOR

WENWU YAN E<lt>968828@gmail.comE<gt>

=head1 LICENSE AND COPYRIGHT

This software is licensed under the same terms as Perl itself.

=cut



Powered by Groonga
Maintained by Kenichi Ishigaki <ishigaki@cpan.org>. If you find anything, submit it on GitHub.