Edit this page Backlinks This page is read only. You can view the source, but not change it. Ask your administrator if you think this is wrong. ===== Tema 1 PP – Casa de marcat ===== Publicare: ''?? martie 2026''\\ Deadline: ''?? aprilie 2026'' Schelet de cod: {{:pp:2025:scala:tema1_pp_2025.zip|}} După ce ați descărcat scheletul, creați un proiect nou din IntelliJ în Scala, în care copiați folderele și fișierul din arhivă în root-ul proiectului (folderul ''src'' va fi suprascris). Dați restart la IDE și ar trebui să funcționeze. <note warning>După deadline fiecare student va prezenta tema la laborator, explicând în detaliu implementarea.</note> <note important>În soluție nu aveți voie să programați cu efecte laterale. Folosiți declarații doar ''val'', nu <del>var</del>.</note> Obiectivul acestei teme este implementarea unui program care simuleaza functionarea unei case de marcat, proces care consta in scanarea unor produse si adaugarea acestora in cosul de cumparaturi. Pentru a implementa acest proiect, avem nevoie de trei module importante: * Partea 1: modulul de decodificare a codului de bare de pe produs * Partea 2: modulul de tabele folosit pentru a stoca informatiile despre produse, cum at fi denumirea, pretul si codul de bare * Partea 3: operatii uzuale de gestionare a cosului de cumparaturi, de exemplu adaugarea deproduse, stergerea sau modificarea cantitatii ===== 1. Coduri de bare ===== Un cod de bare este o reprezentare vizuală a datelor, ușor de citit de dispozitive. De obicei datele descriu proprietăți ale obiectului pe care se află codul de bare. Codurile de bare tradiționale reprezintă datele prin varierea lățimilor liniilor și spațiilor paralele. Această reprezentare este lineară sau 1D. Există și reprezentări 2D, cum ar fi codurile QR, care au o logică de matrice și pot codifica mai multe date. Un cod de bare este un număr reprezentat printr-o succesiune de bare negre (bare) și bare albe (spații). De obicei barele negre codifică biți de **1**, iar barele albe biți de **0**. **Mai mulți biți alăturați pot crea o bară mai groasă**. În imaginea de mai jos puteți vedea codul de bare pentru cifrele 5901234123457, după standardul European Article Number(EAN-13).\\ {{https://upload.wikimedia.org/wikipedia/commons/thumb/a/a2/EAN-13-5901234123457.svg/330px-EAN-13-5901234123457.svg.png}} Codurile de bare descriu 13 cifre, însă doar 12 cifre sunt codificate efectiv deoarece prima cifră este codificată indirect. Fiecare cifra fiind codificată folosind ''4 bare'' de lățimi diferite. Pe lângă barele care codifică cifre, există și o secvență de start de ''3 bare'', una de stop de ''3 bare'' și una de centru de ''5 bare'' (barele acestor secvențe speciale sunt mai lungi). În total sunt ''59 de bare (12 * 4 + 3 + 3 + 5)''.\\ Puteți vedea mai multe în această [[https://en.wikipedia.org/wiki/International_Article_Number#Composition | secțiune]]. Secvența de 59 de bare conține 95 de biți. De la stânga la dreapta aceștia sunt organizați astfel: - 3 biți **101** pentru a marca startul - 42 biți (7 pentru fiecare cifră) pentru cifrele de pe pozițiile 2->7 (prima cifră este codificată indirect) - 5 biți **01010** pentru a marca centrul - 42 biți (7 pentru fiecare cifră) pentru cifrele de pe pozițiile 8->13 - 3 biți **101** pentru a marca sfârșitul === Codificarea cifrelor === Vom considera cifrele numerotate de la stânga la dreapta după poziție, începând de la poziția 1.\\ Cifrele codului de bare sunt împărțite în 3 părți: * Prima cifră, numită cifră de paritate * Primul grup este reprezentat de următoarele 6 cifre, de pe pozițiile 2->7 * Al doilea grup este reprezentat de ultimele 6 cifre, de pe pozițiile 8->13 Spunem despre codificarea unei cifre că e de paritate ''pară'' dacă conține un număr par de biți de 1. Altfel e ''impară''. Pentru fiecare cifră din primul grup (pozițiile 2->7) există două codificări posibile, una cu un număr impar de biți de 1 (codificare **L**, paritate ''impară'') și una cu un numar par de biți de 1 (codificare **G**, paritate ''pară''). Pentru fiecare cifră din al doilea grup (pozițiile 8->13) există o singură codificare posibilă (codificare **R** paritate ''pară'' ). <hidden Tabelul codificarilor L, G si R pentru cifre> ^ Cifra ^ Codificare-L ^ Codificare-G ^ Codificare-R^ | **0** | **0001101** | **0100111** | **1110010**| | **1** | **0011001** | **0110011** | **1100110**| | **2** | **0010011** | **0011011** | **1101100**| | **3** | **0111101** | **0100001** | **1000010**| | **4** | **0100011** | **0011101** | **1011100**| | **5** | **0110001** | **0111001** | **1001110**| | **6** | **0101111** | **0000101** | **1010000**| | **7** | **0111011** | **0010001** | **1000100**| | **8** | **0110111** | **0001001** | **1001000**| | **9** | **0001011** | **0010111** | **1110100**| </hidden> Observații: * Codul de bare începe cu o cifră codificată ''impar'' și se termină cu o cifră codificată ''par'', astfel scannerele pentru codurile de bare pot determina orientarea și să citească și de la stânga la dreapta, dar și invers. * Codificările-R sunt complementele pe biți ale codificărilor-L. * Codificările-G sunt inversele codificărilor-R. * Codificările G și L încep cu **0** și se termină cu **1**. Codificările R încep cu **1** și se termina cu **0**. Astfel, fiecare cifră va fi reprezentată prin 4 bare alternante de grosimi diferite. * Grosimea maximă a unei bare va fi 4. Barele a două cifre nu se amestecă. === Cifra de paritate === După cum am spus anterior, prima cifră se numește cifră de paritate. Cifra de paritate ''nu'' este reprezentată direct printr-o succesiune de bare și spații, ci este codificată indirect, prin alegerea unei combinații de moduri de codificare L sau G pentru primul grup de 6 cifre, conform tabelului de mai jos. Practic, este suficient să știm ce codificare a fost folosită pentru fiecare dintre cele 6 cifre din primul grup pentru a determina cifra de paritate asociată. Dacă combinația găsită nu este asociată cu o cifră din tabelul de mai jos codul este invalid. <hidden Tabelul codificarilor in functie de cifra de paritate> ^ Cifra de paritate ^ Primul grup ^ | **0** | **LLLLLL**| | **1** | **LLGLGG**| | **2** | **LLGGLG**| | **3** | **LLGGGL**| | **4** | **LGLLGG**| | **5** | **LGGLLG**| | **6** | **LGGGLL**| | **7** | **LGLGLG**| | **8** | **LGLGGL**| | **9** | **LGGLGL**| </hidden> === Calculare cifră de control === Ultima cifră a unui cod de bare EAN-13 se numeste cifră de control. Este folosită pentru a confirma citirea corectă a unui cod. Fiecare cifră din codul de bare (fară cea de control), are o pondere de 1 sau 3 în funcție de poziție la calculul cifrei de control (cele de pe poziții impare au pondere 1, iar cele de pe poziții pare au pondere 3, vezi tabelul de mai jos). | Poziție | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | | Pondere | 1 | 3 | 1 | 3 | 1 | 3 | 1 | 3 | 1 | 3 | 1 | 3 | Formula pentru cifra de control este următoarea: $math[C = \left(10 - \left( \sum_{i=1}^{12} w_i \cdot c_i \right) \mod 10 \right) \mod 10] Unde $math[w_i] este ponderea cifrei de pe poziția $math[i] și $math[c_i] este cifra de pe poziția $math[i]. === Exemplu === Să revenim la imaginea pentru codul de bare 5901234123457 din introducere.\\ {{https://upload.wikimedia.org/wikipedia/commons/thumb/a/a2/EAN-13-5901234123457.svg/330px-EAN-13-5901234123457.svg.png}} \\ Reprezentarea binară a acestor bare este următoarea: 10100010110100111011001100100110111101001110101010110011011011001000010101110010011101000100101\\ Prima cifră, adică cifra de paritate, are valoarea 5. Conform tabelului de mai sus, asta va duce la codificarea următoarelor 6 cifre în formatul **LGGLLG**. Ultimele 6 cifre sunt codificate mereu în formatul **RRRRRR**. | Cifra | start | 9| 0 | 1 | 2 | 3 | 4 | centru | 1 | 2 | 3 | 4 | 5 | 7 | end | | Codificare | - | L| G | G | L | L | G | - | R | R | R | R | R | R | - | | Reprezentare | 101 | 0001011| 0100111 | 0110011 | 0010011 | 0111101 | 0011101 | 01010 | 1100110 | 1101100 | 1000010 | 1011100 | 1001110 | 1000100 | 101 | Acum să verificăm dacă cifra de control 7 este corectă. | Poziție | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | | Pondere | 1 | 3 | 1 | 3 | 1 | 3 | 1 | 3 | 1 | 3 | 1 | 3 | | Cifra | 5 | 9 | 0 | 1 | 2 | 3 | 4 | 1 | 2 | 3 | 4| 5 | Suma produselor dintre cifre si ponderile asociate este 83. Cifra de contol este (10 - (83 % 10)) % 10 = 7, deci este corectă. ==== Implementare ==== Avem imagini cu codurile de bare (în format **.ppm**) ale unor produse care trebuie cumpărate. Veți implementa un decodificator simplificat de coduri de bare. Cu ajutorul acestuia magazinele obțin numărul EAN-13 si cu ajutorul bazei de date identifică produsul vândut și calculează prețul.\\ Veți primi codul pentru parsarea si prelucrarea de imagini, care face trecerea de la formatul color **.ppm** la formatul alb-negru **.pbm**, voi fiind nevoiți să implementați doar logica unui decodificator care primește o matrice de pixeli de **0** și de **1**. Această matrice corespunde unei imagini alb-negru, fiecare pixel **negru** având valoarea **1** și fiecare pixel **alb** având valoarea **0**.\\ Vom identifica cifrele bazându-ne pe grosimile relative ale barelor față de o unitate elementară, adică față de o bară care codifică un singur bit. __Veți rezolva TODO-urile din fișierul **Decoder**.__ <color #ed1c24>Nu este nevoie să faceți nimic cu celelalte fișiere, ele v-au fost puse la dispoziție doar în scop didactic.</color> **Notă**: Pentru vizualizarea imaginilor .ppm pe WSL/Linux puteți folosi: <code bash> sudo apt install feh feh image.ppm </code> <hidden Explicarea in detaliu a algoritmului> **1. Transformarea imaginilor** Primim o matrice de biți 0 și 1, reprezentând imaginea în formatul alb-negru descris anterior. Decupăm din matricea de pixeli rândurile din mijloc și rulăm algoritmul de identificare a barelor pe toate aceste rânduri, pentru a avea șanse mai mari de succes. Vrem să găsim barele din imagine, așa că grupăm biții identici și consecutivi în tupluri de tipul **(<număr_repetări>, <bit>)**, unde tuplul descrie grosimea și culoarea barei din imagine. Va rezulta următorul comportament:\\ <nowiki> 0001111111010001100011111 => [(3, 0), (7, 1), (1, 0), (1, 1), (3, 0), (2, 1), (3, 0), (5, 1)] </nowiki> \\ **2. Identificarea unui posibil cod de bare** Căutăm o secvență de 59 de bare pentru fiecare rând din imagine în care identificăm secvențele de start și de stop.\\ Secvențele de 59 de bare care conțin secvențele de start și stop din fiecare rând sunt returnate de funcția ''checkRow'', a cărei implementare este dată de echipă.\\ **3. Identificare cifre** Din secvența de 59 de bare extragem grupuri de 4 bare care reprezintă codificarea unei cifre (după ce am eliminat secvențele de bare care marchează start, centru și sfârșit). Pentru primele 6 cifre găsim codificarea cea mai apropiată L sau G, iar pentru ultimele 6 cifre găsim cea mai apropiată codificare R. Pentru a putea face acest lucru, avem nevoie sa masuram cat de aproape este codificarea obtinuta din poza cu codificarea reala. O cifra e reprezentata prin 4 bare, fiecare bara avand o anumita latime. Se poate ca in realitate, patternul pe care il intalnim sa nu se potriveasca perfect cu niciuna dintre reprezentari. Priviti urmatorul exemplu (codificarea L a cifrei 4: 0100011): <code> ░░░░ = 0 ████ = 1 ░░░░████░░░░░░░░░░░░████████ = Codificare L pentru cifra 4 ░░░░█████░░░░░░░░░░█████████ = Codificare citita de fapt </code> Codificarea pe care am citit-o de fapt difera putin de cea a cifrei 4. Cat de mare este aceasta diferenta va fi masurata de distanta. Va va fi explicata formula ei de calcul in exercitii. Pentru a identifica cifra codificată de un set de 4 bare vom calcula distanța dintre secvența de bare din poză și secvențele de bare pentru codificările L, G sau R. Vom alege codificarea cea mai apropiată (de distanță minimă) pentru fiecare din cele 12 cifre. **4. Calcul lungimi relative** De la formatul descris pentru bare vom lua grupe de câte 4 bare care reprezintă o cifră și vom trece la formatul dimensiunilor relative ale segmentelor, impărțind numărul de aparitii la lungimea totală.\\ <nowiki> [(3, 0), (7, 1), (1, 0), (1, 1)] => [(3/12, 0), (7/12, 1), (1/12, 0), (1/12, 1)] </nowiki>\\ Pentru că biții alternează întotdeauna, vom reduce codificarea la un tuplu format din primul bit și dimensiunile relative ale fiecărei bare:\\ <nowiki> [(3/12, 0), (7/12, 1), (1/12, 0), (1/12, 1)] => (0, (3/12, 7/12, 1/12, 1/12)) </nowiki>\\ Tinand seama de bitul cu care incepe codificarea rezultata **din poza** (in exemplul de mai sus este 0) vom alege codificarea cea mai similara pentru o cifra bazat pe distanta cea mai mica (vezi explicatia de la pasul 3). **5. Cifra de paritate si cifra de control** După ce am identificat ultimele 12 cifre ale codului de bare, vom determina cifra de paritate. Pentru a determina cifra de paritate ne vom uita la paritatea codificărilor identificate pentru cele 6 cifre din primul grup. [[#cifra_de_paritate| vezi mai sus]]. După ce am determinat cifra de paritate, calculăm cifra de control și verificăm dacă este identică cu cea recunoscută din poză. [[#calculare_cifra_de_control| vezi mai sus]]. Dacă cifra de control este corectă, am identificat toate cifrele și întoarcem rezultatul. Altfel întoarcem None. </hidden> <hidden Recapitulare functii de ordin superior> **map()** * Poate fi aplicată pe orice colecție (ex: ''List''). * Primește o funcție ca parametru și aplică funcția respectivă pe fiecare element al colecției. * Întoarce o colecție de același tip (dacă era o colecție de tip ''List'', rămâne tot ''List'' după aplicarea ''map()''). Exemplu: <code scala> val initialList : List[Int] = List(1, 2, 3, 5) // Folosind o funcție def func(x : Int) : Int = x * 2 val listAfterMapWithFunc: List[Int] = initialList.map(func) // List(2, 4, 6, 10) // Folosind lambda val listAfterMapWithLambda: List[Int] = initialList.map(x => x + 7) // List(8, 9, 10, 12) </code> **Notă:** Ultima variantă este echivalenta cu <code scala> initialList.map(_ + 7) </code> **zip()** * Este aplicată pe o colecție, primește ca parametru o altă colecție. * Întoarce o colecție de tupluri cu elementele din cele două colecții. Exemplu: <code scala> val firstList : List[Int] = List(3, 8, 7, 5) val secondList : List[String] = List("verde", "rosu", "albastru", "galben") val result : List[(Int, String)]= firstList.zip(secondList) // List((3,verde), (8,rosu), (7,albastru), (5,galben)) </code> **foldRight() & foldLeft()** * Operația de folding pe o listă în scala înseamnă aplicarea unei operații între un acumulator și fiecare element din listă, rezultatul fiind valoarea acumulatorului după parcurgerea întregii liste. **foldLeft(z: B)(op: (B, A) => B): B** * Primește un acumulator inițial și o funcție, întoarce un acumulator. * Funcția parametru primește un tuplu format dintre acumulator și un element al listei și întoarce un acumulator. * Aplică operația pe listă de la stânga la dreapta. <code scala> val list : List[Int] = List(1, 2, 3) // (((0 - 1) - 2) - 3) val result : Int = list.foldLeft(0)((acc, el) => acc - el) // -6 </code> **Notă:** Poate fi rescris ca <code scala> list.foldLeft(0)(_-_) </code> **foldRight(z: B)(op: (A, B) => B): B** * Primește un acumulator inițial și o funcție, întoarce un acumulator. * Funcția parametru primește un tuplu format dintre un element al listei și acumulator și întoarce un acumulator. * Aplică operația pe listă de la dreapta la stânga. Exemplu: <code scala> val list : List[Int] = List(1, 2, 3) // (1 - (2 - (3 - 0))) val result : Int = list.foldRight(0)((el, acc) => el - acc) // 2 </code> **Notă:** Poate fi rescris ca <code scala> list.foldRight(0)(_-_) </code> </hidden> ==== 1.1. Funcții elementare (20p) ==== 1.1.1. Avem nevoie de un TDA pentru biți. Realizați conversiile de la ''Char'' și ''Int'' la ''Bit''. <code scala> given char2Bit: Conversion[Char, Bit] with def apply(x: Char): Bit = ??? given int2Bit: Conversion[Int, Bit] with def apply(s: Int): Bit = ??? </code> ''Hint:'' Inspectați fișierul **Types**. Mai multe despre conversii implicite [[https://docs.scala-lang.org/tour/implicit-conversions.html|aici]]. 1.1.2. Extindeți tipul Bit cu funcția care întoarce complementul unui bit. <code scala> extension(c:Bit) def complement: Bit = ??? </code> 1.1.3. Pornind de la ''LStrings'' care este lista codificărilor L ca ''String'' (pe fiecare poziție gasim codificarea sa L), definiți listele pentru toate cele 3 tipuri de codificări. **Soluțiile hardcodate nu vor fi luate în considerare, trebuie să vă folosiți de LStrings sau de listele deja create (pentru rightOddList și leftEvenList) împreună cu funcții de ordin superior.** <code scala> val leftOddList: List[List[Bit]] = Nil // codificări L val rightList: List[List[Bit]] = Nil // codificări R val leftEvenList: List[List[Bit]] = Nil // codificări G </code> 1.1.4. Extindeți tipul List cu funcția ''groupedByEquality'' care grupează elementele egale și consecutive dintr-o listă în liste separate. <code scala> extension[A](l: List[A]) def groupedByEquality: List[List[A]] = ??? </code> ''Hint:'' **<nowiki>group([1, 1, 2, 2, 3, 3, 1]) = [[1, 1], [2, 2], [3, 3], [1]]</nowiki>** 1.1.5. Implementați funcția ''runLength'' care grupează elementele egale și consecutive dintr-o listă în elemente noi de tipul **(<număr_apariții>, <element>)**. <code scala> def runLength[A](l: List[A]): List[(Int, A)] = ??? </code> ''Hint:'' Folosiți ''group'' implementat anterior. ==== 1.2. Numere raționale (10p) ==== 1.2.1. Vrem să folosim tipul ''RatioInt'' pentru lucrul cu fracții. Implementați operațiile uzuale cu fracții în clasa ''RatioInt''. <code scala> def -(other: RatioInt): RatioInt = ??? def +(other: RatioInt): RatioInt = ??? def *(other: RatioInt): RatioInt = ??? def /(other: RatioInt): RatioInt = ??? </code> 1.2.2. Implementați funcția de comparare a doua fracții. Întoarce **-1** daca fracția e mai mică decât **other**, **1** dacă e mai mare și **0** dacă sunt egale. <code scala> def compare(other: RatioInt): Int = ??? </code> ==== 1.3. Transformarea inputului (30p) ==== 1.3.1. Implementați funcția care primește o lista de elemente de tipul **(<număr_apariții>, <element>)** și întoarce o listă cu elemente de tipul **(<frecvență relativă>, <element>)**, unde $math[\text{frecvență relativă}=\dfrac{\text{număr apariții}}{\text{număr total de elemente}}] , unde elementele sunt cele din lista inițială pe care am rulat ''runLength''. <code scala> def scaleToOne[A](l: List[(Int, A)]): List[(RatioInt, A)] = ??? </code> 1.3.2. Implementați funcția care primeste o listă de biți și întoarce un tuplu format din primul bit și o listă cu dimensiunile relative ale segmentelor de biți egali. <code scala> def scaledRunLength(l: List[(Int, Bit)]): (Bit, List[RatioInt]) = ??? </code> 1.3.3. Pornind de la un șir de caractere ce reprezintă parități, unde **G** înseamnă par, iar **L** impar, convertiți-l la ''List[Parity]''. <code scala> def toParities(s: Str): List[Parity] = ??? </code> 1.3.4. Creați lista de parițăți pe care o vom folosi pornind de la ''PStrings''. **Soluțiile hardcodate nu vor fi luate în considerare, trebuie să vă folosiți de PStrings** <code scala> val leftParityList: List[List[Parity]] = Nil </code> 1.3.5. Rulați ''scaledRunLength'' pe fiecare element din listele create la task-ul **1.3**. **Soluțiile hardcodate nu vor fi luate în considerare, trebuie să vă folosiți de listele create anterior.** <code scala> type SRL = (Bit, List[RatioInt]) val leftOddSRL: List[SRL] = Nil val leftEvenSRL: List[SRL] = Nil val rightSRL: List[SRL] = Nil </code> ''Hint:'' Folosiți și ''runLength''. ==== 1.4. Identificarea cifrelor (40p) ==== 1.4.1. Definiți funcția de distanță dintre două codificări, care primește tupluri cu primul bit, respectiv lungimile relative ale segmentelor unei codificări (tip definit ca ''SRL'') și întoarce suma modulelor diferențelor dintre rapoartele aflate pe aceeași poziție în ambele codificări. <code scala> def distance(l1: SRL, l2: SRL): RatioInt = ??? </code> ''Hint:'' Dacă codificările sunt alcătuite din bare complementare (■□■□ vs □■□■, unde □ reprezintă o bară de **0**, iar ■ o bară de **1**), puteți întoarce o distanță "infinită" (un număr foarte mare), de exemplu ''RatioInt(100, 1)'' ''Hint:'' Folosiți ''zip'' între listele primite. 1.4.2. Definiți funcția ''bestMatch'' care primește ca parametrii o listă de codificări L, G sau R în forma ''SRL'' și codificarea binară a unei cifre și găsește cea mai bună potrivire din listă. Întoarce un tuplu cu cea mai mică distanță și cifra găsită. <code scala> def bestMatch(SRL_Codes: List[SRL], digitCode: SRL): (RatioInt, Digit) = ??? </code> ''Hint:'' Folosiți ''zip'' și ''min''. 1.4.3. Întoarce paritatea și cea mai bună potrivire pentru o cifră din grupul din stânga (cifrele 2-7). <code scala> def bestLeft(digitCode: SRL): (Parity, Digit) = ??? </code> ''Hint:'' Folosiți-vă de ''leftOddSRL'' și ''leftEvenSRL''. 1.4.4. Întoarce cea mai bună potrivire pentru o cifră din grupul din dreapta (ultimele 6 cifre). Pentru codificările R vom folosi tipul NoParity. <code scala> def bestRight(digitCode: SRL): (Parity, Digit) = ??? </code> ''Hint:'' Folosiți-vă de ''rightSRL''. 1.4.5. Primiți ca argument rezultatul lui ''runLength'' pe o listă de biți. Trebuie să verificați că secvența dată are lungimea de 59 (vedeți în secțiunea [[#Algoritm|Algoritm]] de ce). Delimitați primul grup (stânga) și al doilea grup (dreapta) folosindu-vă de ''chunksOf''. Ignorați verificarea pentru secvențele de start, centru și sfârșit (a fost făcută la checkRow). Pentru fiecare grup de 4 bare găsiți cea mai potrivită cifră. Reuniți rezultatele din cele două liste intr-una singură. Rezultatul va avea 12 cifre. <code scala> def findLast12Digits(rle: List[(Int, Bit)]): List[(Parity, Digit)] = ??? </code> ''Hint:'' Folosiți funcțiile drop și take pe listă pentru a separa grupurile de câte 6 cifre din partea stângă și din partea dreaptă. Va trebui să ignorați primele 3 bare, 5 bare de la mijloc și ultimele 3 bare. 1.4.6. Găsește prima cifră din codul de bare pe baza parităților cifrelor din primul grup (cel din stânga). Pentru prima cifră vom considera paritatea NoParity. <code scala> def firstDigit(l: List[(Parity, Digit)]): Option[Digit] = ??? </code> ''Hint:'' Puteți folosi ''zipWithIndex'' și ''leftParityList''. 1.4.7. Calculează cifra de control (ultima cifră) pe baza primelor 12 cifre dintr-un cod de bare. <code scala> def checkDigit(l: List[Digit]): Digit = ??? </code> ''Hint:'' Găsiți formula [[#Calculare cifră de control|aici]]. 1.4.8. Pentru un cod de bare dat verificați dacă este valid. Funcția primește o listă cu perechi de (Paritate, Cifra) și verifică dacă sunt 13 cifre și dacă cifra de paritate și cifra de control sunt corecte. <code scala> def verifyCode(code: List[(Parity, Digit)]): Option[String] = ??? </code> ''Hint:'' Folosiți ''firstDigit'' și ''checkDigit''. 1.4.9. Definiți funcția de solve care primește rezultatul lui ''runLength'' și întoarce cele 13 cifre reprezentate de codul de bare, dacă potrivirea găsită este un cod valid. <code scala> def solve(rle: List[(Int, Bit)]): Option[String] = ??? </code> ''Hint:'' Folosiți funcțiile ''findLast12Digits'', ''firstDigit'' si ''verifyCode''. ===== 2. Baza de date ===== Vom implementa o mica baze de date pentru un supermarket, sub forma unui tabel. ==== Reprezentarea Tabelelor ==== Considerati exemplul de mai jos. ^ Nume ^ Prenume ^ Varsta ^ | Popescu | Ion | 30 | | Ionescu | Maria | 25 | Acest tabel poate fi reprezentat ca: <code scala> type Row = Map[String, String] // nume_coloana - valoare type Tabular = List[Row] </code> <hidden Funcții utile pentru lucrul cu Map> <code scala> let map = Map(1 -> 2, 3 -> 4): Map[Int, Int] </code> * Adauga o noua pereche cheie-valoare <code scala> 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 </code> * Eliminarea unei perechi cheie-valoare <code scala> map - (3 -> 4) // Map(1 -> 2) </code> * Accesarea valorii asociate unei chei <code scala> 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 </code> * Functii de orin superior <code scala> map mapValues (x => x + 5) // Map(1 -> 7, 2 -> 9) map filterKeys (x => x <= 2) // Map(1 -> 2) </code> * Combinarea a doua map-uri <code scala> 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) </code> </hidden> ==== Clasa Table ==== Vom defini un tabel ca o clasa care are ca atribute numele tabelei ''tableName'' si datele ''tableData''. <code scala> case class Table (tableName: String, tableData: Tabular) { def header: List[String] = ??? def data: Tabular = ??? def name: String = ??? } </code> **2.1.1.** Definiti metoda ''toString'' care returneaza tabelul in forma CSV. <code scala> override def toString: String = ??? </code> **2.1.2** Definiti operatia de inserare a unei linii in tabel. <code scala> def insert(row: Row): Table = ??? </code> **2.1.3** Definiti operatia de stergere a tuturor liniilor exact egale cu cea primita ca parametru. <code scala> def delete(row: Row): Table = ??? </code> **2.1.4.** Definiti operatia de sortare a liniilor din tabel dupa o anumita coloana. <code scala> def sort(column: String): Table = ??? </code> **2.1.5.** Definiti functia select care primeste o lista de stringuri si returneaza un nou obiect de tip Table ce contine doar coloanele specificate. <code scala> def select(columns: List[String]): Table = ??? </code> **2.1.6.** 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. <code scala> def apply(name: String, s: String): Table = ??? </code> ==== Filtre peste Tabele ==== <code> <filter> ::= <filter> && <filter> | <filter> || <filter> | <filter> == <filter> | !<filter> | any [ <filter> ] | all [ <filter> ] | operation [ <filter> ] </code> **2.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. <code scala> 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] = ??? } </code> **2.2.2.** Pentru a simplifica definirea conditiilor de filtrare, vom defini cateva operatori 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. - !! - pentru a nega o conditie de filtrare. <code scala> extension (f: FilterCond) { def ==(other: FilterCond) = ??? def &&(other: FilterCond) = ??? def ||(other: FilterCond) = ??? def unary_! = ?? // Puteti sa adaugati mai multi operatori :) } </code> **2.2.3.** In plus vom abstractiza instantierea unui obiect Field, astfel incat sa putem folosi un tuplu de forma (String, String => Boolean) pentru a crea un obiect Field. <code scala> implicit def tuple2Field(t: (String, String => Boolean)): Field = ??? </code> **2.2.4.** Definiti operatia de filtrare a liniilor din tabel care respecta o anumita conditie. <code scala> def filter(f: FilterCond): Table = ??? </code> **2.2.5.** 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]. <code scala> def update(f: FilterCond, updates: Map[String, String]): Tab le = ??? </code> ==== Query Language ==== Vom dezvolta un limbaj de interogare, care va servi ca API pentru o gama variata de transformari de tabele, anterior implementate sub forma de functii. Acest limbaj de query va permite secvente sau combinatii ale acestor transformari. In cadrul implementarii limbajului de interogare, ne vom concentra pe includerea functionalitatilor asemanatoare cu cele din SQL, precum si pe gestionarea erorilor. Limbajul va permite doar operatii pe un tabel, pentru a simplifica implementarea. Pentru tratarea erorilor, vom utiliza TDA-ul ''Option'', unde ''Some(_)'' indica un rezultat valid al unei operatii, in timp ce ''None'' semnaleaza o eroare. In cazul in care un query genereaza o eroare, aceasta se va propaga daca rezultatul este necesar in executarea unui alt query. **2.3.1.** Vom defini operatiile ce se pot realiza pe o tabela folosind TDA-ul ''PP_SQL_Table''. Funcția ''eval'' trebuie sa apeleze metodele corespunzatoare definite in ''Table''. <code scala> trait PP_SQL_Table{ def eval: Option[Table] } case class InsertRow(table:Table, values: Tabular) extends PP_SQL_Table{ def eval: Option[Table] = ??? } case class UpdateRow(table: Table, condition: FilterCond, updates: Map[String, String]) extends PP_SQL_Table{ def eval: Option[Table] = ??? } case class SortTable(table: Table, column: String) extends PP_SQL_Table{ def eval: Option[Table] = ??? } case class DeleteRow(table: Table, row: Row) extends PP_SQL_Table{ def eval: Option[Table] = ??? } case class FilterRows(table: Table, condition: FilterCond) extends PP_SQL_Table{ def eval: Option[Table] = ??? } case class SelectColumns(table: Table, columns: List[String]) extends PP_SQL_Table{ def eval: Option[Table] = ??? } </code> **Nota:** Am vrea sa avem o sintaxa mai usor de citit pentru aces Query Language. De aceea, vom defini ''implicits'' pentru fiecare din operațiile ''eval'' ale acestor 2 TDA-uri. Forma unui query, fie ca este pe toată baza de date, fie ca este pe o singura tabela are forma unui tuplu de tipul: <code> (tabel, "OPERATIE", ...parametri...) </code> unde operatia este un string: * SelectTables - "SELECT" * InsertRow - "INSERT" * UpdateRow - "UPDATE" * SortTable - "SORT" * DeleteRow - "DELETE" * FilterRows - "FILTER" * SelectColumns - "EXTRACT" **2.3.2.** Implementati functii de conversie implicite intre tuplurile descrise mai sus si query-ul descris de acestea. **Nota:** Erorile ce apar in cardul primului element din tuplu vor fi propagate la rezultat, pentru a putea ulterior combina query-uri. <code scala> implicit def PP_SQL_Table_Insert(t: (Option[Table], String, Tabular)): Option[PP_SQL_Table] = ??? implicit def PP_SQL_Table_Sort(t: (Option[Table], String, String)): Option[PP_SQL_Table] = ??? implicit def PP_SQL_Table_Update(t: (Option[Table], String, FilterCond, Map[String, String])): Option[PP_SQL_Table] = ??? implicit def PP_SQL_Table_Delete(t: (Option[Table], String, Row)): Option[PP_SQL_Table] = ??? implicit def PP_SQL_Table_Filter(t: (Option[Table], String, FilterCond)): Option[PP_SQL_Table] = ??? implicit def PP_SQL_Table_Select(t: (Option[Table], String, List[String])): Option[PP_SQL_Tab le] = ??? </code> ===== 3. Casa de Marcat ===== Vrem sa simulam operatii elementare pe care le-am folosi la o casa de marcat de tip self checkout. Va veti folosi de **productsTable**, un tabel intitulat "Products" ce contine coloeanele "Barcode", "Name" si "Price". Pentru urmatoarele cerinte trebuie sa va folositi de implementarile de la 2.3.2. Va oferim un exemplu de utilizare al **queryT**, in care selectam(cu FILTER) acele randuri care pe coloana "Name" contin cuvantul "lapte": <code scala> queryT(Some(productsTable), "FILTER", Field("Name", _.contains("lapte"))) </code> **3.1.1** Definiti operatia de initializare a unui nou cos de cumparaturi (tabela goala). <code scala> def START_SHOPPING() : Table = ??? </code> **3.1.2** Stiind codul de bare a unui produs si cantitatea dorita, adaugati-l la lista de cumparaturi. Nu uitati sa va folositi de *productsTable* (datele despre produsele din tot magazinul). <code scala> def ADD_PRODUCT(shopList: Table, barcode: String, quantity: Int): Table = ??? </code> **3.1.3** Definiti operatia de stergere a unui produs din lista de cumparaturi. <code scala> def DELETE_PRODUCT(t: Table, name: String): Table = ??? </code> **3.1.4** Definiti operatia de editare a cantitatii unui produs din lista de cumparaturi. <code scala> def EDIT_QUANTITY(t: Table, name: String, newQuantity: Int): Table = ??? </code> ===== Testare ===== În folderul src/test din proiect se află Test.scala, care poate fi rulat din IntelliJ apăsând pe butonul de Run din partea stângă a codului. Fiecare test are un buton propriu și poate fi rulat separat. Există teste pentru cele mai importante funcții, vă recomandăm să testați funcțiile de bază înainte de a începe implementarea task-urilor avansate. La rularea testelor veți primi și un punctaj, după specificațiile din enunț. Dacă folosiți terminalul: <code>sbt test</code> ===== Submisie arhiva ===== <note important> Veti incarca pe moodle o arhivă ce conține, **in rădăcina acesteia**, folderul ''src'' al proiectului vostru, fișierul ''build.sbt'' și un fișier text, intitulat ''ID.txt'' ce conține o singură linie, și anume id-ul vostru anonim (pe care il puteti găsi pe moodle la assignment-ul ''tokenID''). Arhiva trebuie să fie **obligatoriu .zip**. </note> Exemplu structura arhivă: <code> archive.zip |-src/ | |-main/ | | |-scala/ | | | | - ... <fisierele cu sursa scala> |-build.sbt |-ID.txt </code> ===== Cum rulez pe propriile exemple? ===== Găsiți fișierul ''MyBarcodes.scala'' care poate fi rulat din IDE. Funcția ''readBarcodes'' primește numele unui folder de input și a unui folder de output. În cel de input puteți pune propriile imagini cu coduri de bare în format **.ppm**. Decupați doar codul de bare din imagini și aveți grijă ca acestea să fie suficient de clare, altfel algoritmul nu va funcționa. Puteți converti imagini în formatul **.ppm** pe https://convertio.co/. În folderul de output puteți vedea aceleași imagini **.ppm**, dar în format alb-negru (adică **.pbm**). ===== Aplicatie a codurilor de bare ===== Puteti verifica ce contine mancarea folosind site-ul https://world.openfoodfacts.org/. Codul de bare pentru fiecare produs este unic la nivel global. Have fun!