use
Encode
qw/ decode_utf8 encode_utf8 /
;
use
JSON
qw/ to_json decode_json /
;
use
vars
qw/ @EXPORT_OK $VERSION /
;
@EXPORT_OK
=
qw/ run_analysis group_variants analyze_variant_location wit_stringify /
;
$VERSION
=
"1.1"
;
my
$unsolved_problems
= {};
sub
run_analysis {
my
(
$tradition
,
%opts
) =
@_
;
my
$c
=
$tradition
->collation;
my
$aclabel
=
$c
->ac_label;
my
$stemma_id
=
$opts
{
'stemma_id'
} || 0;
my
@ranks
=
ref
(
$opts
{
'ranks'
} ) eq
'ARRAY'
? @{
$opts
{
'ranks'
}} : ();
my
$collapse
= Set::Scalar->new();
if
(
$opts
{
'merge_types'
} &&
ref
(
$opts
{
'merge_types'
} ) eq
'ARRAY'
) {
$collapse
->insert( @{
$opts
{
'merge_types'
}} );
}
elsif
(
$opts
{
'merge_types'
} ) {
$collapse
->insert(
$opts
{
'merge_types'
} );
}
my
$dir
;
if
(
exists
$opts
{
'calcdir'
} ) {
$dir
=
delete
$opts
{
'calcdir'
}
}
elsif
(
exists
$opts
{
'calcdsn'
} ) {
if
( $@ ) {
throw(
"Could not instantiate a directory for "
.
$opts
{
'calcdsn'
}
.
": $@"
);
}
$dir
= Text::Tradition::Directory->new(
dsn
=>
$opts
{
'calcdsn'
} );
}
my
$stemma
=
$tradition
->stemma(
$stemma_id
);
my
$lacunose
= Set::Scalar->new(
$stemma
->hypotheticals );
my
$stemma_wits
= Set::Scalar->new(
$stemma
->witnesses );
my
$tradition_wits
= Set::Scalar->new(
map
{
$_
->sigil }
$tradition
->witnesses );
$lacunose
->insert(
$stemma_wits
->symmetric_difference(
$tradition_wits
)->members );
unless
(
@ranks
) {
my
%common_rank
;
foreach
my
$rdg
(
$c
->common_readings ) {
$common_rank
{
$rdg
->rank} = 1;
}
@ranks
=
grep
{ !
$common_rank
{
$_
} } ( 1 ..
$c
->end->rank-1 );
}
my
@groups
;
my
@use_ranks
;
my
%lacunae
;
my
$moved
= {};
foreach
my
$rank
(
@ranks
) {
my
$missing
=
$lacunose
->clone();
my
$rankgroup
= group_variants(
$tradition
,
$rank
,
$missing
,
$moved
,
$collapse
);
next
unless
keys
%$rankgroup
;
my
$rankgraph
= _graph_for_grouping(
$stemma
,
$rankgroup
,
$missing
,
$aclabel
);
if
(
$opts
{
'exclude_type1'
} ) {
next
unless
_useful_variant(
$rankgroup
,
$rankgraph
,
$aclabel
);
}
push
(
@use_ranks
,
$rank
);
push
(
@groups
, {
grouping
=>
$rankgroup
,
graph
=>
$rankgraph
} );
$lacunae
{
$rank
} =
$missing
;
}
my
$answer
;
try
{
$answer
= solve_variants(
$dir
,
@groups
);
}
catch
( Text::Tradition::Error
$e
) {
if
(
$e
->message =~ /IDP/ ) {
$answer
->{
'variants'
} = [];
map
{
push
( @{
$answer
->{
'variants'
}}, _init_unsolved(
$_
,
'IDP error'
) ) }
@groups
;
}
else
{
$e
->throw;
}
}
my
$conflict_count
= 0;
my
$reversion_count
= 0;
foreach
my
$idx
( 0 ..
$#use_ranks
) {
my
$location
=
$answer
->{
'variants'
}->[
$idx
];
my
$rank
=
$use_ranks
[
$idx
];
$location
->{
'id'
} =
$rank
;
my
%lmiss
;
map
{
$lmiss
{
$_
} = 1 } @{
$lacunae
{
$use_ranks
[
$idx
]}};
$location
->{
'missing'
} = [
keys
%lmiss
];
my
$graph
=
$groups
[
$idx
]->{graph};
analyze_location(
$tradition
,
$graph
,
$location
, \
%lmiss
);
my
@layerwits
;
foreach
my
$rdghash
( @{
$location
->{
'readings'
}} ) {
$conflict_count
++
if
$rdghash
->{
'is_conflict'
};
$reversion_count
++
if
$rdghash
->{
'is_reverted'
};
my
$rdg
=
$c
->reading(
$rdghash
->{
'readingid'
} );
if
(
$rdg
) {
$rdghash
->{
'text'
} =
$rdg
->text .
(
$rdg
->rank ==
$rank
?
''
:
' ['
.
$rdg
->rank .
']'
);
if
(
$rdg
->does(
'Text::Tradition::Morphology'
) ) {
$rdghash
->{
'is_ungrammatical'
} =
$rdg
->grammar_invalid;
$rdghash
->{
'is_nonsense'
} =
$rdg
->is_nonsense;
}
}
my
@realgroup
;
map
{
push
(
@realgroup
,
$_
)
unless
$lmiss
{
$_
} } @{
$rdghash
->{
'group'
}};
$rdghash
->{
'group'
} = \
@realgroup
;
foreach
(
@realgroup
) {
if
(
$_
=~ /^(.*)\Q
$aclabel
\E$/ ) {
push
(
@layerwits
, $1 );
}
}
}
$location
->{
'layerwits'
} = \
@layerwits
if
@layerwits
;
}
$answer
->{
'conflict_count'
} =
$conflict_count
;
$answer
->{
'reversion_count'
} =
$reversion_count
;
return
$answer
;
}
sub
group_variants {
my
(
$tradition
,
$rank
,
$lacunose
,
$transposed
,
$collapse
) =
@_
;
my
$c
=
$tradition
->collation;
my
$aclabel
=
$c
->ac_label;
my
$table
=
$c
->alignment_table;
my
%readings_at_rank
;
my
$check_for_gaps
= Set::Scalar->new();
my
%moved_wits
;
my
$has_transposition
;
foreach
my
$tablewit
( @{
$table
->{
'alignment'
}} ) {
my
$rdg
=
$tablewit
->{
'tokens'
}->[
$rank
-1];
my
$wit
=
$tablewit
->{
'witness'
};
next
if
_is_lacunose(
$wit
,
$lacunose
,
$aclabel
);
if
(
$rdg
&&
$rdg
->{
't'
}->is_lacuna ) {
_add_to_witlist(
$wit
,
$lacunose
,
$aclabel
);
}
elsif
(
$rdg
) {
if
(
$transposed
->{
$rdg
->{
't'
}->id} ) {
map
{
$moved_wits
{
$_
} = 1 } @{
$transposed
->{
$rdg
->{
't'
}->id}};
next
;
}
$readings_at_rank
{
$rdg
->{
't'
}->id} =
$rdg
->{
't'
};
my
@transp
=
grep
{
$_
->rank !=
$rank
}
$rdg
->{
't'
}->related_readings();
foreach
my
$trdg
(
@transp
) {
next
if
exists
$readings_at_rank
{
$trdg
->id};
$has_transposition
= 1;
my
@affected_wits
= _table_witnesses(
$table
,
$trdg
,
$lacunose
,
$aclabel
);
next
unless
@affected_wits
;
map
{
$moved_wits
{
$_
} = 1 }
@affected_wits
;
$transposed
->{
$trdg
->id} =
[ _table_witnesses(
$table
,
$rdg
->{
't'
},
$lacunose
,
$aclabel
) ];
$readings_at_rank
{
$trdg
->id} =
$trdg
;
}
}
else
{
_add_to_witlist(
$wit
,
$check_for_gaps
,
$aclabel
);
}
}
my
$gap_wits
= Set::Scalar->new();
map
{ _add_to_witlist(
$_
,
$gap_wits
,
$aclabel
)
unless
$moved_wits
{
$_
} }
$check_for_gaps
->members;
my
$grouped_readings
= {};
foreach
my
$rdg
(
values
%readings_at_rank
) {
next
if
exists
$grouped_readings
->{
$rdg
->id}
&&
$grouped_readings
->{
$rdg
->id} eq
'COLLAPSE'
;
my
@wits
= _table_witnesses(
$table
,
$rdg
,
$lacunose
,
$aclabel
);
if
(
$collapse
&&
$collapse
->size ) {
my
$filter
=
sub
{
$collapse
->
has
(
$_
[0]->type ) };
foreach
my
$other
(
$rdg
->related_readings(
$filter
) ) {
my
@otherwits
= _table_witnesses(
$table
,
$other
,
$lacunose
,
$aclabel
);
push
(
@wits
,
@otherwits
);
$grouped_readings
->{
$other
->id} =
'COLLAPSE'
;
}
}
$grouped_readings
->{
$rdg
->id} = Set::Scalar->new(
@wits
);
}
if
(
$gap_wits
->members ) {
$grouped_readings
->{
'(omitted)'
} =
$gap_wits
;
}
map
{
delete
$grouped_readings
->{
$_
}
if
(
$grouped_readings
->{
$_
} eq
'COLLAPSE'
||
$grouped_readings
->{
$_
}->is_empty ) }
keys
%$grouped_readings
;
if
(
$has_transposition
) {
_check_transposed_consistency(
$c
,
$rank
,
$transposed
,
$grouped_readings
);
}
return
$grouped_readings
;
}
sub
_table_witnesses {
my
(
$table
,
$trdg
,
$lacunose
,
$aclabel
) =
@_
;
my
$tableidx
=
$trdg
->rank - 1;
my
$has_reading
= Set::Scalar->new();
foreach
my
$row
( @{
$table
->{
'alignment'
}} ) {
my
$wit
=
$row
->{
'witness'
};
next
if
_is_lacunose(
$wit
,
$lacunose
,
$aclabel
);
my
$rdg
=
$row
->{
'tokens'
}->[
$tableidx
];
next
unless
exists
$rdg
->{
't'
} &&
defined
$rdg
->{
't'
};
_add_to_witlist(
$wit
,
$has_reading
,
$aclabel
)
if
$rdg
->{
't'
}->id eq
$trdg
->id;
}
return
$has_reading
->members;
}
sub
_is_lacunose {
my
(
$wit
,
$lac
,
$acstr
) =
@_
;
if
(
$wit
=~ /^(.*)\Q
$acstr
\E$/ ) {
$wit
= $1;
}
return
$lac
->
has
(
$wit
);
}
sub
_add_to_witlist {
my
(
$wit
,
$list
,
$acstr
) =
@_
;
if
(
$wit
=~ /^(.*)\Q
$acstr
\E$/ ) {
return
if
$list
->
has
( $1 );
}
else
{
$list
->
delete
(
$wit
.
$acstr
);
}
$list
->insert(
$wit
);
}
sub
_check_transposed_consistency {
my
(
$c
,
$rank
,
$transposed
,
$groupings
) =
@_
;
my
%seen_wits
;
my
%thisrank
;
foreach
my
$rdg
(
keys
%$groupings
) {
my
$rdgobj
=
$c
->reading(
$rdg
);
$thisrank
{
$rdg
} = 1
if
!
$rdgobj
||
$rdgobj
->rank ==
$rank
;
map
{
push
( @{
$seen_wits
{
$_
}},
$rdg
) } @{
$groupings
->{
$rdg
}};
}
my
@doubled
=
grep
{
scalar
@{
$seen_wits
{
$_
}} > 1 }
keys
%seen_wits
;
return
unless
@doubled
;
if
(
@doubled
==
scalar
keys
%seen_wits
) {
foreach
my
$rdg
(
keys
%$groupings
) {
if
( !
$thisrank
{
$rdg
} ) {
my
$groupstr
= wit_stringify(
$groupings
->{
$rdg
} );
my
(
$matched
) =
grep
{
$groupstr
eq wit_stringify(
$groupings
->{
$_
} ) }
keys
%thisrank
;
delete
$groupings
->{
$rdg
};
unless
(
$matched
) {
delete
$transposed
->{
$rdg
};
warn
"Found problem in evident symmetry with reading $rdg"
;
}
}
}
}
else
{
foreach
my
$dup
(
@doubled
) {
foreach
my
$rdg
( @{
$seen_wits
{
$dup
}} ) {
next
if
$thisrank
{
$rdg
};
next
unless
exists
$groupings
->{
$rdg
};
delete
$groupings
->{
$rdg
};
delete
$transposed
->{
$rdg
};
}
}
foreach
my
$wit
(
keys
%seen_wits
) {
unless
(
grep
{
exists
$groupings
->{
$_
} } @{
$seen_wits
{
$wit
}} ) {
$groupings
->{
'(omitted)'
} = Set::Scalar->new()
unless
exists
$groupings
->{
'(omitted)'
};
_add_to_witlist(
$wit
,
$groupings
->{
'(omitted)'
},
$c
->ac_label );
}
}
}
}
sub
_graph_for_grouping {
my
(
$stemma
,
$grouping
,
$lacunose
,
$aclabel
) =
@_
;
my
$acwits
= [];
my
$extant
= {};
foreach
my
$gs
(
values
%$grouping
) {
map
{
if
(
$_
=~ /^(.*)\Q
$aclabel
\E$/ ) {
push
(
@$acwits
, $1 )
unless
$lacunose
->
has
( $1 );
}
else
{
$extant
->{
$_
} = 1
unless
$lacunose
->
has
(
$_
);
}
}
$gs
->members;
}
my
$graph
;
try
{
$graph
=
$stemma
->situation_graph(
$extant
,
$acwits
,
$aclabel
);
}
catch
( Text::Tradition::Error
$e
) {
throw(
"Could not extend graph with given extant and a.c. witnesses: "
.
$e
->message );
}
catch
{
throw(
"Could not extend graph with a.c. witnesses @$acwits"
);
}
return
$graph
;
}
sub
solve_variants {
my
(
@groups
) =
@_
;
my
$dir
;
unless
(
ref
(
$groups
[0] ) eq
'HASH'
) {
$dir
=
shift
@groups
;
}
my
$variants
= [];
my
$genealogical
= 0;
my
%problems
;
foreach
my
$graphproblem
(
@groups
) {
my
$problem
= Text::Tradition::Analysis::Result->new(
graph
=>
$graphproblem
->{
'graph'
},
setlist
=> [
values
%{
$graphproblem
->{
'grouping'
}} ] );
if
(
exists
$problems
{
$problem
->object_key} ) {
$problem
=
$problems
{
$problem
->object_key};
}
else
{
$problems
{
$problem
->object_key} =
$problem
;
}
$graphproblem
->{
'object'
} =
$problem
;
}
my
%results
;
if
(
$dir
) {
my
$scope
=
$dir
->new_scope;
map
{
$results
{
$_
} =
$dir
->lookup(
$_
) ||
$problems
{
$_
} }
keys
%problems
;
}
else
{
my
$json
= JSON->new->allow_blessed->convert_blessed->utf8->encode(
[
values
%problems
] );
my
$ua
= LWP::UserAgent->new();
my
$resp
=
$ua
->post(
$SOLVER_URL
,
'Content-Type'
=>
'application/json'
,
'Content'
=>
$json
);
my
$answer
;
if
(
$resp
->is_success ) {
$answer
= decode_json(
$resp
->content );
throw(
"Unexpected answer from IDP: $answer"
)
unless
ref
(
$answer
) eq
'ARRAY'
;
}
else
{
throw(
"IDP solver returned "
.
$resp
->status_line .
" / "
.
$resp
->content
.
"; cannot run graph analysis"
);
}
throw(
"Something went wrong with answer symmetricity"
)
unless
keys
(
%problems
) ==
@$answer
;
foreach
my
$a
(
@$answer
) {
my
$r
= Text::Tradition::Analysis::Result->new(
$a
);
$results
{
$r
->object_key} =
$r
;
}
}
foreach
my
$graphproblem
(
@groups
) {
my
$result
=
$results
{
$graphproblem
->{
'object'
}->object_key}
||
$graphproblem
->{
'object'
};
my
$vstruct
;
if
(
$result
->status eq
'OK'
) {
$vstruct
= {
readings
=> [] };
push
(
@$variants
,
$vstruct
);
}
else
{
push
(
@$variants
, _init_unsolved(
$graphproblem
,
$result
->status ) );
next
;
}
$vstruct
->{genealogical} =
$result
->is_genealogical;
$genealogical
++
if
$result
->is_genealogical;
foreach
my
$rid
(
keys
%{
$graphproblem
->{grouping}} ) {
my
$inputset
=
$graphproblem
->{grouping}->{
$rid
};
my
$minset
=
$result
->minimum_grouping_for(
$inputset
);
push
( @{
$vstruct
->{readings}}, {
readingid
=>
$rid
,
group
=>
$minset
} );
}
$vstruct
->{witcopy_types} = {
$result
->classes };
$vstruct
->{reading_roots} = {};
map
{
$vstruct
->{reading_roots}->{
$_
} = 1 }
$result
->sources;
}
return
{
'variants'
=>
$variants
,
'variant_count'
=>
scalar
@$variants
,
'genealogical_count'
=>
$genealogical
};
}
sub
_init_unsolved {
my
(
$graphproblem
,
$status
) =
@_
;
my
$vstruct
= {
'readings'
=> [] };
$vstruct
->{
'unsolved'
} =
$status
;
foreach
my
$rid
(
keys
%{
$graphproblem
->{grouping}} ) {
push
( @{
$vstruct
->{readings}}, {
readingid
=>
$rid
,
group
=> [
$graphproblem
->{grouping}->{
$rid
}->members ] } );
}
return
$vstruct
;
}
sub
analyze_location {
my
(
$tradition
,
$graph
,
$variant_row
,
$lacunose
) =
@_
;
my
$c
=
$tradition
->collation;
if
(
exists
$variant_row
->{
'unsolved'
} ) {
return
;
}
my
$reading_roots
=
delete
$variant_row
->{
'reading_roots'
};
my
$classinfo
=
delete
$variant_row
->{
'witcopy_types'
};
my
$contig
= {};
my
$subgraph
= {};
my
$acstr
=
$c
->ac_label;
my
@acwits
;
foreach
my
$rdghash
( @{
$variant_row
->{
'readings'
}} ) {
my
$rid
=
$rdghash
->{
'readingid'
};
my
@roots
;
foreach
my
$wit
( @{
$rdghash
->{
'group'
}} ) {
$contig
->{
$wit
} =
$rid
;
if
(
$wit
=~ /^(.*)\Q
$acstr
\E$/ ) {
push
(
@acwits
, $1 );
}
if
(
exists
$reading_roots
->{
$wit
} &&
$reading_roots
->{
$wit
} ) {
push
(
@roots
,
$wit
);
}
}
$rdghash
->{
'independent_occurrence'
} = \
@roots
;
}
foreach
my
$rdghash
( @{
$variant_row
->{
'readings'
}} ) {
my
$rid
=
$rdghash
->{
'readingid'
};
my
$rdg
=
$c
->reading(
$rid
);
my
@roots
= @{
$rdghash
->{
'independent_occurrence'
}};
my
@reversions
;
if
(
$classinfo
) {
@reversions
=
grep
{
$classinfo
->{
$_
} eq
'revert'
}
$rdghash
->{
'group'
}->members;
$rdghash
->{
'reversions'
} = \
@reversions
;
}
my
@group
= @{
$rdghash
->{
'group'
}};
$rdghash
->{
'followed'
} =
scalar
(
@group
)
- (
scalar
(
@roots
) +
scalar
(
@reversions
) );
my
$sourceparents
= _find_reading_parents(
$rid
,
$graph
,
$contig
,
@roots
);
_resolve_parent_relationships(
$c
,
$rid
,
$rdg
,
$sourceparents
);
$rdghash
->{
'source_parents'
} =
$sourceparents
;
if
(
@reversions
) {
my
$revparents
= _find_reading_parents(
$rid
,
$graph
,
$contig
,
@reversions
);
_resolve_parent_relationships(
$c
,
$rid
,
$rdg
,
$revparents
);
$rdghash
->{
'reversion_parents'
} =
$revparents
;
}
my
(
%nofollow
,
%unknownfollow
);
foreach
my
$wit
( @{
$rdghash
->{
'group'
}} ) {
foreach
my
$wchild
(
$graph
->successors(
$wit
) ) {
if
(
$reading_roots
->{
$wchild
} &&
$contig
->{
$wchild
}
&&
$contig
->{
$wchild
} ne
$rid
) {
$nofollow
{
$wchild
} = 1;
}
elsif
( !(
$contig
->{
$wchild
}) ) {
$unknownfollow
{
$wchild
} = 1;
}
}
}
$rdghash
->{
'not_followed'
} =
keys
%nofollow
;
$rdghash
->{
'follow_unknown'
} =
keys
%unknownfollow
;
unless
(
$variant_row
->{
'genealogical'
} ) {
$rdghash
->{
'is_conflict'
} =
@roots
!= 1;
$rdghash
->{
'is_reverted'
} =
scalar
@reversions
;
}
}
}
sub
_find_reading_parents {
my
(
$rid
,
$graph
,
$contig
,
@list
) =
@_
;
my
$parenthash
= {};
foreach
my
$wit
(
@list
) {
my
@check
=
$graph
->predecessors(
$wit
);
while
(
@check
) {
my
@next
;
foreach
my
$wparent
(
@check
) {
my
$preading
=
$contig
->{
$wparent
};
if
(
$preading
&&
$preading
ne
$rid
) {
$parenthash
->{
$preading
} = 1;
}
else
{
push
(
@next
,
$graph
->predecessors(
$wparent
) );
}
}
@check
=
@next
;
}
}
return
$parenthash
;
}
sub
_resolve_parent_relationships {
my
(
$c
,
$rid
,
$rdg
,
$rdgparents
) =
@_
;
foreach
my
$p
(
keys
%$rdgparents
) {
my
$pobj
=
$c
->reading(
$p
);
my
$prep
=
$pobj
?
$pobj
->id .
' ('
.
$pobj
->text .
')'
:
$p
;
my
$phash
= {
'label'
=>
$prep
};
if
(
$pobj
) {
my
$rel
=
$c
->get_relationship(
$p
,
$rid
);
if
(
$rel
) {
_add_to_hash(
$rel
,
$phash
);
}
elsif
(
$rdg
) {
if
(
$rdg
->rank !=
$pobj
->rank ) {
foreach
my
$ti
(
$rdg
->related_readings(
'transposition'
) ) {
next
unless
$ti
->text eq
$rdg
->text;
$rel
=
$c
->get_relationship(
$ti
,
$pobj
);
if
(
$rel
) {
_add_to_hash(
$rel
,
$phash
, 1 );
last
;
}
}
unless
(
$rel
) {
foreach
my
$ti
(
$pobj
->related_readings(
'transposition'
) ) {
next
unless
$ti
->text eq
$pobj
->text;
$rel
=
$c
->get_relationship(
$ti
,
$rdg
);
if
(
$rel
) {
_add_to_hash(
$rel
,
$phash
, 1 );
last
;
}
}
}
}
unless
(
$rel
) {
my
$rtext
=
$rdg
->text;
my
$ptext
=
$pobj
->text;
if
( similar(
$rtext
,
$ptext
) ) {
$phash
->{relation} = {
type
=>
'wordsimilar'
};
}
}
}
else
{
$phash
->{relation} = {
type
=>
'deletion'
};
}
$phash
->{
'text'
} =
$pobj
->text
if
$pobj
;
if
(
$pobj
&&
$pobj
->does(
'Text::Tradition::Morphology'
) ) {
$phash
->{
'is_nonsense'
} =
$pobj
->is_nonsense;
$phash
->{
'is_ungrammatical'
} =
$pobj
->grammar_invalid;
}
}
elsif
(
$p
eq
'(omitted)'
) {
$phash
->{relation} = {
type
=>
'addition'
};
}
$rdgparents
->{
$p
} =
$phash
;
}
}
sub
_add_to_hash {
my
(
$rel
,
$phash
,
$is_transposed
) =
@_
;
$phash
->{relation} = {
type
=>
$rel
->type };
$phash
->{relation}->{transposed} = 1
if
$is_transposed
;
$phash
->{relation}->{annotation} =
$rel
->annotation
if
$rel
->has_annotation;
}
sub
similar {
my
(
$word1
,
$word2
) =
sort
{
length
(
$a
) <=>
length
(
$b
) }
@_
;
my
@let1
=
split
(
''
,
lc
(
$word1
) );
my
@let2
=
split
(
''
,
lc
(
$word2
) );
my
$diff
= Algorithm::Diff->new( \
@let1
, \
@let2
);
my
$mag
= 0;
while
(
$diff
->Next ) {
if
(
$diff
->Same ) {
my
$cs
=
$diff
->Range(1) - 2;
$cs
= 0
if
$cs
< 0;
$mag
-=
$cs
;
}
elsif
( !
$diff
->Items(1) ) {
$mag
+=
$diff
->Range(2);
}
elsif
( !
$diff
->Items(2) ) {
$mag
+=
$diff
->Range(1);
}
else
{
my
$c1
=
$diff
->Range(1) || 1;
my
$c2
=
$diff
->Range(2) || 1;
my
$cd
= (
$c1
+
$c2
) / 2;
$mag
+=
$cd
;
}
}
return
(
$mag
<=
length
(
$word1
) / 2 );
}
sub
_useful_variant {
my
(
$rankgroup
,
$rankgraph
,
$acstr
) =
@_
;
my
$is_useful
= 0;
foreach
my
$rdg
(
keys
%$rankgroup
) {
my
@wits
=
$rankgroup
->{
$rdg
}->members;
if
(
@wits
> 1 ) {
$is_useful
++;
}
else
{
$is_useful
++
unless
(
$rankgraph
->is_sink_vertex(
$wits
[0] )
||
$wits
[0] =~ /\Q
$acstr
\E$/ );
}
}
return
$is_useful
> 1;
}
sub
wit_stringify {
my
$groups
=
shift
;
my
@gst
;
unless
(
ref
(
$groups
->[0] ) ) {
my
$mkgrp
= [
$groups
];
$groups
=
$mkgrp
;
}
foreach
my
$g
(
@$groups
) {
push
(
@gst
,
'['
.
join
(
','
,
map
{
"'$_'"
}
@$g
) .
']'
);
}
return
join
(
' / '
,
@gst
);
}
1;
sub
throw {
Text::Tradition::Error->throw(
'ident'
=>
'Analysis error'
,
'message'
=>
$_
[0],
);
}
=head1 LICENSE
This
package
is free software and is provided
"as is"
without express
or implied warranty. You can redistribute it and/or modify it under
the same terms as Perl itself.
=head1 AUTHOR
Tara L Andrews E<lt>aurum
@cpan
.orgE<gt>