C14008 Lesson 4: Julia Packages

What's poppin' ๐Ÿ’ฅ gang ๐Ÿ‘จโ€๐Ÿซ, Cameron is coming in **_HOT_**๐Ÿ”ฅ๐Ÿš’๐Ÿงฏ๐Ÿงจ with a class ๐Ÿซ about packages ๐Ÿ“ฆ and stuff ๐Ÿ™Œ. He wrote this whole notebook ๐Ÿ““, but I've snuck this little bit of textโœ๐Ÿผ in right under โฌ‡๏ธ his nose ๐Ÿ‘ƒ๐Ÿผ!

Presenting solutions

Welcome back to class! Because there was no assigned homework, there are no assigned problems to present. So, solutions to any Euler problems completed over the past week are welcome to presented now. These include:

  • Euler 13
  • Euler 28
  • Euler 29
  • Euler 30
  • Euler 39

Associated files

This file is used to read from in the notebook. In your notebook directory, make a text file called p099_base_exp.txt and save this text in it.

p099_base_exp.txt from Euler 99

519432,525806 632382,518061 78864,613712 466580,530130 780495,510032 525895,525320 15991,714883 960290,502358 760018,511029 166800,575487 210884,564478 555151,523163 681146,515199 563395,522587 738250,512126 923525,503780 595148,520429 177108,572629 750923,511482 440902,532446 881418,505504 422489,534197 979858,501616 685893,514935 747477,511661 167214,575367

File I/O in Julia

Often in Julia, you'll need to read in files to have data to work with, and there's a number of methods we can use to accomplish this. First, let's create a file and put some information in it.

In [4]:
# Open a file for writing and write to it
file = open("practice.txt", "w")
write(file, "Cameron: Cool, Christian: drools\nHi class!")

# Close the file so that changes are actually applied
close(file)

Now, open the file again with another handler to read from it.

In [10]:
# opening the file with open
file_read = open("practice.txt")

# calling read to read from the file
# println(read(file_read))
println(readline(file_read))
Cameron: Cool, Christian: drools

In fact, we can actually shortcut this process just by calling read() directly. read() understands we're talking about a file path and opens it for us.

In [21]:
# using read to read the file directly
# read("practice.txt", String)

# using readlines to read in lines as an array
readlines("practice.txt")

# using readline to read one at a time
file_obj = open("practice.txt")
println(readline(file_obj))
println(readline(file_obj))

println(readline("practice.txt"))
println(readline("practice.txt"))

# using eachline to create an interable
for line in eachline("practice.txt")
    println("Line is: " * line)
end
Cameron: Cool, Christian: drools
Hi class!
Cameron: Cool, Christian: drools
Cameron: Cool, Christian: drools
Line is: Cameron: Cool, Christian: drools
Line is: Hi class!

Taking a moment to talk about Julia's do syntax

Julia has a special syntax using a do block if you want to use a function as your first argument. Take the map function as an example, which takes an Array and a function to apply on that array.

In [26]:
a = [1, 2, 3, 4, 5]

# Applyng map function normally
map(x -> x*5, a)

# Applying map function with do syntax
map(a) do x
    if x % 2 == 0
        x*4
    else
        x*5
    end
end
Out[26]:
5-element Array{Int64,1}:
  5
  8
 15
 16
 25

This syntax actually works for any function with a Function as the first argument. Let's create a function below and use the do function to figure out what f(5) is.

In [31]:
function runSomeMath(f::Function)
    println("Figuring out what f(5) is!")
    return f(5, 6)
end
Out[31]:
runSomeMath (generic function with 1 method)
In [32]:
runSomeMath() do x, y
    println("I'm inside the function argument")
    x*5 + y*6
end
Figuring out what f(5) is!
I'm inside the function argument
Out[32]:
61

Using open() and read() with a do block

The convenience of this method is that the file is automatically opened and closed for us within the block, giving us a scoped area to deal with the file and close it once we're done. This is similar to opening a file with a with statement in Python.

In [41]:
# opening a file for writing with do
open("practice.txt") do f
    println(readline(f))
    println(readline(f))
end

# reading a file with do
open("p099_base_exp.txt") do f
    global lines = readlines(f)
end

lines = readlines("p099_base_exp.txt") # exactly the same as above
Cameron: Cool, Christian: drools
Hi class!
Out[41]:
1000-element Array{String,1}:
 "519432,525806"
 "632382,518061"
 "78864,613712"
 "466580,530130"
 "780495,510032"
 "525895,525320"
 "15991,714883"
 "960290,502358"
 "760018,511029"
 "166800,575487"
 "210884,564478"
 "555151,523163"
 "681146,515199"
 โ‹ฎ
 "203327,566155"
 "798176,509187"
 "667688,515963"
 "636120,517833"
 "137410,584913"
 "217615,563034"
 "556887,523038"
 "667229,515991"
 "672276,515708"
 "325361,545187"
 "172115,573985"
 "13846,725685"

Briefly covering the Random module

because it might come in handy at some point to you. One thing we can do with Random is generate random lists of numbers using the rand(type or set[, dims]) function.

In [47]:
using Random

# generating random decimals
rand(Float64)

# generating a random list
rand(Float64, 7)

# generating a random value in a ranges
rand(1:10, 10)

rand(["Cameron", "Christian"])
Out[47]:
"Cameron"

Another thing we can do with the Random module is shuffle lists.

In [54]:
ajr = ["Adam", "Jack", "Ryan"] # or Aaron, Johnson, Rodger (no)

# shuffling a list
shuffle!(ajr)

# shuffling a range
shuffle(1:10)
Out[54]:
10-element Array{Int64,1}:
  6
  9
  2
 10
  3
  4
  7
  5
  1
  8

How to log in Julia

To write log messages, we're going to use the Logging module. Julia describes the Logging module as:

The Logging module provides a way to record the history and progress of a computation as a log of events.

There's a couple different logging "levels" as defined by Julia that explain how serious a problem is, or what the purpose of the log message is:

The log level is a broad category for the message that is used for early filtering. There are several standard levels of type LogLevel; user-defined levels are also possible. Each is distinct in purpose:

  • Debug is information intended for the developer of the program. These events are disabled by default.
  • Info is for general information to the user. Think of it as an alternative to using println directly.
  • Warn means something is wrong and action is likely required but that for now the program is still working.
  • Error means something is wrong and it is unlikely to be recovered, at least by this part of the code. Often this log-level is unneeded as throwing an exception can convey all the required information.

For your purposes, you're probably going to want to use @info most of the time, or maybe @warn or @error if you wanted to catch someone's attention.

In [62]:
using Logging

# Logging with @info
@info "yo what's up"

# Displaying variables and named parameters
a = [1, 2, 3, 4]
b = [3, 5, 7, 9]
@info "status of lists" a b

# Logging with @warn and @error
@warn "Check something out here!"
@error "Something is broken!"
โ”Œ Info: yo what's up
โ”” @ Main In[62]:4
โ”Œ Info: status of lists
โ”‚   a = [1, 2, 3, 4]
โ”‚   b = [3, 5, 7, 9]
โ”” @ Main In[62]:9
โ”Œ Warning: Check something out here!
โ”” @ Main In[62]:12
โ”Œ Error: Something is broken!
โ”” @ Main In[62]:13

Writing and running tests in Julia

Before we get to tests, there's one quick and dirty way in Julia to have code throw an error if something fails, which is @assert.

In [66]:
# using @assert if true
@assert typeof(5) == Int64

# what happens when @assert is false
@assert 5 + 5 == 11
AssertionError: 5 + 5 == 11

Stacktrace:
 [1] top-level scope at In[66]:3

To write and run tests, we're going to use the Test module. Julia describes the Test module as:

The Test module provides simple unit testing functionality. Unit testing is a way to see if your code is correct by checking that the results are what you expect. It can be helpful to ensure your code still works after you make changes, and can be used when developing as a way of specifying the behaviors your code should have when complete.

First, let's import the Test module.

In [67]:
using Test

The most basic thing we can do with the test module is check whether a statement is true or not by using the @test macro.

In [71]:
# write @test
@test typeof(5) == Int64
@test 5 + 5 == 10
Out[71]:
Test Passed

We can also check whether a function under a set of conditions throws a specific error using @test_throws Error function().

In [72]:
# write @test_throws
a = [1, 2, 3]
@test_throws BoundsError a[4]
Out[72]:
Test Passed
      Thrown: BoundsError

We can check that a function logs a certain message using a certain log level with @test_logs (:level, "message") function().

In [78]:
function iLog(variable)
    @info "You gave me $variable"
end

# write @test_logs
@test_logs (:info, "You gave me 7") iLog(7)

Now, let's create a function to check who won a game of TicTacToe, using a 3ร—3 matrix to represent a game. 1s will represent one player while 2s will represent the other. 0s represent unplayed spaces.

In [79]:
using LinearAlgebra

function ticTacToeCheck(board::Array{Int,2})
    size(board) == (3,3) || throw(DimensionMismatch("Board size mismatch"))
    winSeqs = vcat([board[n, :] for n in 1:3], [board[:, n] for n in 1:3])  # check rows and columns
    winSeqs = vcat(winSeqs, [diag(board)], [diag(board[:, end:-1:1])])  # add diagonals
    if [1,1,1] in winSeqs
        return 1
    elseif [2,2,2] in winSeqs
        return 2
    else
        return 0
    end
end
Out[79]:
ticTacToeCheck (generic function with 1 method)

Now, we're going to use the @testset functionality to check our ticTacToe checker to see if it works.

In [82]:
@testset "Tic Tac Toe" begin
    win1 = [1 0 2
            1 2 0
            1 0 0]
    win2 = [1 1 2
            1 2 0
            2 0 0]
    nowin = [1 1 2
             2 2 1
             1 2 1]
    breaks = [1 2 1
              2 1 2]
    # write tests here
    @test ticTacToeCheck(win1) == 1
    @test ticTacToeCheck(win2) == 2
    @test ticTacToeCheck(nowin) == 0
    @test_throws DimensionMismatch ticTacToeCheck(breaks)
end
Test Summary: | Pass  Total
Tic Tac Toe   |    4      4
Out[82]:
Test.DefaultTestSet("Tic Tac Toe", Any[], 4, false)

Using the Primes module

While the previous modules may help you with general Julia development, the Primes module will specifically be useful for solving Euler problems. Many Euler problems ask you to calculate values from primes and you may want to check whether certain problems are primes.

To generate primes, we can use the primes(hi) function:

In [86]:
using Primes

# generating primes
primes(1000000)
Out[86]:
78498-element Array{Int64,1}:
      2
      3
      5
      7
     11
     13
     17
     19
     23
     29
     31
     37
     41
      โ‹ฎ
 999809
 999853
 999863
 999883
 999907
 999917
 999931
 999953
 999959
 999961
 999979
 999983

If we want to check whether a number is prime, we can use the isprime(num) method. We can also factor numbers using factor(num), which returns a Factorization type that functions a little like a dictionary.

In [94]:
# checking isprime
isprime(43)
isprime(91)

# checking factor
factorization = factor(91)
Out[94]:
7 * 13

Sometimes it's useful to know the totient of a number, $\phi(n)$, which is the number of positve integers less than $n$ that are relatively prime to $n$. Primes can do that for us.

In [97]:
# passing factorization
println(totient(factorization))

# calling totient directly
totient(9)
72
Out[97]:
6

Homework

This week, we'll be giving you some homework based on the packages we taught you this week.

Project Euler Problem 22

https://projecteuler.net/problem=22

Using names.txt (right click and 'Save Link/Target As...'), a 46K text file containing over five-thousand first names, begin by sorting it into alphabetical order. Then working out the alphabetical value for each name, multiply this value by its alphabetical position in the list to obtain a name score.

For example, when the list is sorted into alphabetical order, COLIN, which is worth 3 + 15 + 12 + 9 + 14 = 53, is the 938th name in the list. So, COLIN would obtain a score of 938 ร— 53 = 49714.

What is the total of all the name scores in the file?

Hint: make use of read(), strip(), and map() or the broadcast operator

In [ ]:

Project Euler Problem 42

https://projecteuler.net/problem=42

The nth term of the sequence of triangle numbers is given by, tn = ยฝn(n+1); so the first ten triangle numbers are:

1, 3, 6, 10, 15, 21, 28, 36, 45, 55, ...

By converting each letter in a word to a number corresponding to its alphabetical position and adding these values we form a word value. For example, the word value for SKY is 19 + 11 + 25 = 55 = t10. If the word value is a triangle number then we shall call the word a triangle word.

Using words.txt (right click and 'Save Link/Target As...'), a 16K text file containing nearly two-thousand common English words, how many are triangle words?

In [ ]:

Project Euler Problem 10

https://projecteuler.net/problem=10

The sum of the primes below 10 is 2 + 3 + 5 + 7 = 17.

Find the sum of all the primes below two million.

_With the Primes package, this one is pretty easy. For extra credit, build your own prime generator. Here is one good method._

In [ ]:

Practicing satisfying tests

For this exercise, you're going to provided with a 2D minesweeper board (a 2D matrix) of any given dimensions filled with trues and falses, where trues are mines and falses are empty space. It's your job to generate a minesweeper board from this matrix, replacing the trues with the character 'X' to signify mines, and replacing the falses with the number of nearby mines. A mine is nearby to a space if it is contained in the $3\times 3$ square around the space.

Your goal is to pass the @testset beneath the function below. Once you have done so, add another couple tests of your choosing with gameboards of your choice.

In [ ]:
using Test

function generateMineBoard(board::Array{Bool, 2})::Array{Any, 2}
    mineBoard = Array{Any, 2}(undef, size(board))
    # fill in mineBoard with your code here
end

@testset "Minesweeper Tests" begin
    X = 'X'
    input    = [0 0 0 0 0 0
                0 1 0 0 0 0
                1 0 0 0 1 0
                1 1 0 1 0 0]
    expected = [1 1 1 0 0 0
                2 X 1 1 1 1
                X 4 3 2 X 1
                X X 2 X 2 1]
    @test generateMineBoard(convert(Array{Bool,2}, input)) == expected
    input2    = [0  0  0  0  0  0  0  0  1  0
                 0  0  0  1  0  1  0  0  0  0
                 0  0  0  0  1  0  0  0  0  0
                 1  0  0  0  0  0  0  0  0  0
                 0  0  0  0  0  0  0  0  0  0
                 0  0  0  0  1  0  0  0  1  0
                 0  0  1  0  1  0  0  0  0  0
                 0  0  0  0  0  0  0  0  0  0
                 0  0  0  0  0  1  0  0  0  0]
    expected2 = [0  0  1  1  2  1  1  1  X  1
                 0  0  1  X  3  X  1  1  1  1
                 1  1  1  2  X  2  1  0  0  0
                 X  1  0  1  1  1  0  0  0  0
                 1  1  0  1  1  1  0  1  1  1
                 0  1  1  3  X  2  0  1  X  1
                 0  1  X  3  X  2  0  1  1  1
                 0  1  1  2  2  2  1  0  0  0
                 0  0  0  0  1  X  1  0  0  0]
    @test generateMineBoard(convert(Array{Bool, 2}, input2)) == expected2
    @test generateMineBoard(convert(Array{Bool, 2}, [0 0 0; 1 1 1; 0 0 0])) == [2 3 2; X X X; 2 3 2]
    @test generateMineBoard(convert(Array{Bool, 2}, [1 0; 0 0; 0 1])) == [X 1; 2 2; 1 X]
    @test generateMineBoard(ones(Bool, (5,5))) == fill('X', (5,5))
    @test generateMineBoard(zeros(Bool, (5,5))) == zeros(Bool, (5,5))
end