# L11 - Le strutture dati (parte 2)

# ---==== SLICING ====---
# Il concetto di slicing è molto importante per le liste e le stringhe.
# Lo slicing permette di estrarre una porzione di una lista o di una stringa.
# La sintassi per lo slicing è la seguente:
# lista[start:stop:step]

# - start: indice di partenza dell'intervallo da estrarre (incluso)
# - stop: indice di fine dell'intervallo da estrarre (escluso)
# - step: passo dell'intervallo da estrarre

# Esempio:
#               start            stop: 7 escl
#                 |               | 
# indici: 0   1   2   3   4   5   6   7   8   9
lista = [10, 20, 30, 40, 50, 60, 70, 80, 90, 100]
sottolista = lista[2:7:2]
print(sottolista)  # Output: [30, 50, 70]
# Perché? Perché si parte dall'indice 2 (incluso) e si arriva all'indice 7 (escluso) con passo 2.
#         Ovvero, si parte dall'elemento 3 (indice 2), si arriva all'elemento 7 (indice 6) ma per
#         farlo si salta un elemento ogni volta.

# Capita veramente raramente di dover usare lo step, ma è comunque utile conoscerlo.
# Se non si specifica lo step, viene considerato 1 (ovvero, si prendono tutti gli elementi).

# start e stop invece vengono usati un po' più spesso. Anche questi non sono obbligatori.
# Però c'è da considerare che se non si vogliono mettere, è comunque necessario lasciare i due punti (:)
# perché altrimenti Python non capisce che si sta facendo uno slicing e pensa che si stia cercando
# di accedere a un elemento della lista tramite il suo indice (i.e.: lista[2] invece di lista[2:]).
# Gli unici due punti che si possono omettere sono quelli finali, dopo i quali è solitamente presente lo step.
# Però anche in quel caso, dipende dal contesto: se si vogliono specificare solo lo start e lo step, è necessario
# inserire due due punti: lista[2::2].

# Se si omette lo start, viene considerato 0. Se si omette lo stop, viene considerata la lunghezza della lista.

# Esempio:
lista = [10, 20, 30, 40, 50, 60, 70, 80, 90, 100]
sottolista = lista[2:7]
print(sottolista)  # Output: [30, 40, 50, 60, 70]

sottolista = lista[:7] # Si può leggere come "dall'inizio fino all'indice 7 escluso"
print(sottolista)  # Output: [10, 20, 30, 40, 50, 60, 70]

sottolista = lista[2:] # Si può leggere come "dall'indice 2 incluso fino alla fine"
print(sottolista)  # Output: [30, 40, 50, 60, 70, 80, 90, 100]

# Ovviamente il tutto funziona anche con indici negativi.
# Esempio:
sottolista = lista[-7:-2] # Si può leggere come "dall'indice -7 incluso (ovvero 40, il sett-ultimo elemento) fino all'indice -2 escluso (ovvero 90 escluso)"
print(sottolista)  # Output: [40, 50, 60, 70, 80]

sottolista = lista[-7:] # Si può leggere come "dall'indice -7 incluso (ovvero 40, il sett-ultimo elemento) fino alla fine"
print(sottolista)  # Output: [40, 50, 60, 70, 80, 90, 100]

sottolista = lista[:-2] # Si può leggere come "dall'inizio fino all'indice -2 escluso (ovvero 90 escluso)"
print(sottolista)  # Output: [10, 20, 30, 40, 50, 60, 70, 80]

# Esempio di utilizzo dello slicing per invertire una lista:
lista = [1, 2, 3, 4, 5]
lista_invertita = lista[::-1]
print(lista_invertita)  # Output: [5, 4, 3, 2, 1]
# Lo step -1 permette di scorrere la lista al contrario.
# Funziona anche con le stringhe.

# ---==== STRINGHE ====---
# Le stringhe possono essere considerate come delle liste di caratteri.
# Ciò significa che si possono utilizzare pressoché gli stessi metodi e le stesse operazioni
# delle liste anche sulle stringhe.

# Esempio la stringa "ciao" può essere considerata, nella codifica di un algoritmo, come
# una lista di caratteri ['c', 'i', 'a', 'o'].

# Accesso con indice:
stringa = "ciao"
print(stringa[0])  # Output: 'c'
print(stringa[1])  # Output: 'i'

# Modifica di un carattere:
# Le stringhe sono immutabili, cioè non possono essere modificate.
# Quindi, non è possibile modificare un singolo carattere di una stringa.
# Questo significa che il seguente codice genera un errore:
stringa[0] = 'C'  # TypeError: 'str' object does not support item assignment

# Ma è possibile creare una nuova stringa con il carattere modificato:
nuova_stringa = "C" + stringa[1:] # La notazione [1:] significa "dal carattere 1 in poi"

# Concatenazione di stringhe:
# Le stringhe possono essere concatenate utilizzando l'operatore +.
stringa1 = "ciao"
stringa2 = " mondo"
stringa3 = stringa1 + stringa2
print(stringa3)  # Output: "ciao mondo"

# Lunghezza di una stringa:
# La funzione len() può essere usata anche sulle stringhe per ottenere la loro lunghezza.
lunghezza = len(stringa)
print(lunghezza)  # Output: 4

# Esistono dei metodi delle stringhe che permettono di svolgere operazioni specifiche
# e che non sono disponibili per le liste. Ad esempio:
# - upper(): converte tutti i caratteri della stringa in maiuscolo
# - lower(): converte tutti i caratteri della stringa in minuscolo
# - capitalize(): converte il primo carattere della stringa in maiuscolo
# - title(): converte il primo carattere di ogni parola in maiuscolo
# - strip(): rimuove gli spazi bianchi all'inizio e alla fine della stringa
# - split(): divide la stringa in una lista di sottostringhe utilizzando un separatore
# - join(): unisce una lista di stringhe in una sola stringa utilizzando un separatore
# Ce ne sono molti altri, non li elenco perché una delle skill più importanti di un programmatore
# è saper cercare e trovare le informazioni di cui ha bisogno su internet.

# Esempio di utilizzo del metodo split():
stringa = "ciao mondo"
sottostringhe = stringa.split(" ")
print(sottostringhe)  # Output: ['ciao', 'mondo']
print(type(sottostringhe))  # Output: <class 'list'>

stringa = " buonasera a tutti, mi chiamo Bierozzo !"
stringa = stringa.strip()
print(stringa)  # Output: "buonasera a tutti, mi chiamo Bierozzo !" (senza spazi all'inizio e alla fine)
print(stringa.split(" "))  # Output: ['buonasera', 'a', 'tutti,', 'mi', 'chiamo', 'Bierozzo', '!']
print(stringa.split(","))  # Output: ['buonasera a tutti', ' mi chiamo Bierozzo !']

url = "https://www.youtube.com/watch?v=dQw4w9WgXcQ"  # "You know the rules, and so do I"
pezzi_di_url = url.split("/")
print(pezzi_di_url) # Output: ['https:', '', 'www.youtube.com', 'watch?v=dQw4w9WgXcQ']
# Questo è un esempio pratico di utilizzo di split(). Magari in un programma che, ad esempio,
# deve scaricare un video da YouTube, potrebbe essere utile dividere l'URL in pezzi per estrarre l'ID del video.
# (ovviamente, in un ipotetico programma del genere, si dovrebbe utilizzare una qualche libreria che scarica
# i video tramite ID invece che direttamente dall'URL).
# In questo modo, infatti, potremmo recuperare l'ID (ovvero "dQw4w9WgXcQ") dalla lista pezzi_di_url.
video_id = pezzi_di_url[-1].split("=")[-1]
# Perché? Perché l'ID del video è l'ultimo elemento della lista pezzi_di_url e si trova dopo il carattere "=".
#         Quindi, dividendo l'ultimo elemento della lista pezzi_di_url con split("="), otteniamo l'ID del video
#         che è l'ultimo elemento della lista risultante.

# Esempio di utilizzo del metodo join():
sottostringhe = ['ciao', 'mondo']
stringa = " ".join(sottostringhe)
print(stringa)  # Output: "ciao mondo"
# Quello che abbiamo fatto qui è applicare il metodo join() alla stringa " " (uno spazio) e passare
# come argomento la lista di sottostringhe. Il metodo join() unisce tutte le sottostringhe della lista
# in una sola stringa, separandole con il carattere su cui è stato chiamato il metodo, in questo caso uno spazio.

stringa = "...".join(sottostringhe)
print(stringa)  # Output: "ciao...mondo" 

# N.B.: Ci sono molte operazioni che si possono fare con le liste, ma non con le stringhe.
# E questo dimostra che le stringhe sono diverse dalle liste e le consideriamo simili solo
# per semplificare il processo logico di codifica di un algoritmo.

# Lo slicing funziona anche con le stringhe.
# Esempio:
stringa = "ciao mondo"
sottotringa = stringa[2:7]
print(sottotringa)  # Output: "ao mo"