smartWordWrap_simple.pl 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160
  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 combine() 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 : undef;
  49. }
  50. # This function combines the
  51. # the parents with the children.
  52. sub combine {
  53. my ($root, $hash) = @_;
  54. my @row;
  55. while (my ($key, $value) = each %{$hash}) {
  56. push @{$root}, $key;
  57. if (ref $value eq 'ARRAY') {
  58. foreach my $item (@{$value}) {
  59. push @row, __SUB__->($root, $item);
  60. }
  61. }
  62. else {
  63. @row = [@{$root}];
  64. }
  65. pop @{$root};
  66. }
  67. @row;
  68. }
  69. # This function finds the best
  70. # combination available and returns it.
  71. sub find_best {
  72. my ($self, @arrays) = @_;
  73. my %best = (
  74. score => 'inf',
  75. value => [],
  76. );
  77. foreach my $array_ref (@arrays) {
  78. my $score = 0;
  79. foreach my $string (@{$array_ref}) {
  80. $score += ($self->{width} - length($string))**2;
  81. }
  82. if ($score < $best{score}) {
  83. $best{score} = $score;
  84. $best{value} = $array_ref;
  85. }
  86. }
  87. @{$best{value}};
  88. }
  89. # This is the main function of the algorithm
  90. # which calls all the other functions and
  91. # returns the best possible wrapped string.
  92. sub smart_wrap {
  93. my ($self, %opt) = @_;
  94. if (%opt) {
  95. $self = $self->new(%{$self}, %opt);
  96. }
  97. my @words =
  98. ref($self->{text}) eq 'ARRAY'
  99. ? @{$self->{text}}
  100. : split(' ', $self->{text});
  101. join "\n", $self->find_best(map { combine([], $_) } @{$self->prepare_words(@words)});
  102. }
  103. }
  104. #
  105. ## Usage example
  106. #
  107. my $text = 'As shown in the above phases (or steps), the algorithm does many useless transformations';
  108. my $obj = Smart::Word::Wrap->new(width => 20);
  109. say "=>>> SMART WRAP:";
  110. say $obj->smart_wrap(text => $text);
  111. say "\n=>>> GREEDY WRAP (Text::Wrap):";
  112. require Text::Wrap;
  113. $Text::Wrap::columns = $obj->{width};
  114. $Text::Wrap::columns += 1;
  115. say Text::Wrap::wrap('', '', $text);
  116. say "\n", '-' x 80, "\n";
  117. say "=>>> SMART WRAP:";
  118. $text = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.';
  119. say $obj->smart_wrap(text => $text);
  120. say "\n=>>> GREEDY WRAP (Text::Wrap):";
  121. say Text::Wrap::wrap('', '', $text);