diff --git a/Changes b/Changes index 805b5ac..212ff25 100644 --- a/Changes +++ b/Changes @@ -1,6 +1,7 @@ Revision history for String-Utils {{$NEXT}} + - Add support for "paragraphs" - Separate documentation into a separate file 0.0.24 2024-07-29T15:44:22+02:00 diff --git a/README.md b/README.md index b8c6b85..6dba7ca 100644 --- a/README.md +++ b/README.md @@ -66,6 +66,9 @@ say all-same("aaaaaa"); # "a" say all-same("aaaaba"); # Nil say all-same(""); # Nil +.say for paragraphs("a\n\nb"); # 0 => a␤2 => b␤ +.say for paragraphs($path.IO.lines); # … + use String::Utils ; # only import "before" and "after" ``` @@ -340,6 +343,19 @@ say all-same(""); # Nil If the given string consists of a single character, returns that character. Else returns `Nil`. +paragraphs +---------- + +```raku +.say for paragraphs($path.IO.lines); # … +.say for paragraphs("a\n\nb"); # 0 => a␤2 => b␤ +.say for paragraphs("a\n\nb", 1); # 1 => a␤3 => b␤ +``` + +Lazily produces a `Seq` of `Pairs` with paragraphs from a `Seq` or string in which the key is the line number where the paragraph starts, and the value is the paragraph (without trailing newline). + +The optional second argument can be used to indicate the ordinal number of the first line in the string. + AUTHOR ====== diff --git a/doc/String-Utils.rakudoc b/doc/String-Utils.rakudoc index e43cf1d..06fbe1f 100644 --- a/doc/String-Utils.rakudoc +++ b/doc/String-Utils.rakudoc @@ -65,6 +65,9 @@ say all-same("aaaaaa"); # "a" say all-same("aaaaba"); # Nil say all-same(""); # Nil +.say for paragraphs("a\n\nb"); # 0 => a␤2 => b␤ +.say for paragraphs($path.IO.lines); # … + use String::Utils ; # only import "before" and "after" =end code @@ -399,6 +402,23 @@ say all-same(""); # Nil If the given string consists of a single character, returns that character. Else returns C. +=head2 paragraphs + +=begin code :lang + +.say for paragraphs($path.IO.lines); # … +.say for paragraphs("a\n\nb"); # 0 => a␤2 => b␤ +.say for paragraphs("a\n\nb", 1); # 1 => a␤3 => b␤ + +=end code + +Lazily produces a C of C with paragraphs from a C or +string in which the key is the line number where the paragraph starts, +and the value is the paragraph (without trailing newline). + +The optional second argument can be used to indicate the ordinal number +of the first line in the string. + =head1 AUTHOR Elizabeth Mattijsen diff --git a/lib/String/Utils.rakumod b/lib/String/Utils.rakumod index a4a9ea9..8976b31 100644 --- a/lib/String/Utils.rakumod +++ b/lib/String/Utils.rakumod @@ -395,6 +395,73 @@ my sub all-same(str $string) { !! Nil } +my proto sub paragraphs(|) {*} +my multi sub paragraphs(@source, Int:D $initial = 0) { + my class Paragraphs does Iterator { + has $!iterator; + has int $!line; + + method new($iterator, int $line) { + my $self := nqp::create(self); + nqp::bindattr( $self,Paragraphs,'$!iterator',$iterator); + nqp::bindattr_i($self,Paragraphs,'$!line', $line - 1); + $self + } + + method pull-one() { + + # Last iteration produced a paragraph, finish now + return IterationEnd if nqp::isnull($!iterator); + + # Production logic + my int $line = $!line; + my $collected := nqp::list_s; + my sub paragraph() { + $!line = $line; + + Pair.new( + $line - nqp::elems($collected), + nqp::join("\n", $collected) + ) + } + + # Collection logic + nqp::until( + nqp::eqaddr(($_ := $!iterator.pull-one),IterationEnd), + nqp::stmts( + ++$line, + nqp::if( + is-whitespace($_), + nqp::if( + nqp::elems($collected), + (return paragraph) + ), + nqp::push_s($collected, $_) + ) + ) + ); + + # Still need to produce final paragraph + if nqp::elems($collected) { + $!iterator := nqp::null; + ++$line; + paragraph + } + + # No final paragraph, we're done + else { + IterationEnd + } + } + } + + # Produce the sequence + Seq.new: Paragraphs.new(@source.iterator, $initial) +} +my multi sub paragraphs(Cool:D $string, Int:D $initial = 0) { + paragraphs $string.Str.lines, $initial +} + my sub EXPORT(*@names) { Map.new: @names ?? @names.map: { diff --git a/t/01-basic.rakutest b/t/01-basic.rakutest index 0e0d66a..743131f 100644 --- a/t/01-basic.rakutest +++ b/t/01-basic.rakutest @@ -1,7 +1,7 @@ use Test; use String::Utils; -plan 71; +plan 74; is after("foobar","foo"), "bar", 'after(foo) ok?'; is "foobar".&after("foo"), "bar", '.&after(foo) ok?'; @@ -105,4 +105,14 @@ is-deeply nomark("élève"), "eleve", 'nomark with marks ok'; is-deeply nomark("eleve"), "eleve", 'nomark without marks ok'; is-deeply nomark(""), "", 'nomark empty ok'; +is-deeply paragraphs("a\n\nb\n\n\nc"), + (0 => "a", 2 => "b", 5 => "c"), + 'paragraphs from string ok'; + +is-deeply paragraphs("\na\n\nb\n\n\nc", 1), + (2 => "a", 4 => "b", 7 => "c"), + 'paragraphs from string ok with initial line number set'; + +is paragraphs($*PROGRAM.lines).elems, 28, 'reading from file lazily'; + # vim: expandtab shiftwidth=4