smartWordWrap.pl 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204
  1. #!/usr/bin/perl
  2. # Author: Daniel "Trizen" Șuteu
  3. # License: GPLv3
  4. # Date: 15th October 2013
  5. # https://trizenx.blogspot.com
  6. # Smart word wrap algorithm
  7. # See: https://en.wikipedia.org/wiki/Word_wrap#Minimum_raggedness
  8. use 5.016;
  9. use strict;
  10. use warnings;
  11. package Smart::Word::Wrap {
  12. sub new {
  13. my (undef, %args) = @_;
  14. my %opt = (
  15. width => 6,
  16. text => '',
  17. );
  18. foreach my $key (keys %args) {
  19. if (exists $opt{$key}) {
  20. $opt{$key} = delete $args{$key};
  21. }
  22. else {
  23. local $" = ', ';
  24. die "ERROR: invalid key-option '$key' (expected one of {@{[keys %opt]}})";
  25. }
  26. }
  27. bless \%opt, __PACKAGE__;
  28. }
  29. # This is the ugliest function! It, recursively,
  30. # prepares the words for the make_paths() function.
  31. sub prepare_words {
  32. my ($self, @array) = @_;
  33. my @root;
  34. my $len = 0;
  35. for (my $i = 0 ; $i <= $#array ; $i++) {
  36. $len += (my $wordLen = length($array[$i]));
  37. if ($len > $self->{width}) {
  38. if ($wordLen > $self->{width}) {
  39. $len -= $wordLen;
  40. splice(@array, $i, 1, unpack "(A$self->{width})*", $array[$i]);
  41. $i--, next;
  42. }
  43. last;
  44. }
  45. push @root, [@array[0 .. $i], __SUB__->($self, @array[$i + 1 .. $#{array}])];
  46. last if ++$len >= $self->{width};
  47. }
  48. @root ? @root : @array ? \@array : ();
  49. }
  50. # This function creates all the
  51. # available paths, for further processing.
  52. sub make_paths {
  53. my (@array) = @_;
  54. my @head;
  55. while (@array) {
  56. last if ref($array[0]) eq 'ARRAY';
  57. push @head, shift @array;
  58. }
  59. my @row;
  60. foreach my $path (@array) {
  61. push @row, {"@head" => __SUB__->(@{$path})};
  62. }
  63. @row ? \@row : "@head";
  64. }
  65. # This function combines the
  66. # the parents with the children.
  67. sub combine {
  68. my ($root, $hash) = @_;
  69. my @row;
  70. while (my ($key, $value) = each %{$hash}) {
  71. push @{$root}, $key;
  72. if (ref $value eq 'ARRAY') {
  73. foreach my $item (@{$value}) {
  74. push @row, __SUB__->($root, $item);
  75. }
  76. }
  77. else {
  78. push @row, @{$root}, $value;
  79. }
  80. pop @{$root};
  81. }
  82. \@row;
  83. }
  84. # This function normalize the combinations.
  85. # Example: [[["abc"]]] is normalized to ["abc"];
  86. sub normalize {
  87. my ($array_ref) = @_;
  88. my @strings;
  89. foreach my $item (@{$array_ref}) {
  90. if (ref $item eq 'ARRAY') {
  91. push @strings, __SUB__->($item);
  92. }
  93. else {
  94. push @strings, $array_ref;
  95. last;
  96. }
  97. }
  98. @strings;
  99. }
  100. # This function finds the best
  101. # combination available and returns it.
  102. sub find_best {
  103. my ($self, @arrays) = @_;
  104. my %best = (score => 'inf');
  105. foreach my $array_ref (@arrays) {
  106. my $score = 0;
  107. foreach my $string (@{$array_ref}) {
  108. $score += ($self->{width} - length($string))**2;
  109. }
  110. if ($score < $best{score}) {
  111. $best{score} = $score;
  112. $best{value} = $array_ref;
  113. }
  114. }
  115. exists($best{value}) ? @{$best{value}} : ();
  116. }
  117. # This is the main function of the algorithm
  118. # which calls all the other functions and
  119. # returns the best possible wrapped string.
  120. sub smart_wrap {
  121. my ($self, %opt) = @_;
  122. if (%opt) {
  123. $self = $self->new(%{$self}, %opt);
  124. }
  125. my @words =
  126. ref($self->{text}) eq 'ARRAY'
  127. ? @{$self->{text}}
  128. : split(' ', $self->{text});
  129. my @paths;
  130. foreach my $group ($self->prepare_words(@words)) {
  131. push @paths, make_paths(@{$group});
  132. }
  133. my @combinations;
  134. while (@paths) {
  135. if (ref($paths[0]) eq 'ARRAY') {
  136. push @paths, @{shift @paths};
  137. next;
  138. }
  139. my $path = shift @paths;
  140. push @combinations, ref($path) eq 'HASH' ? [combine([], $path)] : [$path];
  141. }
  142. join("\n", $self->find_best(normalize(\@combinations)));
  143. }
  144. }
  145. #
  146. ## Usage example
  147. #
  148. my $text = 'aaa bb cc ddddd';
  149. my $obj = Smart::Word::Wrap->new(width => 7);
  150. say "=>>> SMART WRAP:";
  151. say $obj->smart_wrap(text => $text);
  152. say "\n=>>> GREEDY WRAP (Text::Wrap):";
  153. require Text::Wrap;
  154. $Text::Wrap::columns = $obj->{width};
  155. $Text::Wrap::columns += 1;
  156. say Text::Wrap::wrap('', '', $text);