4. Arrays

Introduction

//----------------------------------------------------------------------------------
simple = [ "this", "that", "the", "other" ]
nested = [ "this", "that", [ "the", "other" ] ]
assert nested.size() == 3
assert nested[2].size() == 2

flattenNestedToSimple = [ "this", "that", [ "the", "other" ] ].flatten()
assert flattenNestedToSimple.size() == 4
//----------------------------------------------------------------------------------

Specifying a List In Your Program

//----------------------------------------------------------------------------------
a = [ "quick", "brown", "fox" ]
assert a.size() == 3
a = 'Why are you teasing me?'.split(' ')
assert a == ["Why", "are", "you", "teasing", "me?"]

removeLeadingSpaces = { it.trim() }
nonBlankLines = { it }
lines = '''
    The boy stood on the burning deck,
    It was as hot as glass.
'''.split('\n').collect(removeLeadingSpaces).findAll(nonBlankLines)

assert lines == ["The boy stood on the burning deck,",
                 "It was as hot as glass."]

// initialiseListFromFileScript:
lines = new File('mydata.txt').readLines()

// processFileScript:
new File('mydata.txt').eachLine{
    // dosomething
}
//----------------------------------------------------------------------------------

Printing a List with Commas

//----------------------------------------------------------------------------------
marbleColors = ['red', 'green', 'yellow']
assert marbleColors.join(', ') == 'red, green, yellow'

def commify(items) {
    if (!items) return items
    def sepchar = items.find{ it =~ /,/ } ? '; ' : ', '
    switch (items.size()) {
        case 1: return items[0]
        case 2: return items.join(' and ')
    }
    items[0..-2].join(sepchar) + sepchar + 'and ' + items[-1]
}

assert commify(marbleColors) == 'red, green, and yellow'

lists = [
    [ 'just one thing' ],
    [ 'Mutt', 'Jeff' ],
    'Peter Paul Mary'.split(' '),
    [ 'To our parents', 'Mother Theresa', 'God' ],
    [ 'pastrami', 'ham and cheese', 'peanut butter and jelly', 'tuna' ],
    [ 'recycle tired, old phrases', 'ponder big, happy thoughts' ],
    [ 'recycle tired, old phrases',
      'ponder big, happy thoughts',
      'sleep and dream peacefully' ],
]

expected = '''
just one thing
Mutt and Jeff
Peter, Paul, and Mary
To our parents, Mother Theresa, and God
pastrami, ham and cheese, peanut butter and jelly, and tuna
recycle tired, old phrases and ponder big, happy thoughts
recycle tired, old phrases; ponder big, happy thoughts; and sleep and dream peacefully
'''

assert expected == '\n' + lists.collect{commify(it)}.join('\n') + '\n'
//----------------------------------------------------------------------------------

Changing Array Size

//----------------------------------------------------------------------------------
// In Groovy, lists and arrays are more or less interchangeable
// here is the example using lists
people = ['Crosby', 'Stills', 'Nash']
assert people.size() == 3
people[3] = 'Young'
assert people.size() == 4
assert people == ['Crosby', 'Stills', 'Nash', 'Young']
// to use arrays simply do 'people = peopleArray.toList()' at the start
// and 'peopleArray = people as String[]' at the end
// if you attempt to do extension on a Java array you will get an
// ArrayIndexOutOfBoundsException - which is why Java has ArrayList et al
//----------------------------------------------------------------------------------

Doing Something with Every Element in a List

//----------------------------------------------------------------------------------
// list to process
people == ['Crosby', 'Stills', 'Nash', 'Young']
// helper
startsWithCapital = { word -> word[0] in 'A'..'Z' }

// various styles are possible for processing lists
// closure style
people.each { person -> assert startsWithCapital(person) }
// for loop style
for (person in people) { assert startsWithCapital(person) }

// unixScriptToFindAllUsersStartingWithLetterA:
all = 'who'.execute().text.replaceAll('\r', '').split('\n')
all.grep(~/^a.*/).each{ println it }

// printFileWithWordsReversedScript:
new File('Pleac/src/SlowCat.groovy').eachLine{ line ->
     line.split(' ').each{ print it.reverse() }
}

a = [0.5, 3]; b = [0, 1]
assert [a, b].flatten().collect{ it * 7 } == [3.5, 21, 0, 7]
// above doesn't modify original arrays
// instead use a = a.collect{ ... }
//----------------------------------------------------------------------------------

Iterating Over an Array by Reference

//----------------------------------------------------------------------------------
// not relevant in Groovy since we have always references
items = []
for (item in items) {
    // do something with item
}
//----------------------------------------------------------------------------------

Extracting Unique Elements from a List

//----------------------------------------------------------------------------------
assert [ 1, 1, 2, 2, 3, 3, 3, 5 ].unique() == [ 1, 2, 3, 5 ]
//----------------------------------------------------------------------------------

Finding Elements in One Array but Not Another

//----------------------------------------------------------------------------------
assert [ 1, 1, 2, 2, 3, 3, 3, 4, 5 ] - [ 1, 2, 4 ]  ==  [3, 3, 3, 5]
assert [ 1, 1, 2, 2, 3, 3, 3, 4, 5 ].unique() - [ 1, 2, 4 ]  ==  [3, 5]
//----------------------------------------------------------------------------------

Computing Union, Intersection, or Difference of Unique Lists

//----------------------------------------------------------------------------------
a = [1, 3, 5, 6, 7, 8]
b = [2, 3, 5, 7, 9]
// intersection
assert a.intersect(b) == [3, 5, 7]
// union
assert (a + b).unique().sort() == [1, 2, 3, 5, 6, 7, 8, 9]
// difference
assert (a - b) == [1, 6, 8]
//----------------------------------------------------------------------------------

Appending One Array to Another

//----------------------------------------------------------------------------------
members = [ "Time", "Flies" ]
initiates =  [ "An", "Arrow" ]
members += initiates
assert members == ["Time", "Flies", "An", "Arrow"]

members.add(2, "Like")
assert members == ["Time", "Flies", "Like", "An", "Arrow"]

members[0] = "Fruit"
members[3..4] = ["A", "Banana"]
assert members == ["Fruit", "Flies", "Like", "A", "Banana"]
//----------------------------------------------------------------------------------

Reversing an Array

//----------------------------------------------------------------------------------
items = ["the", "quick", "brown", "fox"]
assert items.reverse() == ["fox", "brown", "quick", "the"]

firstLetters = []
items.reverseEach{ firstLetters += it[0] }
assert firstLetters.join() == 'fbqt'

descending = items.sort().reverse()
assert descending == ["the", "quick", "fox", "brown"]
descendingBySecondLastLetter = items.sort { a,b -> b[-2] <=> a[-2] }
assert descendingBySecondLastLetter == ["brown", "fox", "the", "quick"]
//----------------------------------------------------------------------------------

Processing Multiple Elements of an Array

//----------------------------------------------------------------------------------
// warning: not an exact equivalent, idiomatic use would return copies
def shift2 = {one = friends[0]; two = friends[1]; 2.times{friends.remove(0)}}
friends = 'Peter Paul Mary Jim Tim'.split(' ').toList()
shift2()
assert one == 'Peter'
assert two == 'Paul'
assert friends == ["Mary", "Jim", "Tim"]

def pop2(items) { items[0..1] }
beverages = 'Dew Jolt Cola Sprite Fresca'.split(' ').toList()
pair = pop2(beverages)
assert pair == ["Dew", "Jolt"]
//----------------------------------------------------------------------------------

Finding the First List Element That Passes a Test

//----------------------------------------------------------------------------------
class Employee {
    def name
    def position
    def salary
}
staff = [new Employee(name:'Jim',position:'Manager',salary:26000),
         new Employee(name:'Jill',position:'Engineer',salary:24000),
         new Employee(name:'Jack',position:'Engineer',salary:22000)]
highestEngineer = staff.find { emp -> emp.position == 'Engineer' }
assert highestEngineer.salary == 24000
//----------------------------------------------------------------------------------

Finding All Elements in an Array Matching Certain Criteria

//----------------------------------------------------------------------------------
engineers = staff.findAll { e -> e.position == 'Engineer' }
assert engineers.size() == 2

highPaid = staff.findAll { e -> e.salary > 23000 }
assert highPaid*.name == ["Jim", "Jill"]
//----------------------------------------------------------------------------------

Sorting an Array Numerically

//----------------------------------------------------------------------------------
// sort works for numbers
assert [100, 3, 20].sort() == [3, 20, 100]
// strings representing numbers will be sorted alphabetically
assert ['100', '3', '20'].sort() == ["100", "20", "3"]
// closure style sorting allows arbitrary expressions for the comparison
assert ['100', '3', '20'].sort{ a,b -> a.toLong() <=> b.toLong()} == ["3", "20", "100"]

// obtain the following on unix systems using: 'ps ux'.execute().text
processInput = '''
      PID    PPID    PGID     WINPID  TTY  UID    STIME COMMAND
     3868       1    3868       3868  con 1005 06:23:34 /usr/bin/bash
     3456    3868    3456       3528  con 1005 06:23:39 /usr/bin/ps
'''
nonEmptyLines = {it.trim()}
lines = processInput.split("\n").findAll(nonEmptyLines)[1..-1]
def col(n, s) { s.tokenize()[n] }
commandIdx = 7
pidIdx = 0
ppidIdx = 1
linesByPid = lines.sort{ col(pidIdx,it).toLong() }
assert col(commandIdx, linesByPid[0]) == '/usr/bin/ps'
linesByPpid = lines.sort{ col(ppidIdx,it).toLong() }
assert col(commandIdx, linesByPpid[0]) == '/usr/bin/bash'
//----------------------------------------------------------------------------------

Sorting a List by Computable Field

//----------------------------------------------------------------------------------
// sort staff from 4.12 by name
assert staff.sort { a,b -> a.name <=> b.name }*.name == ["Jack", "Jill", "Jim"]
// sort by first two characters of name and if equal by descending salary
assert staff.sort { a,b ->
    astart = a.name[0..1]
    bstart = b.name[0..1]
    if (astart == bstart) return b.salary <=> a.salary
    return astart <=> bstart
}*.name == ["Jack", "Jim", "Jill"]
//----------------------------------------------------------------------------------

Implementing a Circular List

//----------------------------------------------------------------------------------
items = [1, 2, 3, 4, 5]
processed = []
10.times{
    processed << items[0]
    items = items[1..-1] + items[0]
}
assert processed == [1, 2, 3, 4, 5, 1, 2, 3, 4, 5]
//----------------------------------------------------------------------------------

Randomizing an Array

//----------------------------------------------------------------------------------
import java.text.DateFormatSymbols as Symbols
items = new Symbols().shortWeekdays.toList()[1..7]
assert items == ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]
// not as random as you might expect
println items.sort{ Math.random() }
// => ["Sat", "Tue", "Sun", "Wed", "Mon", "Thu", "Fri"]
// better to use the built-in method for this purpose
Collections.shuffle(items)
println items
// => ["Wed", "Tue", "Fri", "Sun", "Sat", "Thu", "Mon"]
//----------------------------------------------------------------------------------

Program: words

//----------------------------------------------------------------------------------
symbols = new Symbols()
words = symbols.weekdays.toList()[1..7] +
    symbols.months.toList()[0..11] +
    symbols.eras.toList() +
    symbols.amPmStrings.toList()

expected = //
'AD        August    February  July      May       October   September Tuesday   \n' +
'AM        BC        Friday    June      Monday    PM        Sunday    Wednesday \n' +
'April     December  January   March     November  Saturday  Thursday  \n'

class WordFormatter {
    def cols

    def process(list) {
        def sb = new StringBuffer()
        def colWidth = list.max{it.size()}.size() + 1
        int columns = [cols/colWidth, 1].max()
        def numWords = list.size()
        int rows = (numWords + columns - 1) / columns
        for (row in 0..<rows) {
            for (col in 0..<columns) {
                def target = row + col * rows
                if (target < numWords)
                    sb << list[target].padRight(colWidth)
            }
            sb << '\n'
        }
        return sb.toString()
    }
}

// get nr of chars that fit in window or console, see PLEAC 15.4
// hard-coded here but several packages are available, e.g. in JLine
// use a concrete implementation of Terminal.getTerminalWidth()
def getWinCharWidth() { 80 }

// main script
actual = new WordFormatter(cols:getWinCharWidth()).process(words.sort())
assert actual == expected
//----------------------------------------------------------------------------------

Program: permute

//----------------------------------------------------------------------------------
// recursive version is simplest but can be inefficient
def fact(n) { (n == 1) ? 1 : n * fact(n-1)}
assert fact(10) == 3628800
// unwrapped version: note use of BigInteger
def factorial(n) {
    def result = 1G // 1 as BigInteger
    while (n > 0) {
        result *= n
        n -= 1
    }
    return result
}
expected = 93326215443944152681699238856266700490715968264381621468592963895217599993229915608941463976156518286253697920827223758251185210916864000000000000000000000000
assert expected == factorial(100)
// println factorial(10000)
// => 284625... (greater than 35,000 digits)

// simple version but less efficient
def simplePermute(items, perms) {
    if (items.size() == 0)
        println perms.join(' ')
    else
        for (i in items) {
            newitems = items.clone()
            newperms = perms.clone()
            newperms.add(i)
            newitems.remove(i)
            simplePermute(newitems, newperms)
        }
}
simplePermute(['dog', 'bites', 'man'], [])
// =>
//dog bites man
//dog man bites
//bites dog man
//bites man dog
//man dog bites
//man bites dog

// optimised version below
expected = '''
man bites dog
man dog bites
bites man dog
bites dog man
dog man bites
dog bites man
'''

// n2pat(n, len): produce the N-th pattern of length len
def n2pat(n, length) {
    def pat = []
    int i = 1
    while (i <= length) {
        pat << (n % i)
        n = n.intdiv(i)
        i += 1
    }
    pat
}

// pat2perm(pat): turn pattern returned by n2pat() into
// permutation of integers.
def pat2perm(pat) {
    def source = (0 ..< pat.size()).collect{ it/*.toString()*/ }
    def perm = []
    while (pat.size() > 0) {
        def next = pat.remove(pat.size()-1)
        perm << source[next]
        source.remove(next)
    }
    perm
}

def n2perm(n, len) {
    pat2perm(n2pat((int)n,len))
}

data = ['man', 'bites', 'dog']
sb = new StringBuffer()
numPermutations = fact(data.size())
for (j in 0..<numPermutations) {
    def permutation = n2perm(j, data.size()).collect { k -> data[k] }
    sb << permutation.join(' ') + '\n'
}
assert '\n' + sb.toString() == expected
//----------------------------------------------------------------------------------