(per il gruppo A-L)
- Introduzione alla console
cd
pwd
ls
man
einfo
- l'interprete
ruby
- introduzione al filesystem Unix
- La funzione
puts
e l'interpolazione#{ ... }
- La programmazione condizionale
if then ... else ... end
Esercitazione: Radici di un polinomio di secondo ordine
- ripasso console
- introduzione a
irb
(interprete ruby interattivo) - ripasso assegnazione
- ripasso
if
- esteso ad
if..elsif
,if
postfisso e operatore ternario... ? ... : ...
- introdotti
not
and
or
- introdotti cicli
while
efor
while condizione_vera
end
for i in 0..n do
end
- introdotte le funzioni
- definizione del fattoriale
- funzione ricorsiva fattoriale
Esercitazione: coefficiente binomiale
# Funzione fattoriale
def fatt(n)
ret = 1
for i = 1..n do
ret *= i
end
return ret
end
def coeff_bin(n,k)
return fatt(n)/(fatt(k) * fatt(n - k))
end
puts "(10,3) = #{coeff_bin(10,3)}"
- attributi di accesso ai file
- rendere eseguibile un file:
chmod
e shebang - ripasso funzioni, cicli
for
ewhile
Esercitazione: Definire la funzione di Collatz
e definita la sequenza di Collatz la serie
(ovvero la serie termina quando converge a 1). Trovare nell'insieme di valori iniziali
#!/usr/bin/env ruby
# Attenzione: questo codice non funziona nell'interprete browser
def collatz(n)
return (n % 2 == 0 ? n/2 : 3 * n + 1)
end
def serie(n)
z = 0
while n != 1
z += 1
n = collatz(n)
end
return z
end
# Main
z_max = 1
n_max = 1
for n in 1..10000 do
z = serie(n)
z_max, n_max = z, n if z > z_max
end
puts "Numero = #{n_max}, Iterazioni = #{z_max}"
Esercitazione proposta sotto forma di competizione in aula, vinta da Valentin Bernard. Soluzione:
- Introduzione teorica alle classi
- Ripasso delle classi
- incapsulamento (separazione interfaccia implementazione)
- polimorfismo (client utilizzano interfacce comuni)
- ereditarietà (una classe può ereditare metodi e attributi da una sua classe antenata)
- Panoramica delle classi Tipo di Dato fondamentali:
- Numeric
- Fixnum e Bignum
- Array
- Hash
- String
- NilClass
- Le
variabili
e leCostanti
- I numeri interi (Fixnum e Bignum) e i numeri Float
- definizione e alcuni esempi di metodi associati
- trasformazione da intero a float e viceversa (
to_f
,to_i
,abs
,floor
,ceil
,round
)
- Array
- definizione di Array e di indice
size
- attraversare gli Array
Esercitazione: Ottimizzare la soluzione del problema di Collatz in modo da espandere la ricerca ad ordini di grandezza superiore in un lasso di tempo ragionevole (nessuna ottimizzazione dal punto di vista di utilizzo della memoria)
#!/usr/bin/env ruby
# Attenzione: questo codice non funziona nell'interprete browser
# Parte il cronometro
tic = Time.new
def collatz(n)
return (n % 2 == 0 ? n/2 : 3 * n + 1)
end
def serie(n, z_ary)
z = 0
while n != 1
z += 1
n = collatz(n)
# Controlliamo se esiste già la soluzione per n
# all'interno del nostro z_ary. Se la soluzione
# è già stata calcolata, la utilizziamo e
# usciamo dal ciclo while
if z_ary[n]
z += z_ary[n]
break
end
end
return z
end
# Main
MaxNum = 1000000
z_max = 1
n_max = 1
# Creiamo un nuovo Array, dentro il quale
# per ogni indice n salviamo la soluzione z
# della serie di Collatz
z_ary = Array.new(MaxNum + 1)
# Un Array creato in questo modo sarà un Array
# formato da n + 1 elementi tutti nil
puts "BEGIN"
for n in 1..MaxNum do
z = serie(n, z_ary)
# Salviamo l'ultimo risultato nell'Array
z_ary[n] = z
z_max, n_max = z, n if z > z_max
end
puts "END"
puts "Numero = #{n_max}, Iterazioni = #{z_max}"
# Si arresta il cronometro
toc = Time.new
puts "Computation time: #{(toc - tic)} seconds"
Il primo a risolvere il problema è stato Valentin Bernard.
Leggete gli avvisi in cima alla pagina!
- Ripasso rapido degli Array: indice a base 0, dimensione, attraversamento
- Esercizio: Scrivere una funzione che trova massimo e minimo in Array anche se contiene tipi di dato diversi da
Numeric
# funzione minmax
# input:
# ary - un array
# output:
# [min,max] - un array contenente il
# valore minimo e massimo
# La funzione è robusta rispetto a tipi di dato
# che non sono numerici
def minmax(ary)
min = Float::INFINITY # <- Non me lo ricordavo (-.-')
max = -Float::INFINITY
for i in 1...ary.size
# Controlliamo se l'elemento è Numeric
if ary[i].is_a?(Numeric) then
min = ary[i] if ary[i] < min
max = ary[i] if ary[i] > max
end
end
return [min, max]
end
- Esercitazione: Definire la funzione Algoritmo BUBBLE SORT (a lezione ne abbiamo presentate due versioni: la prima corretta, la seconda modificata a fini didattici)
# funzione bubble_sort
# input
# a - un array non ordinato
# output
# a - un array ordinato
# Ordina il contenuto di un Array
def bubble_sort(a)
for j in 1..a.size
for i in 0...a.size - j
a[i], a[i+1] = a[i+1], a[i] if a[i] > a[i+1]
end
end
return a
end
ary = (0..10).to_a.shuffle
bubble_sort(ary)
# funzione bubble_sort
# input
# a - un array non ordinato
# output
# a - un array ordinato
# Ordina il contenuto di un Array
# Versione didattica con un po'
# di stampa a schermo
def bubble_sort_did(a)
puts "INITIAL = #{a} (#{a.size} elements)"
for j in 1...a.size
n = a.size - j
puts "** Scan: 0 to #{n}"
for i in 0...n
if a[i] > a[i+1]
puts "**** Change a[#{i}] = #{a[i]} <-> a[#{i + 1}] = #{a[i+1]}"
a[i], a[i+1] = a[i+1], a[i]
end
end
puts "** Array = #{a}"
puts
end
puts "FINAL = #{a}"
return a
end
bubble_sort_did(ary.shuffle)
- Introduzione all'utilizzo dei blocchi
- Introduzione alla scrittura dei blocchi
ary = (1..10).to_a.shuffle
# Utilizzo del blocco EACH
# con do |...| ... end
ary.each do |elemento|
puts "Elemento = #{elemento}"
end
# con { |...| ... }
ary.each { |elemento|
puts "Elemento = #{elemento}"
}
# utilizzo del blocco EACH_WITH_INDEX
ary.each_with_index { |elemento, indice|
puts "Elemento[#{indice}] = #{elemento}"
}
# Costruire una nostra funzione each
def mio_each(ary)
for i in 0...ary.size
yield ary[i]
end
end
# Costruire una nostra funzione each with index
def mio_each_with_index(ary)
for i in 0...ary.size
yield ary[i], i
end
end
# Usare le nostre funzioni
mio_each(ary) do |e|
puts "Elemento = #{e}"
end
mio_each_with_index(ary) do |e, i|
puts "Elemento[#{i}] = #{e}"
end
- Introduzione a
String
. Inizializzazione (con o senza interpolazione) e metodi. - Introduzione ai
Symbols
, utilizzati come elementi di identificazione univoca all'interno del codice. - Introduzione agli
Hash
, contenitori di dati "ordinati per chiave arbitraria". - Esercitazione: scrivere una piccola rubrica personale, con i metodi per stampare i contatti, cercare e ordinare. Attenzione, per poter mettere un argomento di dafault in una funzione, il contenuto dell'argomento deve essere una variabile globale (quindi iniziare con la lettere
$
- è un problema di visibilità della variabile), a differenza di quanto abbiamo visto a lezione (errore mio, scusate). Questo non è vero quando la funzione è un metodo che fa parte di una classe.
DB = [
{ :nome => "John", :cognome => "Dorian", :tel => "1234567"},
{ :nome => "Percival", :cognome => "Cox", :tel => "3425345"},
{ :nome => "Christopher", :cognome => "Turkleton", :tel => "calltur(k)"}, # :D
{ :nome => "Carla", :cognome => "Espinoza", :tel => "9838762"},
{ :nome => "Jan", :cognome => "Itor", :tel => "9873624"} # 8)
]
# stampa a schermo il contenuto di ary
# se non si passa nessun valore usa di
# default l'intero database DB
def stampa(db = DB)
db.each { |e|
puts "#{e[:nome]} #{e[:cognome]}: #{e[:tel]}"
}
end
# cerca all'interno di un array tutti i record
# che hanno come valore value alla chiave key
# Restituisce un array popolato di tutti i record
# della query (ricerca)
def cerca(key, value, db = DB)
record = []
db.each { |e|
record << e if e[key] == value
}
end
# ordina gli elementi all'interno dell'array
# basandosi suala chiave key. Se non si passa nessun valore
# usa in automatico il simbolo :cognome
def ordina(key = :cognome, db = DB)
for j in 1..db.size
for i in 0...db.size - j
db[i], db[i+1] = db[i+1], db[i] if db[i][key] > db[i+1][key]
end
end
return db
end
# utilizziamo le funzioni che abbiamo definito
puts "Stampa tutta la rubrica"
stampa
puts "Cerca Percival e stampa"
stampa( cerca(:name, "Percival") )
puts "Ordina tutto l'array sulla base del nome"
stampa( ordina(:nome) )
- I block, costruire un integratore con il metodo dei trapezi, basato sulla relazione
def integratore(a,b,n)
deltax = (b - a)/n
result = 0.0
for s in 0...n
x_s = s*deltax + a
fA = yiald(x_s)
fB = yield(x_s + deltax)
result += 0.5 (fA + fB) * deltax
end
return result
end
risultato = integratore(1, 2, 100) { |x| Math::E**(Math::cos(x)) }
- Le classi
Proc
- I file: apertura, lettura e scrittura
#!/usr/bin/env ruby
File.open("data.txt", "w") { |file|
for i in 1..10 do
for j in 1..10 do
file.print "#{i + j}\t"
end
file.puts
end
}
# Leggere il file
ary = []
File.foreach("data.txt") { |line|
ary << line.chomp.split("\t").map { |e| e.to_i }
}
# come funziona map?
# proviamo a ricostruirlo
def my_map(ary)
for i in 0...ary.size do
ary[i] = yield(ary[i])
end
return ary
end
- La gestione degli errori
def dividi(a,b)
raise ArgumentError, "a è di tipo #{a.class} deve essere Numeric" if not a.is_a?(Numeric)
raise ArgumentError, "b è di tipo #{b.class} deve essere Numeric" if not b.is_a?(Numeric)
raise RuntimeError, "Divisione per zero" if b == 0
return a/b
rescue RuntimeError
if b == 0 then
b += 10**(-15)
retry
end
ensure
puts "Codice da eseguire SEMPRE"
end
- Implementare il gioco del mastermind
class GameError < RuntimeError; end
=begin
+--- Mastermind --------------------------------+
| |
| Attributi: Metodi: |
| - @length (read) - play() = Bool <----->
| lunghezza codice da interfaccia |
| indovinare per giocare |
| - @range (read) con l'utente |
| range per i perks |
| - @guess (read) - initialize( |
| massimo numero di length = 4, |
| tentativi (negativo range = 1..7,|
| per tent. infiniti) guess = -1, |
| - @try (priv) int = true |
| tentativo ) = obj <------>
| - @code (priv) inzializza un |
| codice da indovinare nuovo oggetto |
| - @interactive (priv) Mastermind |
| interattività (puts) |
| - @crp (priv) - delta(x,y) = d |
| perks corretti nella delta di |
| posizione corretta kronecker |
| - @cwp (priv) |
| perks corretti nella - print_status() |
| posizione sbagliata = nil |
| stampa il |
| risultato del |
| l'ultimo try |
| |
| - count_perks() |
| = [@rcp, @rwp] |
| conta i perks |
| dell'ultima |
| giocata |
+-----------------------------------------------+
=end
class Mastermind
# Definizione degli attributi readable.
attr_reader :length, :range, :try, :guess
# questa è una scorciatoia che definisce i metodi
#
# # attr_reader :variabile
# def variabile
# return variabile
# end
#
# allo stesso modo esistono shortcuts per
# gli attributi writable:
#
# attr_writable :variabile
#
# che definisce il seguente metodo:
#
# def variabile=(v)
# @variabile = v
# end
#
# e per le variabili read&write:
#
# attr_accessor :variabile
# oggetto = initialize(length, range, guess, interactive)
#
# Costruttore dell'oggetto. Solitamente si inizializzano tutte le
# variabili dell'oggetto. Questo è l'unico punto in cui l'utente
# ha controllo sulla partita.
def initialize(length = 4, range = 1..7, guess = -1, int = true)
# controllo sull'argomento length
raise ArgumentError, "length" if not @length.is_a?(Fixnum)
@length = length
# Controllo sull'argomento guess
raise ArgumentError, "guess" if not @guess.is_a?(Fixnum)
@guess = guess
# controllo sull'argomento range, che può essere un
# Fixnum o un Range
if range.class == Fixnum and range > 1 then
@range = 1..range
elsif range.class == Range then
@range = range
else
raise ArgumentError, "range"
end
if int == true or int == false then
@interactive == int
else
raise ArgumentError, "int"
end
# inizializza le variabili private
@code = []
@rcp = 0
@rwp = 0
@try = 0
end # initialize
# (true, false) = play()
#
# La partita segue questa logica
#
# reset -> player = yield(@rcp, @rwp) <----+
# | |
# +-> count_perks(player) |
# | True|
# +-> (logica giocata)?* ----+
# False| False
# +-> (@rcp == @length)? -+-----> Sconfitta
# | return false
# |True
# +-----> Vittoria
# return true
#
# * logica giocata:
# se @guess > 0:
# @try <= @guess and @rcp != @length
# altrimenti
# @rcp != @length
#
# La logica giocata tiene conto della strategia di uscita per l'utente:
# se l'utente gioca "exit", "quit", ... etc. il programma esce.
def play
reset
while (@guess > 0 ? @try <= @guess : true) and @rcp != @length
begin
player = yield(@rcp, @rwp)
raise Exception if %w|exit quit close q x c|.include?(player)
raise GameError, "Play must be an Array. I received #{player.class}" if not(player.is_a?(Array))
raise GameError, "Play must be of #{@length} perks. I received #{player.size} perks" if player.size != @length
player.each { |e|
raise GameError, "Perks must be Fixnum, not #{e.class}" if not(e.is_a?(Fixnum))
}
count_perks(player)
print_status
rescue GameError => err
puts "[ERROR] #{err.message}"
if @interactive then
retry
else
exit(1)
end
rescue Exception
puts "The code is #{@code}. Bye looser!" if @interactive
exit(0)
end
end
if @rcp == @length then
puts "[PC] YOU WIN!" if @interactive
return true
else
puts "[PC] Stupid LOOSER!" if @interactive
return false
end
end # play
# nil = print_status
# Stampa a schermo lo stato dell'ultima giocata
def print_status
if @interactive then
puts "[PC] Perks in correct position: #{@rcp}"
puts "[PC] Perks in wrong position: #{@rwp}"
print "[PC] Tries: #{@try}"
puts (@guess > 0 ? " of #{@guess}" : "")
end
end # print_status
private
# d = delta(x,y)
# Stampa a schermo il detla di kronecker:
# d = 1 se x == y
# d = 0 se x != y
def delta(x, y)
return (x == y ? 1 : 0)
end # delta
# @code = reset()
# resetta una partita generando un nuovo codice.
def reset
puts "[PC] Generating a new game..." if @interactive
@code = []
@rcp = 0
@rwp = 0
@try = 0
@code = Array.new(@length) { Random.rand(@range) }
end # reset
# [@rcp, @rwp, @try] = count_perks(player)
# Calcola la giocata sulla base del codice inserito dal
# giocatore. Utilizza le formule in calce a questo script.
def count_perks(player)
@rcp = 0
for j in 0...@length do
@rcp += delta(@code[j], player[j])
end
@rwp = 0
for i in @range do
alpha, beta = 0, 0
for j in 0...@length do
alpha += delta(@code[j], i)
beta += delta(player[j], i)
end
@rwp += [alpha, beta].min
end
@rwp -= @rcp
@try += 1
[@rcp, @rwp, @try]
end # count_perks
end # class Mastermind
# Ambiente di test per la classe
if $0 == __FILE__ then
game = Mastermind.new
game.play { |rcp, rwp|
print "Insert: "
s = gets.chomp
if %w|quit close x c q exit|.include?(s)
s
else
s.split(/\s*,\s*/).map { |e| e.to_i }
end
}
end
- Risoluzione esercizio d'esame (problema del campionato). La soluzione prevede l'uso di classi specializzate che ereditano dalla classe
Hash
. Questo ci ha permesso di introdurre le keywordsuper
(richiama lo stesso metodo della classe precedente, passando gli stessi argomenti), e laself
.
#!/usr/bin/env ruby
=begin
http://goo.gl/UYM4kv -> Testo otriginale esercizio
http://goo.gl/ZuuXQf -> campionato.txt
-- esercizio ispirato da ---
Esercizi di programmazione in C
Esercitazioni per il corso di Fondamenti di Informatica
Fulvio Corno Silvia Chiusano Politecnico di Torino – Dipartimento di Automatica e Informatica
-------------------------
Realizzare un programma in grado di calcolare la classifica di un campionato di calcio giocato
tra N squadre, numerate consecutivamente a partire da 0.
I risultati delle partite sono memorizzati in un file ASCII il cui nome e' passato come
primo argomento alla funzione classifica.
Questo file contiene un risultato per riga nel formato:
squadra1 squadra2 goal1 goal2
ove squadra1 e squadra2 sono stringhe con i nomi delle squadre che si sono incontrate
mentre e goal1 e goal2 sono le reti segnate da ciascuna squadra.
Le colonne sono separate da \t.
Il programma deve calcolare e riempire una hash di output.
Le chiavi della hash sono stinghe e sono i nomi delle squadre.
I valori della hash sono a loro volta delle hash chiave -> valore che contengono
:partite => numero di partite giocate
:punti => i punti conseguiti (si ricorda che la vittoria vale tre punti ed il pareggio un punto)
:vinte => numero di partite vinte
:perse => numero di partite perse
A titolo di esempio, supponendo che il file CAMPIONATO.TXT contenga le seguenti informazioni:
ROSSI BIANCHI 1 1
VERDI NERI 1 0
ROSSI NERI 2 0
allora la hash risultante deve contenere le seguenti informazioni:
{
'ROSSI' => { :partite => 2, :punti => 4, :vinte => 1, :perse => 0 },
'BIANCHI' => { :partite => 1, :punti => 1, :vinte => 0, :perse => 0 },
'NERI' => { :partite => 2, :punti => 0, :vinte => 0, :perse => 2 },
'VERDI' => { :partite => 1, :punti => 3, :vinte => 1, :perse => 0 },
}
=end
################################################################################
# RISOLVERE L'ESERCIZIO QUI
################################################################################
require 'pry'
class CampArgumentError < ArgumentError; end
class CampRuntimeError < RuntimeError; end
class Partita < Hash
def initialize(ary)
super
if ary.is_a?(Array) and ary.size == 4 then
result = ary[2] <=> ary[3]
self[ary[0]], self[ary[1]] = result, -result
else
raise ArgumentError "Wut??"
end
end
end
class Squadra < Hash
def initialize
super
self[:partite] = 0
self[:vinte] = 0
self[:perse] = 0
self[:punti] = 0
end
def result(p)
self[:partite] += 1
if p == 1 then
self[:vinte] += 1
self[:punti] += 3
elsif p == 0 then
self[:punti] += 1
elsif p == -1 then
self[:perse] += 1
end
end
end
class Campionato < Hash
def initialize(file)
if File.exist?(file) then
File.foreach(file) { |line|
self.+ Partita.new(line.chomp.split("\t"))
}
else
raise CampArgumentError, "Non esiste il file: #{file}"
end
end
def +(p)
if p.is_a?(Partita) then
p.keys.each { |squadra|
self[squadra] = Squadra.new if not self.keys.include?(squadra)
self[squadra].result(p[squadra])
}
else
raise CampArgumentError, "Mi aspetto una partita, non #{p.class}"
end
end
end
def campionato(file)
return Campionato.new(file)
end
RESULT = campionato("campionato.txt")
binding.pry
################################################################################
#
################################################################################
- Risoluzione dell'esercizio del metodo di approssimazione zeri funzione
star
, ponendo l'accento sulla efficienza del codice.
#!/usr/bin/env ruby
=begin
http://goo.gl/ -> Testo originale esercizio
Scrivere la funzione `star` che si aspetta 3 argomenti
x0 = approssimazione iniziale della radice
x1 = seconda approssimazione iniziale della radice
tol = tolleranza ammessa
imax = massimo numero di iterate
ed un blocco che restituisce il valore della funzione della
quale cechiamo lo zero.
La funzione deve implementare il metodo iterativo STAR per il calcolo
dello zero di una funzione. La funzione restituisce una hash con 3 chiavi
:solution => con la soluzione
:iter => un intero con il conteggio del numero delle iterate fatte
:converged => true/false (vero se arrivato a convegenza)
Se x0, x1, x2 sono le ultime tre
iterate e f0 = f(x0), f1 = f(x1), f2 = f(x2) allora x3 con il metodo STAR
si calcola con
f2
x3 = x2 - ---------------
d20 + d21 - d10
dove
f2 - f0 f2 - f1 f1 - f0
d20 = --------- d21 = --------- d10 = ---------
x2 - x0 x2 - x1 x1 - x0
quindi il metodo star richiede le ultime 3 approssimazioni della radice
mentre all'inizio ne forniamo solo 2.
Per far partire il metodo usiamo quindi UN SOLO passo del metodo
delle secanti
f1 f1 - f0
x2 = x1 - ----- d10 = ---------
d10 x1 - x0
Le iterate terminano se si supera imax (fallimento) o se |f(xk)| <= tol (successo)
ESEMPIO:
approssimare la radice di f(x) = (3x/2)**4-1
x0 = 10
x1 = 9
tol = 1e-12
imax = 20
sol = star(x0,x1,tol,imax) { |x| (3*x/2)**4-1 }
sol è una hash che contiene sol = { :solution => ..., :iter => ... , :converged => ...}
=end
################################################################################
# RISOLVERE L'ESERCIZIO QUI
################################################################################
require 'pry'
def star( x0, x1, tol, imax )
# Controllo degli argomenti
raise ArgumentError, "Fornire un blocco" if not block_given?
if x0.is_a?(Numeric) then
x0 = x0.to_f
else
raise ArgumentError, "x0: must be Numeric, passed #{x0.class}"
end
if x1.is_a?(Numeric) then
x1 = x1.to_f
else
raise ArgumentError, "x1: must be Numeric, passed #{x1.class}"
end
if tol.is_a?(Numeric) then
tol = tol.to_f
else
raise ArgumentError, "tol: must be Numeric, passed #{tol.class}"
end
if imax.is_a?(Numeric) then
imax = imax.to_i
else
raise ArgumentError, "imax: must be Numeric, passed #{imax.class}"
end
# inizializzazione del risultato
ret = { :solution => nil, :iter => 0, :converged => false}
# Definite qui per motivi di visibilità
x2, f0, f1 = nil, nil, nil
# Main loop da eseguire fino a quando
while (ret[:iter] <= imax)
#binding.pry
if x2
# Effettivo calcolo del metodo star
f2 = yield(x2)
break if f2.abs < tol
d20 = (f2 - f0)/(x2 - x0)
d21 = (f2 - f1)/(x2 - x1)
d10 = (f1 - f0)/(x1 - x0)
f0 = f1
f1 = f2
x0 = x1
x1 = x2
x2 -= f2/(d20 + d21 - d10)
else
# da eseguire solo la prima volta
f0 = yield(x0)
f1 = yield(x1)
d10 = (f1 - f0)/(x1 - x0)
x2 = x1 - f1/d10
end
ret[:iter].next
end
ret[:solution] = x2
ret[:converged] = (ret[:iter] <= imax ? true : false)
return ret
end
x0 = 10.0
x1 = 9.0
tol = 1e-12
imax = 20
sol = star(x0, x1, tol, imax) { |x| (3*x/2)**4-1 }
pp sol
################################################################################
#
################################################################################
- Implementare la classe PlotAscii che data una funzione o dei dati li stampa a schermo sotto forma di grafico
# PlotAscii
# Require returns true if it is possible to
# load a gem, false if it is not possible
$COLORIZE = require 'colorize'
class Array
# Expands Array standard class to add a method
# that is called min_at, that returns the index
# of the minimum element of an Array
def min_at
return index(min)
end
# Evaluates distances froma a given target
def distances(target)
map { |e| (e.is_a?(Numeric) ? (e.to_f - target)**2 : nil) }
end
# Finds the index at wich we have the minimum distance
def find_min_at(target)
distances(target).min_at
end
end # class Array
# This extension of the class String will be performed only if
# and only if the colorize gem is not available
class String
# The following is an hash that contains the escape
# sequence for colors. To print a coloured string whe need
# to enclose it in a first escape sequence and than close the
# string with a resetting escaped sequence
TXTclr = {
:black => "\e[0;30m",
:red => "\e[0;31m",
:green => "\e[0;32m",
:yellow => "\e[0;33m",
:blue => "\e[0;34m",
:purple => "\e[0;35m",
:cyan => "\e[0;36m",
:white => "\e[0;37m"
}
TXTrst = "\e[0m"
# define a new method with the same name as the symbol in TXTclr hash
# that returns a coloured string embraced in ecape tags.
TXTclr.keys.each do |color|
define_method color do
TXTclr[color] << self << TXTrst
end
end
end if not $COLORIZE # class String
class PlotAscii
PX_ON = '*'
PX_OFF = ' '
@@instances = 0
attr_reader :xstep, :lims, :ncols, :nrows
def initialize(ncols = 60, nrows = 20, leftmargin = 2)
# Checking arguments
if nrows.is_a?(Fixnum) then
if nrows > 2 then
@nrows = nrows
else
raise ArgumentError, "Rows number must be positive and greater than 2"
end
else
raise ArgumentError, "Rows number must be a Fixnum"
end
if ncols.is_a?(Fixnum) then
if ncols > 2 then
@ncols = ncols
else
raise ArgumentError, "Cols number must be positive and greater than 2"
end
else
raise ArgumentError, "Cols number must be a Fixnum"
end
if leftmargin.is_a?(Fixnum) then
if leftmargin >= 0 then
@leftmargin = leftmargin
else
raise ArgumentError, "Left margin must be positive"
end
else
raise ArgumentError, " number must be a Fixnum"
end
@lims = {}
lims({ :x0 => 0, :x1 => @ncols,
:y0 => 0, :y1 => @nrows })
@functions = []
@@instances += 1
@title = "Plot #{@@instances}"
clear
end
def lims(h)
h.keys.each do |e|
raise ArgumentError, "#{e} must be a Numeric" if not h[e].is_a?(Numeric)
end
@lims[:x0] = h[:x0].to_f if h[:x0]
@lims[:x1] = h[:x1].to_f if h[:x1]
@lims[:y0] = h[:y0].to_f if h[:y0]
@lims[:y1] = h[:y1].to_f if h[:y1]
raise ArgumentError, ":x1 must be greater then :x0" if @lims[:x0] >= @lims[:x1]
raise ArgumentError, ":y1 must be greater then :y0" if @lims[:y0] >= @lims[:y1]
@lims[:dx] = (@lims[:x1] - @lims[:x0])/@ncols.to_f
@lims[:dy] = (@lims[:y1] - @lims[:y0])/@nrows.to_f
@xpoints = Array.new(@ncols) { |i| (@lims[:x0] + i * @lims[:dx]) + @lims[:dx]/2 }
@ypoints = Array.new(@ncols) { |i| (@lims[:y0] + i * @lims[:dy]) + @lims[:dy]/2 }
return self
end
def xlim=(*varg)
if varg.size == 2 then
return lims({:x0 => varg[0], :x1 => varg[1]})
elsif varg.size == 1 then
if varg[0].is_a?(Array) then
return lims({:x0 => varg[0][0], :x1 => varg[0][1]})
elsif varg[0].is_a?(Hash)
return lims(varg[0])
end
else
raise ArgumentError, "Wrong number of argument in xlim: expected 1 or 2, got #{varg.size}"
end
end
def ylim=(*varg)
if varg.size == 2 then
return lims({:y0 => varg[0], :y1 => varg[1]})
elsif varg.size == 1 then
if varg[0].is_a?(Array) then
return lims({:y0 => varg[0][0], :y1 => varg[0][1]})
elsif varg[0].is_a?(Hash)
return lims(varg[0])
end
else
raise ArgumentError, "Wrong number of argument in ylim: expected 1 or 2, got #{varg.size}"
end
end
def add_function(ch, desc, color = nil, &block)
@functions << {
:class => :function,
:ch => (color ? ch.send(color) : ch),
:desc => desc,
:block => block
}
return self
end
def add_data(ch, desc, color = nil)
data = yield
raise RuntimeError, "Block must pass an Array, not #{data.class}" if not data.is_a?(Array)
data.each do |point|
raise RuntimeError, "Not valid data in add_data" if not point.is_a?(Array)
raise RuntimeError, "Not valid data in add_data" if not (point[0].is_a?(Numeric) and point[0].is_a?(Numeric))
end
@functions << {
:class => :data,
:ch => (color ? ch.send(color) : ch),
:desc => desc,
:data => data
}
return self
end
def title=(s)
@title = s.to_s
end
def draw
@functions.each do |h|
case h[:class]
when :function
(0...@ncols).each do |i|
y = h[:block].call(@xpoints[i])
j = @ypoints.find_min_at(y)
set_pixel(i, j, h[:ch])
end
when :data
h[:data].each do |p|
i = @xpoints.find_min_at(p[0])
j = @ypoints.find_min_at(p[1])
set_pixel(i, j, h[:ch])
end
end
end
end
def to_s
draw
margin = ' ' * @leftmargin
label_width = [@lims[:y0].to_s.size, @lims[:y1].to_s.size].max
label_margin = ' ' * label_width + ' '
# Ticks (upper Y)
@string = margin + "y".rjust(label_width) + ' ' + "▲\n" + margin + label_margin + "│\n"
@string << margin + @lims[:y1].to_s.rjust(label_width) + ' ┼' + "\n"
# Effective plot
@plot.reverse.each { |line| @string << margin + label_margin + '│' + line.join + "\n" }
# Ticks (lower Y)
@string << margin + @lims[:y0].to_s.rjust(label_width) + ' ┼' + ('─' * @ncols) + "┼─► x \n"
@string << margin + label_margin + @lims[:x0].to_s + (' ' * (@ncols - @lims[:x0].to_s.size - @lims[:x1].to_s.size)) + @lims[:x1].to_s + "\n"
@string << "\n Legend:\n"
@functions.each do |h|
@string << " [" << h[:ch] << "] : " + h[:desc] << "\n"
end
return @string
end
# │ ▲ ┼ ─ ►
private
def clear
@plot = Array.new(@nrows) { Array.new(@ncols) { PX_OFF }}
end
def set_pixel(i,j, ch = PX_ON)
if in_range?(i,j)
@plot[j][i] = ch
end
return self
end
def set_pixels(ary, ch = PX_ON)
ary.each do |p|
set_pixel(p[0], p[1], ch)
end
return self
end
def in_range?(i, j)
return ((0...@ncols).include?(i) and (0...@nrows).include?(j))
end
def pixel?(i, j)
return false if not in_range?(i,j)
return (not @plot == PX_OFF)
end
end
if __FILE__ == $0 then
plot = PlotAscii.new
plot.lims({:x0 => -1, :x1 => 10, :y0 => -2, :y1 => 10})
# Simple plot example
plot.add_function("*", "sin(x)", :red) { |x| Math::sin(x) }
# Parametric plot example
[0, 1, 2].each do |p|
plot.add_function("#{p}", "cos(x) + #{p}", :blue) { |x| Math::cos(x) + p }
end
# Point data example
data = []
(0).upto(10) do |e|
data << [e, e]
end
plot.add_data(".", "data", nil) do; data; end
# Print out a plot
puts plot.to_s
end