8.2 Solution

Let’s begin with a function that will return a textual representation of a progress bar.

function getProgressBar(perc::Int)::Str
    @assert 0 <= perc <= 100 "perc must be in range [0-100]"
    return "|" ^ perc * "." ^ (100-perc) * "$perc%"
end

In order to understand the function we must remember the precedence of mathematical operations [exponentiation (^) before multiplication (*)]. Moreover, we must remember that in the context of strings * is a concatenation operator (it glues two strings into a one longer string), whereas ^ multiplies a string to its left the number of times on its right (i.e. "a"^3 gives us "a" * "a" * "a" so "aaa"). getProgressBar accepts percentage (perc) and draws as many vertical bars as perc tells us. The unused spots (100-perc) are filed with the placeholders ("."). We finish by appending the number itself and the % symbol by using string interpolation.

Printing a string of 100 characters (actually a bit more) may not look good on some terminals (by default many terminals are 80 characters wide). That is why we may want to limit ourselves to a smaller value of characters (maxNumOfChars) for the progress bar and rescale the percentage (perc) accordingly (see below).

function getProgressBar(perc::Int)::Str
    @assert 0 <= perc <= 100 "perc must be in range [0-100]"
    maxNumOfChars::Int = 50
    p::Int = round(Int, perc / (100 / maxNumOfChars))
    return "|" ^ p * "." ^ (maxNumChars-p) * "$perc%"
end

The above function looses some resolution in translation of perc to vertical bars (|). However, the percentage is displayed as a number anyway ("$perc%") so it is not such a big problem after all.

Now, we are ready to write the first version of our animateProgressBar.

function animateProgressBar()
    fans::Vec{Str} = ["\\", "-", "/", "-"]
    ind::Int = 1
    for p in 0:100
        println(getProgressBar(p), fans[ind])
        ind = (ind == length(fans)) ? 1 : ind + 1
    end
    println(getProgressBar(100))
    return nothing
end

The function is rather simple. For every value of percentage (for p in 0:100) we draw the progress bar with a fan that changes into one of four positions (alternating \, -, /, - in one place a few times a second will give the impression of a fan). Note, the double backslash character ("\\") in fans. The \ symbol got a particular meaning in programming. It is used to designate that the next character(s) is/are special. For instance println("and") will just print the conjunction ‘and’. On the other hand println("a\nd") will print ‘a’ and ‘d’, one below the other, since in Julia "\n" stands for newline. To get rid of the special meaning of "\" we precede it with another backslash, hence "\\".

OK, let’s see what we got.

animateProgressBar()
.................................................. 0% \
.................................................. 1% -
|................................................. 2% /
||................................................ 3% -
||................................................ 4% \
||................................................ 5% -
# part of the output trimmed
||||||||||||||||||||||||||||||||||||||||||||||||.. 96%\
||||||||||||||||||||||||||||||||||||||||||||||||.. 97%-
|||||||||||||||||||||||||||||||||||||||||||||||||. 98%/
|||||||||||||||||||||||||||||||||||||||||||||||||| 99%-
|||||||||||||||||||||||||||||||||||||||||||||||||| 100%\
|||||||||||||||||||||||||||||||||||||||||||||||||| 100%

Pretty good. However, there is a small problem. Namely, the output is printed on the screen instantaneously with one line beneath the other. The first problem will be solved with sleep that makes the program wait for a specific number of milliseconds before executing the next line of code. The second problem will be solved with ANSI escape codes a sequence of characters with a special meaning [as found in the link (see this sentence) to the Wikipedia’s page].

# the terminal must support ANSI escape codes
# https://en.wikipedia.org/wiki/ANSI_escape_code
function clearPrintout()
    #"\033[xxxA" - xxx moves cursor up xxx lines
    print("\033[1A")
    # clears from cursor position till end of display
    print("\033[J")
end

function animateProgressBar()
    delayMs::Int = 0
    fans::Vec{Str} = ["\\", "-", "/", "-"]
    ind::Int = 1
    for p in 0:100
        delayMs = rand(100:250)
        println(getProgressBar(p), fans[ind])
        sleep(delayMs / 1000) # sleep accepts delay in seconds
        clearPrintout()
        ind = (ind == length(fans)) ? 1 : ind + 1
    end
    println(getProgressBar(100))
    return nothing
end

This time running animateProgressBar() will give us the desired result.

As a final touch we will add some functionality for running our script (saved as progress_bar.jl) from a terminal.

function main()
    println("Toy program.")
    println("It animates a progress bar.")
    println("Note: your terminal must support ANSI escape codes.\n")

    # y(es) - default choice (also with Enter), anything else: no
    println("Continue with the animation? [Y/n]")
    choice::Str = readline()
    if lowercase(strip(choice)) in ["y", "yes", ""]
        animateProgressBar()
    end

    println("\nThat's all. Goodbye!")
end

if abspath(PROGRAM_FILE) == @__FILE__
    main()
end

Although not strictly required in Julia the main function is by convention a starting point of any (big or small) program (in many programming languages). Whereas the if abspath part makes sure that our main function is run only if the program was called directly from the terminal, i.e.

julia progress_bar.jl

will run it, while:

julia other_file_that_imports_progress_bar.jl

will not.

The final effect (at least a part of it) is to be seen below.

A mock progress bar (animation works only in an HTML document)


CC BY-NC-SA 4.0 Bartlomiej Lukaszuk