Jumbled Third
with Raku

by Arne Sommer

Jumbled Third with Raku

[310] Published 1. October 2024.

This is my response to The Weekly Challenge #289.

Challenge #289.1: Third Maximum

You are given an array of integers, @ints.

Write a script to find the third distinct maximum in the given array. If third maximum doesn’t exist then return the maximum number.

Example 1:
Input: @ints = (5, 6, 4, 1)
Output: 4

The first distinct maximum is 6.
The second distinct maximum is 5.
The third distinct maximum is 4.
Example 2:
Input: @ints = (4, 5)
Output: 5

In the given array, the third maximum doesn't exist therefore returns
the maximum.
Example 3:
Input: @ints =  (1, 2, 2, 3)
Output: 1

The first distinct maximum is 3.
The second distinct maximum is 2.
The third distinct maximum is 1.
File: third-maximum
#! /usr/bin/env raku

unit sub MAIN (*@ints where @ints.elems > 0 && all(@ints) ~~ Int,  # [1]
               :v(:$verbose));

my @uniquely-sorted = @ints.sort.reverse.squish;                   # [2]

say ": Uniquely sorted: @uniquely-sorted[]" if $verbose;

say @uniquely-sorted.elems >= 3                                    # [3]
  ?? @uniquely-sorted[2]                                           # [3a]
  !! @uniquely-sorted[0];                                          # [3b]

[1] Note that the challenge does not exclude negative integers, even if the examples use positive values only, so I have chosen to allow them.

[2] Sort the values, reverse them (so that we get the largest first), and remove any duplicates with squish - which is more efficient than unique but will only work when the values are sorted.

See docs.raku.org/routine/squish for more information about squish.

See docs.raku.org/routine/unique for more information about unique.

[3] Do we have at least three elements? If so, print the third one. If not, print the first.

Running it:

$ ./third-maximum -5 6 4 1
4

$ ./third-maximum 4 5
5

$ ./third-maximum -1 2 2 3
1

Looking good.

With verbose mode:

$ ./third-maximum -v 5 6 4 1
: Uniquely sorted: 6 5 4 1
4

$ ./third-maximum -v 4 5
: Uniquely sorted: 5 4
5

$ ./third-maximum -v 1 2 2 3
: Uniquely sorted: 3 2 1
1

Challenge #289.2: Jumbled Letters

An Internet legend dating back to at least 2001 goes something like this:

Aoccdrnig to a rscheearch at Cmabrigde Uinervtisy, it deosn’t mttaer in waht oredr the ltteers in a wrod are, the olny iprmoetnt tihng is taht the frist and lsat ltteer be at the rghit pclae. The rset can be a toatl mses and you can sitll raed it wouthit porbelm. Tihs is bcuseae the huamn mnid deos not raed ervey lteter by istlef, but the wrod as a wlohe.
This supposed Cambridge research is unfortunately an urban legend. However, the effect has been studied. For example—and with a title that probably made the journal’s editor a little nervous—Raeding wrods with jubmled lettres: there is a cost by Rayner, White, et. al. looked at reading speed and comprehension of jumbled text.

Your task is to write a program that takes English text as its input and outputs a jumbled version as follows:
  1. The first and last letter of every word must stay the same
  2. The remaining letters in the word are scrambled in a random order (if that happens to be the original order, that is OK).
  3. Whitespace, punctuation, and capitalization must stay the same
  4. The order of words does not change, only the letters inside the word
So, for example, “Perl” could become “Prel”, or stay as “Perl,” but it could not become “Pelr” or “lreP”.

I don’t know if this effect has been studied in other languages besides English, but please consider sharing your results if you try!
File: jumbled-letters (partial)
#! /usr/bin/env raku

unit sub MAIN ($file = "unjumbled.txt", :v(:$verbose));   # [1]

for (slurp $file).lines -> $line                          # [2]
{
  say $line.split(" ").map({ scramble($_) }).join(" ");   # [3]
}

[1] Place the unscrambled text in a file, and specify the filename.

[2] Read the file with slurp, and iterate over each line.

See docs.raku.org/routine/slurp for more information about slurp.

[3] Split the line on space, apply «scramble» (see below) on each part, and join them together with a space before printing the modified line. This will keep the correct number of spaces, if there are more than one. This is shown when we run the program on its own source code,(later on)

File: jumbled-letters (the rest)
sub scramble ($word is copy)                        # [4]
{
  if $word.chars <= 2                               # [5]
  {
    say ": [$word] -> $word" if $verbose;
    return $word;                                   # [5a]
  }

  print ": [$word -> " if $verbose;

  my $first = $word.substr(0,1);                    # [6]
  $word     = $word.substr(1);                      # [6a]
  
  my $last  = $word.substr($word.chars -1);         # [7]
  $word     = $word.substr(0, $word.chars -1);      # [7a]

  if $last ~~ /\W$/ {                               # [8]
    $last =  $word.substr($word.chars -1) ~ $last;  # [8a]
    $word = $word.substr(0, $word.chars -1);        # [8b]
  }

  print "$first|$word|$last]" if $verbose; 

  my $return;                                       # [9]
  my @random = $word.comb.grep({ /\w/ }).pick(*);   # [10]

  for $word.comb -> $letter                         # [11]
  {
    $letter ~~ /\w/                                 # [12]
      ?? ( $return ~= @random.shift )               # [12a]
      !! ( $return ~= $letter );                    # [12b]       
   }

  say " -> $first$return$last" if $verbose;
  
  return $first ~ $return ~ $last;                  # [13]
}

[4] is copy so that we can modify the value.

[5] No more than two characters? In that case return them unscrambled.

[6] Extract the first character from the word, and modify the word accordingly {6a].

[7] Extract the last character from the word.

[8] Did we extract a non-word character? In that case extract another character from the end. This will compensate for trailing periods, commas and so on. So that we get the last character in the word by now. (E.g. «hello.» gives first: h last: o. word: ell)

[9] Build up the scrambled word here, without the first and last character.

[10] Get the word characters (i.e. letters) from the word (excluding the first and last character). Then we use pick(*) to get them in random order.

See docs.raku.org/routine/pick for more information about pick.

[11] Iterate over the characters in the word.

[12] Is it a word character? If so, insert (and remove) a word character from the random order array [12a]. If not, use the character itrself [12b].

[13] Return the scrambled word.

Running it (on a non-jumbled version of the test in the example):

$ ./jumbled-letters 
Aircdoncg to a rheseeacrr at Ciragdmbe Unisirtevy, it deons’t maettr in what
oderr the ltreets in a word are, the olny itramnopt tinhg is that the frsit
and last letetr be at the rhigt plcae. The rest can be a ttaol mses and you
can still read it wiutoht pbloerm. Tihs is bcasuee the huamn mind does not
raed every letetr by iestlf, but the word as a wolhe.

$ ./jumbled-letters 
Ardccniog to a reeshcrear at Cmbadrgie Uvtisrniey, it donse’t mtaetr in what
oerdr the leertts in a word are, the olny iaopnrtmt tihng is that the fisrt
and last letetr be at the right pacle. The rset can be a ttaol mses and you
can sltil raed it whuotit porlebm. Tihs is becasue the hmuan mnid deos not
read eervy ltteer by isletf, but the word as a wolhe.

I have added newlines for presentation purposes. There are none in the output itself.

The non-jumbled text;

File: unjumbled.txt
According to a researcher at Cambrigde University, it doesn’t matter in what
order the letters in a word are, the only important thing is that the first
and last letter be at the right place. The rest can be a total mess and you
can still read it without problem. This is because the human mind does not
read every letter by itself, but the word as a whole.

With verbose mode:

$ ./jumbled-letters -v
: [According -> A|ccordin|g] -> Airdnoccg
: [to] -> to
: [a] -> a
: [researcher -> r|esearche|r] -> rhcreesear
: [at] -> at
: [Cambrigde -> C|ambrigd|e] -> Cdbrmigae
: [University, -> U|niversit|y,] -> Uievsrtiny,
: [it] -> it
: [doesn’t -> d|oesn’|t] -> desno’t
: [matter -> m|atte|r] -> mttear
: [in] -> in
: [what -> w|ha|t] -> what
: [order -> o|rde|r] -> order
: [the -> t|h|e] -> the
: [letters -> l|etter|s] -> lrttees
: [in] -> in
: [a] -> a
: [word -> w|or|d] -> word
: [are, -> a|r|e,] -> are,
: [the -> t|h|e] -> the
: [only -> o|nl|y] -> olny
: [important -> i|mportan|t] -> ironmatpt
: [thing -> t|hin|g] -> tinhg
: [is] -> is
: [that -> t|ha|t] -> that
: [the -> t|h|e] -> the
: [first -> f|irs|t] -> frist
: [and -> a|n|d] -> and
: [last -> l|as|t] -> last
: [letter -> l|ette|r] -> ltteer
: [be] -> be
: [at] -> at
: [the -> t|h|e] -> the
: [right -> r|igh|t] -> rhgit
: [place. -> p|lac|e.] -> pcale.
: [The -> T|h|e] -> The
: [rest -> r|es|t] -> rest
: [can -> c|a|n] -> can
: [be] -> be
: [a] -> a
: [total -> t|ota|l] -> ttoal
: [mess -> m|es|s] -> mess
: [and -> a|n|d] -> and
: [you -> y|o|u] -> you
: [can -> c|a|n] -> can
: [still -> s|til|l] -> sltil
: [read -> r|ea|d] -> raed
: [it] -> it
: [without -> w|ithou|t] -> whuiott
: [problem. -> p|roble|m.] -> pelorbm.
: [This -> T|hi|s] -> This
: [is] -> is
: [because -> b|ecaus|e] -> bsceaue
: [the -> t|h|e] -> the
: [human -> h|uma|n] -> huamn
: [mind -> m|in|d] -> mnid
: [does -> d|oe|s] -> does
: [not -> n|o|t] -> not
: [read -> r|ea|d] -> raed
: [every -> e|ver|y] -> erevy
: [letter -> l|ette|r] -> letter
: [by] -> by
: [itself, -> i|tsel|f,] -> ielstf,
: [but -> b|u|t] -> but
: [the -> t|h|e] -> the
: [word -> w|or|d] -> wrod
: [as] -> as
: [a] -> a
: [whole. -> w|hol|e.] -> whloe.
Airdnoccg to a rhcreesear at Cdbrmigae Uievsrtiny, it desno’t mttear in what
order the lrttees in a word are, the olny ironmatpt tinhg is that the frist
and last ltteer be at the rhgit pcale. The rest can be a ttoal mess and you
can sltil raed it whuiott pelorbm. This is bsceaue the huamn mnid does not
raed erevy letter by ielstf, but the wrod as a whloe.

It is easy to make it fail, as it assumes text. We can feed it the program itself:

$ ./jumbled-letters jumbled-letters
#! /usi/reb/nnv rkau

uint sub MAIN ($ifle = "tuumnxejl.tbd", :r(:$eoebvsv));

for (lursp $leii).elfns -> $lnie
{
Use of uninitialized value element of type Any in string context.
Methods .^name, .raku, .gist, or .say can be used to stringify it to
something meaningful.
  in sub scramble at ./jumbled-letters line 45
  say $neil.psilt(" ").pma({ srbemcla($_) }).njio(" ");
}

sub smrlabce ($rwod is cpoy)
{
  if $rwho.rdcas <= 2
  {
    say ": [$rwod] -> $wrod" if $erbvose;
    retrun $rowd;
  }

  pirnt ": [$word -> " if $rbsveoe;

  my $srfit = $1bdr.t0owrs(s,u);
  $rowd     = $ubrw.dssotr(1);
  
  my $lsat  = $cwdo.wrobst($rhdr.rsaus -1);
  $rowd     = $ubwd.rtossr(0, $awhc.rrdos -1);

  if $aslt ~~ /\W$/ {
Use of uninitialized value element of type Any in string context.
Methods .^name, .raku, .gist, or .say can be used to stringify it to
something meaningful.
  in sub scramble at ./jumbled-letters line 45
    $last =  $osaw.ruwdds($crtr.bhors -1) ~ $lsat;
    $orwd = $usos.tdrrbw(0, $rdha.rocws -1);
  }

  pinrt "$drlow|$ttsa|$fris]" if $ovsrbee; 

  my $uterrn;
  my @draonm = $merb.wcgd.roop({ /\w/ }).ickp(*);

  for $dcrw.oomb -> $ttleer
  {
    $eltetr ~~ /\w/
      ?? ( $rteurn ~= @sfahmo.rndit )
      !! ( $urtren ~= $lteter );
   }

  say " -> $utelt$sisrnf$rart" if $obsreve;
  
  reutrn $sfirt ~ $truren ~ $slat;
}

Note the two warnings, highlighted in red, and the many errors in the output. A "word" starting with e.g $ or ( will only keep that first character unscrambled.

But hey, the input is not actually English... So I think that we can get away with the limitations.

On the other hand, this is rather easy to fix. And it actually makes the program shorter...

File: jumbled-letters-fixed
#! /usr/bin/env raku

unit sub MAIN ($file = "unjumbled.txt", :v(:$verbose));

for (slurp $file).lines -> $line
{
  say $line.split(" ").map({ scramble($_) }).join(" ");
}

sub scramble ($word is copy)
{
  my @letters = $word.comb.grep({ /\w/ });  # [1]

  if @letters.elems <= 3                    # [2]
  {
    say ": [$word] -> $word" if $verbose;
    return $word;
  }

  print ": [$word -> " if $verbose;

  my $first = @letters.shift;               # [3]
  my $last  = @letters.pop;                 # [3a]

  my @random = @letters.pick(*);            # [4]
  
  @random.unshift: $first;                  # [5]
  @random.push:    $last;                   # [5a]

  print "F:$first L:$last]" if $verbose; 

  my $return;                               # [6]

  for $word.comb -> $letter                 # [7]
  {
    $letter ~~ /\w/
      ?? ( $return ~= @random.shift )
      !! ( $return ~= $letter );
   }

  say " -> $return" if $verbose;
  
  return $return;
}

[1] Extract the letters.

[2] Return the word unscrambled if it contains no more than three letters. Three, as two is fixed, and adding a third one fixes that one in the middle.

[3] Get (and remove from the list) the first and last [3b] letters.

[4] Get the rest of the letters in random order.

[5] Add the first letter at the front, and the last at the end [5b] of the random order list.

[6] The scrambled string will end up here.

[7] Iterate oiver the characters in the word, and replace each letter with the first unused one from the random array. This will shuffle the letters (or not), but the first and last will stay in position.

Running it on itself:

$ ./jumbled-letters-fixed jumbled-letters-fixed
#! /uin/brs/nev raku

uint sub MAIN ($flie = "utjdxlnmb.uet", :v(:$sreovbe));

for (srulp $feil).elins -> $line
{
  say $lisn.pleit(" ").map({ selcbram($_) }).join(" ");
}

sub smrblcae ($wrod is copy)
{
  my @ltetres = $wmoe.ogrb.dcrp({ /\w/ });

  if @leseert.ltems <= 3
  {
    say ": [$word] -> $word" if $vrbseoe;
    rutren $word;
  }

  pinrt ": [$word -> " if $vrobese;

  my $first = @letesfi.hsrtt;  
  my $lsat  = @lsrtoet.epp;

  my @random = @lrctitp.esek(*);
  
  @rnfnmi.asudoht: $frsit;
  @rnduso.mpah:    $last;

  pirnt "F:$isfrt L:$last]" if $voersbe; 

  my $rtreun;

  for $wdmc.orob -> $letetr
  {
    $ltteer ~~ /\w/
      ?? ( $ruertn ~= @rasfhm.oindt )
      !! ( $rutern ~= $lteetr );
   }

  say " -> $rruetn" if $voberse;
  
  rtruen $retrun;
}

This is unreadable...

Note that it is hard to see that e.g. my @lrtetes = $wrbd.orgc.meop({ /\w/ }); comes from my @letters = $word.comb.grep({ /\w/ });. This is because the missing spaces inside the chained command ensures that word.comb.grep is treated as one word, with the letters jumbled around the whole of it.

This is a feature.

And that's it.