# -*- coding: utf-8 -*-
# ---
# jupyter:
#   jupytext:
#     text_representation:
#       extension: .jl
#       format_name: light
#       format_version: '1.5'
#       jupytext_version: 1.14.4
#   kernelspec:
#     display_name: julia1.6 1.6.1
#     language: julia
#     name: julia1.6-1.6
# ---

# # INTRODUÇÃO À LINGUAGEM JULIA
# (2ª Parte)
#
# ---
# ## Prof. Dr. <NAME>¹²³ (<EMAIL>)
# #### <NAME>²
# #### <NAME>²
# #### <NAME>²
# #### <NAME>²
# #### <NAME>²
#
# 1. *Grupo de Estudos em Sistemas Energéticos e Simulação Computacional (GESESC)*
# 2. *IFMG - Campus Arcos*
# 3. *Departamento de Engenharia Nuclear - UFMG*
# ---

# ## Blocos de Execução Condicional
#
# Os operadores relacionais e lógicos, são destinados, na grande maioria das vezes a comporem crivos de execução para determinados blocos de código. Ou seja, determinar quando um determinado bloco de código de execução condicional, deve ou não ser executado. 
#
# Para criar um blodo de código de execução concidicional, utilizamos as palavras reservadas da linguagem `if` e `end`. Um exemplo simples poderia ser feito para exibir uma mensagem informando se um determinado número inteiro é par ou ímpar. Da seguinte forma:

num = 21
if num%2 == 0
    println("O número $num é par.")
end
if num%2 != 0
    println("O número $num é impar.")
end

# Note que `if` e `end`delimitam o bloco de execução condicional e o verificação relacional à direita de `if` especifíca o critério de execução daquele bloco. 
#
# O caso anterior, no entanto, poderia ser simplificado para uma estrutura mais elegante, caso fosse garantido inexistência de excessões para o valor de `num`. Isto quer dizer que, se de alguma forma o programador garantir que `num` atenderá os critérios de teste, por exemplo, ser sempre inteiro e maior ou igual a zero, então a palavra reservada `else` poderia ser utilizada no lugar do segundo bloco `if`. 

num = 22
if num%2 == 0
    println("O número $num é par.")
else
    println("O número $num é impar.")
end

num = 21
if num%2 == 0
    println("O número $num é par.")
else
    println("O número $num é impar.")
end

# O bloco `else` será executado apenas se o bloco `if` não o for independete de quaisquer outros critérios. Isto exige que ele seja cuidadosamente utilizado, e que sejam garantidas as inocorrência de quaiquer excessões ao teste. Como a seguinte, por exemplo. 

num = 2.2
if num%2 == 0
    println("O número $num é par.")
else
    println("O número $num é impar.")
end

# Veja que quando um `float`foi passado, o código determinou que o número é ímpar, no entanto, é sabido que a paridade é uma característica apenas de números inteiros, o que demonstra a debilidade do método como está implementado aqui, para valores de `num` inadequados.
#
# Para corrigirmos esse comportamento, poderíamos validar a entrada e usar o recurso de blocos de execução condicional aninhados, ou seja, blocos declarados dentro de outos blocos.

num = 2.5
if typeof(num) == Int
    if num%2 == 0
        println("O número $num é par.")
    else
        println("O número $num é impar.")
    end
else
    println("O valor informado não possui paridade.")
end

# Ainda é possível associar múltiplas verificações relacionais e lógicas sequenciais utilizando as palavra reservada `elseif`. No notebook anterior, um jogo de dados foi simulado, porém o teste lógico realizado foi bastante grosseiro. Podemos melhor esse teste, inclusive exibindo o resultado da avaliação relacional para o usuário. 

# +
# Jogadas sucessivas

jogador1 = rand(1:6);
jogador1 += rand(1:6);
jogador1 += rand(1:6);
jogador2 = rand(1:6);
jogador2 += rand(1:6);
jogador2 += rand(1:6);
# Já já vamos melhorar essa coisa horrível aí em cima

if jogador1 == jogador2
    println("EMPATE!")
elseif jogador1 > jogador2
    println("Vitória do Jogador 1 com $jogador1 pontos contra $jogador2 pontos do Jogador 2.")
else
    println("Vitória do Jogador 2 com $jogador2 pontos contra $jogador1 pontos do Jogador 1.")
end
# -

# ## *Containers* de Julia
#
# *Containers* são tipos de dados abstratos, com os quais o usuário pode armazenar uma determinada **coleção** de 
# valores de distintos tipos. 
#
# Julia possui três tipos de *containers* distintos, as tuplas, os dicionários e os *arrays*. 
#
# ### Tuplas de Julia
#
# Tuplas são coleções imutáveis de dados sequenciais. As tuplas são inicializadas através da declaração de seus valores entre parênteses e separados por vírgulas, como mostrado abaixo.

tupla1 = (22, 15.7, 198+80im, 'R', "fígado")

# Note a versatilidade da estrutura que é capaz de armazenar diferentes tipos de dados simultaneamente. Todavia é importante ressaltar mais uma vez que tuplas são imutáveis e seus ítens não podem ser modificados após a inicialização da estrutura. Veja como os valores dos ítens podem ser acessados individualmente e o que acontece quando tentamos modificar qualquer um desses valores.

println(tupla1[1])
println(tupla1[2])
println(tupla1[3])
println(tupla1[4])
println(tupla1[5])
tupla1[5] = "Já falei que fígado é muito bom?";

# Veja que não houve modificação no ítem de índice 5.

tupla1

# Fatias de tuplas também podem ser obtidas, veja o caso da criação de uma fatia com os três primeiros elementos de `tupla1`.

tupla1[1:3]

# ### AVISO AOS PYTHONISTAS:
#
# __SIM__, Julia inicia a indexação dos *containers* pelo número 1, e __SIM__ de novo, o limite superior é fechado na delimitação dos *slices**, e daí? 
#
# ---
#
# ### Tuplas nomeadas
#
# Os elementos de uma tupla em Julia podem receber nomes, que devem respeitar as regras de escolha dos nomes de variáveis de tal forma que a `tupla1` poderia ser redeclarada nomeadamente também. Note como cada elemento recebeu um nome válido.

tupla1_nom = (data=22, data_e_mes=15.7, idade_peso_imaginario = 1+98im, inicial='R', bom="fígado")

# Agora os elementos da tupla podem ser acessados tanto por seu índice, quando por seu nome utilizando o operador de acesso `.`.

println(tupla1_nom[1])
println(tupla1_nom.data)
println(tupla1_nom[2])
println(tupla1_nom.data_e_mes)
println(tupla1_nom[3])
println(tupla1_nom.idade_peso_imaginario)
println(tupla1_nom[4])
println(tupla1_nom.inicial)
println(tupla1_nom[5])
println(tupla1_nom.bom)
# Aquele negócio absurdo de ficar repetindo instruções semelhantes de novo né ?

# ### Dicionários de Julia
#
# Os dicionários são estruturas bastante semelhantes às tuplas nomeadas, porém são mutáveis e não admitem acesso elementar pelo operador ponto. O nome dos elementos de um dicionário também é mais flexível, podendo ser qualquer, desde valores numéricos à strings de qualquer formato.
#
# A inicialização de um dicionário se dá por meio da função `Dict()` conforme mostrado abaixo:

dicionario1 = Dict(
    "data" => 22, 
    "data_e_mes" => 15.7, 
    "idade_peso_imaginario" => 1+98im, 
    "inicial" =>'R', 
    "bom" => "fígado"
)
println(dicionario1)

# Veja como os elementos do dicionário são mutávies e também como são acessados por meio de suas chaves.

dicionario1["data"] = 02;
dicionario1["bom"] = "Já falei que fígado é muito bom?";
println(dicionario1)

# Uma outra grande versatilidade dos dicionários, é sua extensibilidade. Uma vez que tenham sido inicializados com a função `Dict()` podem ser extendidos sempre que necessário.

dicionario1["lendaurbana"] = "fígado com jiló"
dicionario1

# Por outro lado, se você deseja remover um elemento do dicionário pode fazê-lo utilizando a função `pop!()` que recebe como primeiro argumento o nome do dicionário que modificará, e como segundo argumento a chave a ser deletada.

pop!(dicionario1, "data")
dicionario1

# ### *Arrays* 
#
# Os *arrays* são os *containers* mais simples da linguagem, são totalmente mutáveis e não são nomeáveis, o acesso aos seus elementos se dá apenas pelos respectivos índices. 
#
# Arrays que possuem apenas uma série de valores são ditos unidimensionais e denominados vetores, se combinarem duas séries ou mais podem ser bidimensionais, quando todas as séries interiores ocupam o mesmo nível interno, ou multidimensionais, quando as séries interiores ocupam níveis mais internos do *array*. 
#
# A inicialização de um *array* é semelhante à das tuplas não nomeadas, porém, substituindo-se os parênteses por colchetes. Vejo o vetor e array 2D abaixo.

vetor = [1,2,3,4,5,6,7,8]
arranjo2d = [[1,2,3,4,5],[6,7,8,9,10],[0,-1,-2,-3,-4]];

# Note que quando acessamos o primeiro elemento de `vetor` obtemos um número, no entanto, quando acessamos o primeiro elemento de `arranjo2d` obtemos um vetor, indicando que `vetor` é composto por elementos zero-dimensionais e portando deve ser unidimensional, e `arranjo2d` é composto por elementos unidimensionais, e por isso é bidimensional.

println(vetor[1])
println(arranjo2d[1])

# Vetores e arranjos podem combinar elementos de diferentes tipo, conforme pode ser visto abaixo.

vetor = [1,2,3,4,5,6,7,"oito"]
arranjo2d = [[1,2,3,4,5],[6,7,8,9,"dez"],[0,-1,-2,-3,-4]]

# No entanto, é preciso ser cuidado quanto a isso, quando você inicializa um vetor ou *array* preenchido com apenas um tipo de dado, Julia atribui esse tipo à sua estrutura, tentar modificar ou inserior um elemento de outro tipo na coleção gera um erro.

vetor2 = [1,2,3,4,5]

# Veja que quando o vetor é inicializado ele passa a ser um elemento `Vector{Int64}` preenchido com inteiros de 64 bits, se por ventura tentar modificar um dos valores para ponto flutuante ocorrerá um erro já que os elementos foram tipificados. 

vetor2[2] = 2.5

# Elementos podem ser adicionados dentro de um vetor utilizando a função modificadora `push!()` e retirados por meio da função `pop!()`

push!(vetor2, 6)
println(vetor2)
pop!(vetor2)
println(vetor2)

# Atenção ao fato de que as funções utilizadas `push!()` e `pop!()`, respectivamente, adicionam e retiram elementos __ao final da lista__.

# __Que tal um cafezinho__ ☕?

# ## Laços 🎀
#
# Anteriormente você notou o comentário que deixei logo após essa coisa absurda aqui?
#
# ~~~Julia
# jogador1 = rand(1:6);
# jogador1 += rand(1:6);
# jogador1 += rand(1:6);
# jogador2 = rand(1:6);
# jogador2 += rand(1:6);
# jogador2 += rand(1:6);
# # Já já vamos melhorar essa coisa horrível aí em cima
# ~~~
#
# Os prgramas de computador existem exatamente para se evitar esse tipo de abordagem, repetir manualmente várias vezes uma determinada operação ou instrução, o que é totalmente contrário aos princípios de automatização da computação.  
#
# Para promover ações repetitivas consecutivas como essas, todas as linguagens de programação oferecem recursos de repetição denominados laços. 
#
# Os laços, ou _loops_ são blocos de código que serão executados um determinado número de vezes, ou enquanto uma determinada condição for satisfeita, ou seja resolvida como `true`. 
#
# A estrutura de repetição cujo o número de execuções (iterações), é previamente controlada, é denominada laço `for`. Já a estrutura de repetição cujo o controle do número de iterações é dependente do atendimento a uma condição lógica previamente estabelecida é denominada laço `while`. 
#
# Vamos refatorar o jogo de dados utilizando primeiro um laço `while` e então repetiremos o procedimento utilizando um laço `for`.
#
# Utilizaremos a seguinte estratégia, em primeiro lugar, criaremos uma variável para armazenar o número de jogadas disponíveis para cada jogador, e então a decrementaremos iterativamente para determinar o ganhador.

jogadas = 3
jogador1, jogador2 = 0, 0
while jogadas > 0
    println("INÍCIO DE RODADA!")
    println("Jogador 1, lance o dado agora.")
    jogador1 += rand(1:6);
    
    println("Jogador 2, lance o dado agora.")
    jogador2 += rand(1:6);
    jogadas -= 1
end

# Note a declaração do critério de parada à direita de `while`, que determina que o bloco será executado enquanto a variável `jogadas` for maior de 0. 
# Associando esse código ao teste lógico implementado anteriormente, pode-se obter um programa mais estruturado, lógico e funcional.

jogadas = 3
jogador1, jogador2 = 0, 0
while jogadas > 0
    println("INÍCIO DE RODADA!")
    println("Jogador 1, lance o dado agora.")
    jogador1 += rand(1:6);
    
    println("Jogador 2, lance o dado agora.")
    jogador2 += rand(1:6);
    jogadas -= 1
end
if jogador1 == jogador2
    println("EMPATE!")
elseif jogador1 > jogador2
    println("Vitória do Jogador 1 com $jogador1 pontos contra $jogador2 pontos do Jogador 2.")
else
    println("Vitória do Jogador 2 com $jogador2 pontos contra $jogador1 pontos do Jogador 1.")
end

# O laço `for` por sua vez, tem sua execução controlada por meio da iteração com os elementos de um iterável. Um iterável é uma estrutura que passa um valor para o bloco `for` cada vez que é solicitado. Quando todos os elementos do iterável são consumidos pelo for, diz-se que o iterável se extenuou, e o bloco cessa a execução. 
#
# Como o tamanho do iterável é geralmente conhecido ou conhecível, imediatamente antes do início das interações, sabe-se perfeitamente bem, quantas vezes aquele bloco será executado. 
#
# Lista e tuplas são iteráveis, dicionários também, sob certas condições, como serão três lançamentos para cada jogador, criaremos uma lista com os elementos 1, 2 e 3 como interável para o bloco.

jogador1, jogador2 = 0, 0
for j in [1,2,3] # iterável do bloco for
    println("INÍCIO DE RODADA!")
    println("Jogador 1, lance o dado agora.")
    jogador1 += rand(1:6);
    
    println("Jogador 2, lance o dado agora.")
    jogador2 += rand(1:6);
end
if jogador1 == jogador2
    println("EMPATE!")
elseif jogador1 > jogador2
    println("Vitória do Jogador 1 com $jogador1 pontos contra $jogador2 pontos do Jogador 2.")
else
    println("Vitória do Jogador 2 com $jogador2 pontos contra $jogador1 pontos do Jogador 1.")
end

# Opcionalmente, uma estrutura mais otimizada poderia ser utilizada no lugar da lista, um `UnitRange`. Essa estrutura promove a criação de um iterador unitário iniciando no primeiro elemento declarado e terminando no último elemento declardo. Um `UnitRange` com elementos de 0 a 100 seria declarodo em Julia da seguinte forma `1:100`. Vamos substituir a lista por um `UnitRange` em nosso exemplo.

jogador1, jogador2 = 0, 0
for j in 1:3 # iterável do bloco for
    println("INÍCIO DE RODADA!")
    println("Jogador 1, lance o dado agora.")
    jogador1 += rand(1:6);
    
    println("Jogador 2, lance o dado agora.")
    jogador2 += rand(1:6);
end
if jogador1 == jogador2
    println("EMPATE!")
elseif jogador1 > jogador2
    println("Vitória do Jogador 1 com $jogador1 pontos contra $jogador2 pontos do Jogador 2.")
else
    println("Vitória do Jogador 2 com $jogador2 pontos contra $jogador1 pontos do Jogador 1.")
end

# Outras melhorias poderiam ser feitas, mas por hora o código está bem mais lógico e estruturado que antes.

#

# ## Funções
#
# Finalmente chegamos a uma dos elementos mais úteis de qualquer linguagem de programação, as funções. Uma função bem modelada e estruturada pode, virtualmente, representar qualquer coisa que se queira, basta o programador ter suficiente expertise para tal. 
#
# As funções são blocos de código que realizam tarefas específicas e que podem ser utilizadas em diferentes momentos do seu código para realizar aquela tarefa. Porém, ainda possuem uma versatilidade adicional, o comportamento da função é determinado não apenas por seu conteúdo, mas também pelos dados que são passados para ela, de modo que, uma mesma função, pode realizar tarefas bem diferentes quando dados de diferentes tipos são passados para sua operação. 
#
# Vamos iniciar definindo uma função matemática na forma de função computacional. Utilizaremos uma função de segundo grau como modelo matemático para nossa implementação.
#
# $$
# f(x) = ax^2 + bx + c
# $$
#
# Esta função funciona com base em 4 distintos parâmetros, os coeficientes constantes a, b e c e a variável intedependente x, estes parâmetros de funcionamento devem ser recebidos externamente pela função e tratados internamente por ela. Para dizer a Julia que a, b, c e x devem ser informados pelo cliente da função, estes parâmetros devem ser declarados ao se definir a mesma. 

# +
function func_2grau(par_a, par_b, par_c, var_x)
    return par_a * var_x^2 + par_b * var_x + par_c
end 

func_2grau(10 , 2, 5, 6)
# -

# Obviamente as funções não precisam ser matemáicas veja o seguinte exemplo:

# +
function saudacoes(nome, print_=false)
    if print_
        println("Saudações terráqueo $nome")
        return nothing
    else
        return "Saudações terráqueo $nome"
    end
end

saud = saudacoes("Reginaldo", true);
# -

# Funções de uma linha, principalmente as matemáticas, também podem ser declaradas como funções anônimas.
# Uma função anônima é aquela que, ao invés de receber um nome para si quando criada, é armazenada em uma variável no lugar disso. Veja o caso da implementação da função do segundo grau anterior em uma função anônima.x

func2d = (a,b,c,x) -> a*x^2 + b*x + c
func2d(10, 2, 5, 6) == func_2grau(10 , 2, 5, 6)

# __Hora de descansar pessoal!__

# ## *ESSE CURSO FOI PRODUZIDO PELO _GRUPO DE ESTUDOS EM SISTEMAS ENERGÉTICOS E SIMULAÇÃO COMPUTACIONAL_ (GESESC)*
# ![GESESC](logo_gesesc.png)
#
# ### *Todos os direitos reservados segundo os termos da licença MIT*



StarcoderdataJupyterScriptsDedupFiltered