====== Tema 2 PP 2025 ======
Schelet: {{:pp:2025:scala:skel_tema2.zip|}}
**DEADLINE 4 Mai 2025**
* Temele trebuie submise pe curs.upb.ro, in assignment-ul numit ''Tema 2 Scala''.
* Temele ce nu sunt acceptate de validatorul de arhive **NU** vor fi puncate.
**Folosiți un stil de programare funcțional. NU se vor accepta:**
* **Efecte laterale** (de exemplu modificarea parametrilor dați ca input la funcție)
* **var** (**val** este ok!)
==== Scopul Temei ====
In cadrul acestei teme veti implementa un Query Language inspirat de SQL, ce utilizeaza o baza de date implementata de voi.
Va vom ghida in realizarea operatiunilor atat pe tabele individuale, cat si pe interactiuni intre mai multe tabele, pe care apoi le veti putea combina.
==== Reprezentarea Tabelelor ====
Considerati exemplul de mai jos.
^ Nume ^ Prenume ^ Varsta ^
| Popescu | Ion | 30 |
| Ionescu | Maria | 25 |
Acest tabel poate fi reprezentat ca:
type Row = Map[String, String] // nume_coloana - valoare
type Tabular = List[Row]
let map = Map(1 -> 2, 3 -> 4): Map[Int, Int]
* Adauga o noua pereche cheie-valoare
map + (5 -> 6) // Map(1 -> 2, 3 -> 4, 5 -> 6)
map + (3 -> 5) // Map(1 -> 2, 3 -> 5) -- if key exists, it updates the value
* Eliminarea unei perechi cheie-valoare
map - (3 -> 4) // Map(1 -> 2)
* Accesarea valorii asociate unei chei
map get 1 // return 2
map get 3 // return 4
map getOrElse (1, 0) // return 2
map getOrElse (5, 0) // return 0 - daca cheia nu exista, returneaza valoarea default
map contains 1 // True
map contains 5 // False
* Functii de orin superior
map mapValues (x => x + 5) // Map(1 -> 7, 2 -> 9)
map filterKeys (x => x <= 2) // Map(1 -> 2)
* Combinarea a doua map-uri
val map1: Map[Int, Int] = Map(1 -> 2, 3 -> 4)
val map2: Map[Int, Int] = Map(5 -> 6, 7 -> 8)
map ++ map2 // Map(1 -> 2, 3 -> 4, 5 -> 6, 7 -> 8)
==== Clasa Table ====
Vom defini un tabel ca o clasa care are ca atribute numele tabelei ''tableName'' si datele ''tableData''.
case class Table (tableName: String, tableData: Tabular) {
def header: List[String] = ???
def data: Tabular = ???
def name: String = ???
}
**1.1** Definiti metoda ''toString'' care returneaza tabelul in forma CSV.
override def toString: String = ???
**1.2** Definiti metoda ''fromCSV'' care returneaza un tabel dintr-un string de tip CSV.
def fromCSV(CSV: String): Table = ???
**1.3** Definiti operatia de inserare a unei linii in tabel.
def insert(row: Row): Table = ???
**1.4** Definiti operatia de stergere a tuturor liniilor exact egale cu cea primita ca parametru.
def delete(row: Row): Table = ???
**1.5** Definiti operatia de sortare a liniilor din tabel dupa o anumita coloana. Functia are un parametru optional ce determina ordinea in care trebuiesc sortate. Mai mult despre parametrii optionali puteti gasi [[https://docs.scala-lang.org/tour/default-parameter-values.html | aici]].
def sort(column: String, ascending: Boolean = true): Table = ???
**1.6** Definiti functia select care primeste o lista de stringuri si returneaza un nou obiect de tip Table ce contine doar coloanele specificate.
def select(columns: List[String]): Table = ???
**1.7** Definiti functia cartesianProduct care primeste un alt tabel si returneaza un nou obiect de tip Table ce contine produsul cartezian al celor doua tabele.
def cartesianProduct(otherTable: Table): Table = ???
**1.8** Definiti functia join, care primeste un alt tabel si o coloana specifica pentru fiecare tabel. Aceasta functie va combina tabelele folosind coloanele indicate, rezultand intr-un nou tabel.
* Cand valorile din coloanele folosite pentru combinatie sunt identice, se va retine o singura valoare din acele coloane.
* Daca valorile difera, ele vor fi unite intr-un singur camp, separandu-le prin semnul ";".
* Se considera ca valorile de tip sir de caractere gol ("") sunt echivalente cu NULL, adica acestea nu vor fi incluse daca exista o valoare specifica intr-o alta tabela.
* In situatiile in care o linie este prezenta in tabelul A dar nu are corespondent in tabelul B, se vor completa campurile corespunzatoare din tabelul B cu sirul vid "".
* Similar, daca o linieeste prezenta in tabelul B dar nu are corespondent in tabelul A, se vor completa campurile corespunzatoare din tabelul A cu sirul vid "".
* Numele coloanei utilizate pentru join in tabelul final va fi preluat din primul tabel.
* Se va intoarce eroare cand unul din tabele nu exista. Daca un tabel este gol, se va intoarce celalalt tabel.
* Liniile din rezultat sunt in ordinea: linii ce au intrări in ambele tabele, linii doar in prima tabela, linii doar in a doua tabela.
**Exemplu:**
**Tabelul A**
^id^name^age^
| 1 | Ana | 20 |
| 2 | Ion | 30 |
| 4 | Maria | |
**Tabelul B**
^id^city^job^age^
| 1 | Cluj | IT | 20 |
| 2 | Iasi | HR | |
| 3 | Buc | MKT | 40 |
| 4 | Buc | | |
**join("A", "id", "B", "id")**
^id^name^age^city^job^
| 1 | Ana | 20 | Cluj | IT |
| 2 | Ion | 30 | Iasi | HR |
| 3 | | 40 | Buc | MKT |
| 4 | Maria| | Buc | |
def join(other: Table)(col1: String, col2: String): Table = ???
**1.9** Definiti functia apply intr-un **companion object** al clasei ''Table''. Functia trebuie sa parseze un sir de caractere si sa returneze un tabel cu numele dat.
def apply(name: String, s: String): Table = ???
==== Filtre peste Tabele ====
::=
&& |
|| |
== |
! |
any [ ] |
all [ ] |
operation [ ]
**2.1.** Vom defini operatia de filtrare a datelor dintr-o tabela sub forma unui TDA. Acest lucru ne permite sa definim operatii de filtrare complexe, compuse din mai multe conditii. Acest TDA are urmtorii constructori:
- Field - reprezinta o conditie de filtrare pe un camp al tabelei. Aceasta conditie este satisfacuta daca valoarea de pe coloana specificata respecta predicatul.
- Compound - reprezinta o conditie de filtrare compusa din mai multe conditii. Aceasta conditie este satisfacuta daca toate conditiile din lista conditions sunt satisfacute.
- Not - reprezinta negarea unei conditii de filtrare.
- And - reprezinta conjunctia a doua conditii de filtrare.
- Or - reprezinta disjunctia a doua conditii de filtrare.
- Equal - reprezinta o conditie de egalitate intre doua conditii de filtrare.
- Any - reprezinta o conditie de filtrare care este satisfacuta daca cel putin una dintre conditiile din lista este satisfacuta.
- All - reprezinta o conditie de filtrare care este satisfacuta daca toate conditiile din lista sunt satisfacute.
trait FilterCond {
def eval(r: Row): Option[Boolean]
}
case class Field(colName: String, predicate: String => Boolean) extends FilterCond {
override def eval(r: Row): Option[Boolean] = ???
}
case class Compound(op: (Boolean, Boolean) => Boolean, conditions: List[FilterCond]) extends FilterCond {
override def eval(r: Row): Option[Boolean] = ???
}
case class Not(f: FilterCond) extends FilterCond {
override def eval(r: Row): Option[Boolean] = ???
}
def And(f1: FilterCond, f2: FilterCond): FilterCond = ???
def Or(f1: FilterCond, f2: FilterCond): FilterCond = ???
def Equal(f1: FilterCond, f2: FilterCond): FilterCond = ???
case class Any(fs: List[FilterCond]) extends FilterCond {
override def eval(r: Row): Option[Boolean] = ???
}
case class All(fs: List[FilterCond]) extends FilterCond {
override def eval(r: Row): Option[Boolean] = ???
}
**2.2.** Pentru a simplifica definirea conditiilor de filtrare, vom defini cateva operatori in cadrul traitului `FilterCond` care sa ne permita sa scriem cod mai concis.
Vom folosi urmatorii operatori ce extind clasa FilterCond:
- == - pentru a verifica egalitatea a doua conditii de filtrare.
- && - pentru a face conjunctia a doua conditii de filtrare.
- || - pentru a face disjunctia a doua conditii de filtrare.
- ! (operator unar) - pentru a nega o conditie de filtrare.
def ==(other: FilterCond) = ???
def &&(other: FilterCond) = ???
def ||(other: FilterCond) = ???
def unary_! = ??
// Puteti sa adaugati mai multi operatori :)
**2.3.** Definiti operatia de filtrare a liniilor din tabel care respecta o anumita conditie.
def filter(f: FilterCond): Table = ???
**2.4.** Definiti operatia de update a unei linii din tabel. Funcția primeste ca input o conditie care dicteaza liniile ce vor fi modificate. Valorile schimbate se găsesc intr-un Map[nume_coloana, valoare_noua]. Trebuiesc modificate TOATE liniile ce respectă condiția.
def update(f: FilterCond, updates: Map[String, String]): Table = ???
==== Op
eratii cu una sau mai multe Tabele =====
O baza de date contine mai multe tabele, pe care putem aplica o serie de operatii:
- insert - creaza o noua tabela cu nume unic si o lista de coloane
- update - actualizeaza informatiile unei tabele deja existente
- delete - sterge o tabela existenta
- selectTables - extrage din lista de tabele existente un subset de tabele
Pentru a gestiona operatii cu una sau mai multe tabele, vom folosi clasa:
case class Database(tables: List[Table]) {
override def toString: String = ???
}
**3.1**. Implementati functia insert, care primeste numele unei tabele si creeaza o noua tabela doar daca numele tabelei nu exista deja in baza de date. Daca numele tabelei exista, functia va intoarce baza de date nemodificata.
def insert(tableName: String): Database = ???
**3.2** Implementati functia update care primeste numele unei tabele si o tabela noua. Functia trebuie sa inlocuiasca tabela veche din baza de date cu noua tabela.
Daca numele tabelei nu exista in baza de date, functia va intoarce baza de date nemodificata.
def update(tableName: String, newTable: Table): Option[Database] = ???
**3.3** Implementati functia delete, care primeste numele unei tabele si sterge tabela respectiva din baza de date.
Daca numele tabelei nu exista in baza de date, functia va intoarce baza de date nemodificata.
def delete(tableName: String): Database = ???
**3.4** Implementati functia selectTables care primeste o lista de nume de tabele si extrage din baza de date doar acele tabele.
Daca unul dintre numele de tabele nu exista in baza de date, functia va intoarce None.
def selectTables(tableNames: List[String]): Database = ???
**3.5** Mai adăugati ceva (cautati voi functia de implementat :-D) la Table astfel incat sa putem accesa Rows din tabel folosind un index.
Faceți același lucru si pentru Database ca sa putem accesa tabelele sale folosind index direct din numele unei instante.
val tabel = new Table("People", List(
Map("id" -> "1", "name" -> "John", "age" -> "23", "CNP" -> "1234567890123"),
Map("id" -> "2", "name" -> "Jane", "age" -> "25", "CNP" -> "1234567890124"),
Map("id" -> "3", "name" -> "Jack", "age" -> "27", "CNP" -> "1234567890125"),
Map("id" -> "4", "name" -> "Jill", "age" -> "29", "CNP" -> "1234567890126"),
))(1) // index aici
val dbPeople = Database(List(tabel))
val tabel2 = dbPeople(0) // index aici
==== Que
ry ====
Testati structurile folosite executand niste query-uri, ce ar putea fi operatii necesare intr-un backend. **Query-urile ar trebuie sa fie one-linere, fara sa folositi ''val'' intermediare, dar puteti defini functii auxiliare (pentru filtre)**
**4.1** Selectati din tabela ''Customers'' toti cei care au peste X ani si locuiesc intr-un oras din lista primita. Returnati raspunsul ordonat dupa ''ID''.
def query_1(db: Database, ageLimit: Int, cities: List[String]): Option[Table] = {
...
}
**4.2** Selectati din tabela ''Orders'' toate comenzile de dupa o data care nu au fost procesate de un angajat anume. Returnati doar ''OrderID'' si ''Cost'', ordonat dupa ''Cost'' descrescator.
def query_2(db: Database, date: String, employeeID: Int): Option[Table] = {
...
}
**4.3** Selectati din tabela ''Orders'' cine a facut comenzii cu ''Cost'' peste o valoare data. Returnati ''OrderID'', ''EmployeeID'' si ''Cost'', ordonat dupa ''EmployeeID'' crescator.
def query_3(db: Database, minCost: Int): Option[Table] = {
...
}
==== Te
stare ====
''Scalatest'' este o biblioteca de testare pentru Scala care suporta mai multe stiluri de scriere a testelor, inclusiv testarea traditionala unitara. ''Scalactic'' este o biblioteca destinata sa faciliteze scrierea de cod mai clar si mai intretinabil in Scala, utilizata in combinatie cu Scalatest pentru a imbunatati claritatea si precizia testelor.
Pentru a rula testele utilizand aceasta configuratie, puteti folosi comanda de mai jos in terminal, de la radacina proiectului. Acest lucru va compila si executa toate testele definite in proiect care depind de Scalatest si Scalactic pentru a verifica corectitudinea codului.
sbt test
===== Submisie arhiva =====
Veti incarca pe moodle o arhiva ce contine, in radacina acesteia, folderul ''src'' al proiectului vostru, fisierul ''build.sbt'' si un fisier text, intitulat ''ID.txt'' ce contine o singura linie, si anume id-ul vostru anonim (pe care il puteti gasi pe moodle la assignment-ul ''tokenID'').
Exemplu structura arhiva:
archive.zip
|-src/
| |-main/
| | |-scala/
| | | | - ...
|-build.sbt
|-ID.txt
==== Punctaje ====
Pentru ca avem cam multe exercitii de implementat, găsiți mai jos un tabel cu punctajele grupate:
^ Parte a temei ^ Functionalitate ^ Punctaj ^
| Table | toString | 3 |
| Table | fromCSV | 3 |
| Table | insert | 3 |
| Table | delete | 3 |
| Table | sort | 3 |
| Table | select | 3 |
| Table | cartesianProduct | 7.5 |
| Table | join | 7.5 |
| Table | apply | 4 |
| Table | filter | 4 |
| Table | update | 4 |
| **TABLE** | **TOTAL** | **45** |
| Filter | Field | 3 |
| Filter | Compound | 3 |
| Filter | Not | 3 |
| Filter | And | 3 |
| Filter | Or | 3 |
| Filter | Equal | 3 |
| Filter | Any | 3 |
| Filter | All | 3 |
| Filter | operator == | 1.5 |
| Filter | operator && | 1.5 |
| Filter | operator `|`| | 1.5 |
| Filter | operator ! | 1.5 |
| **FILTER** | **TOTAL** | **30** |
| Database | insert | 3 |
| Database | update | 3 |
| Database | delete| 3 |
| Database | selectTables | 3 |
| Database | indexing | 3 |
| **DATABASE** | **TOTAL** | **15** |
| Query | query1 | 3 |
| Query | query2 | 3 |
| Query | query3 | 4 |
| **QUERY** | **TOTAL** | **10** |
| ''TEMA 2'' | ''TOTAL'' | ''100'' |