From 0837f447fc922c3e9a5db47c7bd4930a2f599902 Mon Sep 17 00:00:00 2001 From: Marco Pedone Date: Thu, 21 Aug 2025 13:53:55 +0200 Subject: [PATCH] Refactor parser functionality: move ParseDettaglio and related tests to new parser package; remove unused utils functions --- apps/backend/from_bkp.go | 24 +- apps/backend/miogest_handler.go | 204 ++------------ apps/backend/miogest_test.go | 208 -------------- apps/backend/parser/parser.go | 187 +++++++++++++ apps/backend/parser/parser_test.go | 418 +++++++++++++++++++++++++++++ apps/backend/utils.go | 122 --------- 6 files changed, 638 insertions(+), 525 deletions(-) delete mode 100644 apps/backend/miogest_test.go create mode 100644 apps/backend/parser/parser.go create mode 100644 apps/backend/parser/parser_test.go delete mode 100644 apps/backend/utils.go diff --git a/apps/backend/from_bkp.go b/apps/backend/from_bkp.go index e1fa8d6..ac90747 100644 --- a/apps/backend/from_bkp.go +++ b/apps/backend/from_bkp.go @@ -1,7 +1,9 @@ package main import ( + "backend/parser" "backend/typedefs" + "backend/utils" "encoding/xml" "fmt" "io" @@ -64,7 +66,7 @@ func extractData_bkp(annuncio typedefs.AnnuncioBKP, extractedData chan typedefs. var tmp typedefs.AnnuncioParsed = typedefs.AnnuncioParsed{} - codice := RemoveWhitespace(GetStringValue(annuncio.Codice)) + codice := utils.RemoveWhitespace(utils.GetStringValue(annuncio.Codice)) tmp.Codice = codice tmp.Email = nil if annuncio.Proprietario != nil && annuncio.Proprietario.Cliente != nil && annuncio.Proprietario.Cliente.Nome != nil { @@ -75,16 +77,16 @@ func extractData_bkp(annuncio typedefs.AnnuncioBKP, extractedData chan typedefs. tmp.Idlocatore = annuncio.Proprietario.Cliente.ID } - upperIndirizzo := MakeUppercase(GetStringValue(annuncio.Indirizzo)) + upperIndirizzo := utils.MakeUppercase(utils.GetStringValue(annuncio.Indirizzo)) tmp.Indirizzo = &upperIndirizzo tmp.Civico = annuncio.Civico - upperComune := MakeUppercase(GetStringValue(annuncio.Comune)) + upperComune := utils.MakeUppercase(utils.GetStringValue(annuncio.Comune)) tmp.Comune = &upperComune tmp.Cap = annuncio.Cap tmp.Provincia = annuncio.Provincia tmp.Regione = annuncio.Regione - tmp.Lat = CommaToDot(annuncio.Lat) - tmp.Lon = CommaToDot(annuncio.Lon) + tmp.Lat = utils.CommaToDot(annuncio.Lat) + tmp.Lon = utils.CommaToDot(annuncio.Lon) tmp.Indirizzo_secondario = nil tmp.Civico_secondario = nil tmp.Lat_secondario = nil @@ -93,12 +95,12 @@ func extractData_bkp(annuncio typedefs.AnnuncioBKP, extractedData chan typedefs. var tipo = tipologiaParser(*annuncio.Tipo) tmp.Tipo = &tipo - tmpanno := GetStringValue(annuncio.Anno) + tmpanno := utils.GetStringValue(annuncio.Anno) if tmpanno != "0" && tmpanno != "" { tmp.Anno = &tmpanno } - tmp.Prezzo = prezzoParse(annuncio.Prezzo) - tmp.Consegna = parseConsegna(annuncio.Consegna) + tmp.Prezzo = parser.ParsePrezzo(annuncio.Prezzo) + tmp.Consegna = parser.ParseConsegna(annuncio.Consegna) tmp.Classe = annuncio.Classe if annuncio.Categorie != nil && annuncio.Categorie.Categoria != nil { @@ -117,7 +119,7 @@ func extractData_bkp(annuncio typedefs.AnnuncioBKP, extractedData chan typedefs. *tmp.Categorie = resultArray } - tmp.Mq = CommaToDot(annuncio.Mq) + tmp.Mq = utils.CommaToDot(annuncio.Mq) tmp.Piano = annuncio.Piano tmp.Piano_palazzo = annuncio.PianoPalazzo tmp.Unita_condominio = annuncio.UnitaImmobiliari @@ -128,7 +130,7 @@ func extractData_bkp(annuncio typedefs.AnnuncioBKP, extractedData chan typedefs. tmp.Numero_terrazzi = annuncio.NumeroTerrazzi tmp.Numero_box = annuncio.NumeroBox tmp.Numero_postiauto = annuncio.NumeroPostiauto - tmpstato := GetStringValue(annuncio.Stato) + tmpstato := utils.GetStringValue(annuncio.Stato) var tmpstatoString string if tmpstato == "1" { tmpstatoString = "Attivo" @@ -240,7 +242,7 @@ func TestTipologieBkp() { var tipologie []string for _, r := range bkp.Annunci.Record { if r.Tipo != nil { - if !contains(tipologie, *r.Tipo) { + if !utils.Contains(tipologie, *r.Tipo) { tipologie = append(tipologie, *r.Tipo) } } diff --git a/apps/backend/miogest_handler.go b/apps/backend/miogest_handler.go index 188f81e..92e0931 100644 --- a/apps/backend/miogest_handler.go +++ b/apps/backend/miogest_handler.go @@ -1,7 +1,9 @@ package main import ( + "backend/parser" "backend/typedefs" + "backend/utils" "fmt" "io" "log" @@ -11,7 +13,6 @@ import ( "reflect" "regexp" "slices" - "strconv" "strings" "sync" "time" @@ -46,7 +47,7 @@ func (m *MiogestHandler) GetAnnunci() typedefs.AnnunciXML { } func (m *MiogestHandler) GetAnnunciFromMiogest() { - m.AnnunciXML = GetDataXML[typedefs.AnnunciXML]("http://partner.miogest.com/agenzie/infoalloggi.xml") + m.AnnunciXML = utils.GetDataXML[typedefs.AnnunciXML]("http://partner.miogest.com/agenzie/infoalloggi.xml") m.annunci_cache_expires = time.Now().Add(1 * time.Hour) } @@ -71,7 +72,7 @@ func (m *MiogestHandler) GetCategorie() []typedefs.Categoria { } func (m *MiogestHandler) GenerateCaratteristiche() { - var result = GetDataXML[typedefs.DefaultCaratteristiche]("https://www.miogest.com/apps/revo.aspx?tipo=schede") + var result = utils.GetDataXML[typedefs.DefaultCaratteristiche]("https://www.miogest.com/apps/revo.aspx?tipo=schede") var array typedefs.ListaCaratteristiche for _, tag := range result.Scheda { var c = typedefs.Caratteristica{Id: tag.Id, Tagxml: tag.Tagxml} @@ -82,7 +83,7 @@ func (m *MiogestHandler) GenerateCaratteristiche() { } func (m *MiogestHandler) GenerateCategorie() { - var result = GetDataXML[typedefs.DefaultCategories]("https://www.miogest.com/apps/revo.aspx?tipo=categorie") + var result = utils.GetDataXML[typedefs.DefaultCategories]("https://www.miogest.com/apps/revo.aspx?tipo=categorie") var listaCategorie []typedefs.Categoria for _, elem := range result.Cat { var c = typedefs.Categoria{Id: elem.ID, Nome: elem.Nome} @@ -122,12 +123,12 @@ func (m *MiogestHandler) ParseAnnunci() { lista_codici := []string{} for i := range annunci.Annuncio { - lista_codici = append(lista_codici, RemoveWhitespace(*annunci.Annuncio[i].Codice)) + lista_codici = append(lista_codici, utils.RemoveWhitespace(*annunci.Annuncio[i].Codice)) sem <- struct{}{} go func(i int) { defer wg.Done() defer func() { <-sem }() - result := extractData_update(&annunci.Annuncio[i], contains(cod_image_to_update, *annunci.Annuncio[i].Codice)) + result := extractData_update(&annunci.Annuncio[i], utils.Contains(cod_image_to_update, *annunci.Annuncio[i].Codice)) AnnunciArray.Annuncio = append(AnnunciArray.Annuncio, result) }(i) } @@ -155,11 +156,12 @@ func strUpd(s string) *string { func extractData_update(annuncio *typedefs.AnnuncioXML, updateImages bool) typedefs.AnnuncioParsed { var tmp typedefs.AnnuncioParsed = typedefs.AnnuncioParsed{} - tmp.Codice = RemoveWhitespace(GetStringValue(annuncio.Codice)) + tmp.Codice = utils.RemoveWhitespace(utils.GetStringValue(annuncio.Codice)) processLocatore(&tmp, annuncio) processIndirizzo(&tmp, annuncio) - processTipologia(&tmp, annuncio) - tmp.Consegna = parseConsegna(annuncio.Consegna) + + tmp.Tipo = parser.ParseTipologia(annuncio.Tipologia, annuncio.Tipologia2) + tmp.Consegna = parser.ParseConsegna(annuncio.Consegna) processCategorie(&tmp, annuncio) processDatiImmobile(&tmp, annuncio) tmp.Stato = annuncio.Stato @@ -224,7 +226,7 @@ func extractData_update(annuncio *typedefs.AnnuncioXML, updateImages bool) typed if updateImages { processImages(&tmp, annuncio) } - folders := GetPathDirsArray(filepath.Join(imgbasepath, tmp.Codice)) + folders := utils.GetPathDirsArray(filepath.Join(imgbasepath, tmp.Codice)) for _, folder := range folders { folderpath := filepath.Join(filepath.Join(imgbasepath, tmp.Codice), folder) files, err := os.ReadDir(folderpath) @@ -263,51 +265,26 @@ func processLocatore(tmp *typedefs.AnnuncioParsed, annuncio *typedefs.AnnuncioXM } tmp.Numero = &number if annuncio.Clienti.Cliente.Cognome != nil && annuncio.Clienti.Cliente.Nome != nil { - tmp.Locatore = strUpd(fmt.Sprintf("%s %s", MakeUppercase(*annuncio.Clienti.Cliente.Cognome), MakeUppercase(*annuncio.Clienti.Cliente.Nome))) + tmp.Locatore = strUpd(fmt.Sprintf("%s %s", utils.MakeUppercase(*annuncio.Clienti.Cliente.Cognome), utils.MakeUppercase(*annuncio.Clienti.Cliente.Nome))) } tmp.Idlocatore = annuncio.Clienti.Cliente.Id } func processIndirizzo(tmp *typedefs.AnnuncioParsed, annuncio *typedefs.AnnuncioXML) { - tmp.Indirizzo = strUpd(MakeUppercase(GetStringValue(annuncio.Indirizzo))) + tmp.Indirizzo = strUpd(utils.MakeUppercase(utils.GetStringValue(annuncio.Indirizzo))) tmp.Civico = annuncio.Civico - tmp.Comune = strUpd(MakeUppercase(GetStringValue(annuncio.Comune))) + tmp.Comune = strUpd(utils.MakeUppercase(utils.GetStringValue(annuncio.Comune))) tmp.Cap = annuncio.Cap tmp.Provincia = annuncio.Provincia tmp.Regione = annuncio.Regione tmp.Lat = annuncio.Latitudine tmp.Lon = annuncio.Longitudine - tmp.Indirizzo_secondario = strUpd(MakeUppercase(GetStringValue(annuncio.IndirizzoSecondario))) + tmp.Indirizzo_secondario = strUpd(utils.MakeUppercase(utils.GetStringValue(annuncio.IndirizzoSecondario))) tmp.Civico_secondario = annuncio.CivicoSecondario tmp.Lat_secondario = annuncio.LatitudineSecondario tmp.Lon_secondario = annuncio.LongitudineSecondario } -func processTipologia(tmp *typedefs.AnnuncioParsed, annuncio *typedefs.AnnuncioXML) { - if annuncio.Tipologia == nil { - tmp.Tipo = strUpd("Errore") - return - } - if *annuncio.Tipologia == "V" { - tmp.Tipo = strUpd("Vendita") - return - } - if *annuncio.Tipologia == "I" { - tmp.Tipo = strUpd("Cessione") - return - } - if *annuncio.Tipologia == "A" { - if *annuncio.Tipologia2 == "Residenziale" || *annuncio.Tipologia2 == "Turistico" || *annuncio.Tipologia2 == "Stanza" { - tmp.Tipo = strUpd("Transitorio") - } else { - tmp.Tipo = strUpd("Stabile") - } - return - } - tmp.Tipo = strUpd("Errore") - -} - func processCategorie(tmp *typedefs.AnnuncioParsed, annuncio *typedefs.AnnuncioXML) { if annuncio.Categoria == nil { return @@ -328,27 +305,13 @@ func processCategorie(tmp *typedefs.AnnuncioParsed, annuncio *typedefs.AnnuncioX *tmp.Categorie = resultArray } -func prezzoParse(prezzo *string) int { - zero := 0 - if prezzo == nil { - return zero - } - f, err := strconv.ParseFloat(strings.TrimSpace(strings.ReplaceAll(*prezzo, ",", ".")), 64) - if err != nil { - log.Printf("Failed to parse price: %v", err) - return zero - } - prezzoInt := int(f * 100) // Convert to cents - return prezzoInt -} - func processDatiImmobile(tmp *typedefs.AnnuncioParsed, annuncio *typedefs.AnnuncioXML) { - tmpanno := GetStringValue(annuncio.Anno) + tmpanno := utils.GetStringValue(annuncio.Anno) if tmpanno != "0" && tmpanno != "" { tmp.Anno = &tmpanno } - tmp.Prezzo = prezzoParse(annuncio.Prezzo) + tmp.Prezzo = parser.ParsePrezzo(annuncio.Prezzo) tmp.Classe = annuncio.Classe if annuncio.Mq != nil { replaced := strings.ReplaceAll(*annuncio.Mq, ",", "") @@ -420,7 +383,7 @@ func downloadImage(url, filename string) { } func processWebpImages(path string) { - array, err := GetPathsArray(path) + array, err := utils.GetPathsArray(path) if err != nil { log.Fatal(err) } @@ -442,7 +405,7 @@ func processWebpImages(path string) { } func folderCleanup(codice string) error { - folders := GetPathDirsArray(filepath.Join(imgbasepath, codice)) + folders := utils.GetPathDirsArray(filepath.Join(imgbasepath, codice)) non_webp := []string{} for _, folder := range folders { folderpath := filepath.Join(imgbasepath, codice, folder) @@ -490,130 +453,3 @@ func parseCaratteristiche(wrkrecord *typedefs.AnnuncioXML) map[string]interface{ } return tmpCaratteristiche } - -var mesiMapping = map[int]string{ - 1: "Gennaio", - 2: "Febbraio", - 3: "Marzo", - 4: "Aprile", - 5: "Maggio", - 6: "Giugno", - 7: "Luglio", - 8: "Agosto", - 9: "Settembre", - 10: "Ottobre", - 11: "Novembre", - 12: "Dicembre", -} - -func parseConsegna(original *string) *int { - currentMonth := int(time.Now().Month()) - if original == nil { - return nil - } - if *original == "Libero" { - return ¤tMonth - } else { - for k, v := range mesiMapping { - if *original == v { - return &k - } - } - } - return ¤tMonth -} - -func ParseDettaglio(dettaglio string) (disp_time *string, perm_arr *[]int, persone_arr *[]string, err error) { - - dettaglio = strings.TrimSpace(dettaglio) - if dettaglio == "" { - return - } - - disp := getKeyValueUpToXorWhitespace(dettaglio, "disp:", "perm:") - - perm := getKeyValueUpToXorWhitespace(dettaglio, "perm:", "persone:") - - persone := getKeyValueUpToXorWhitespace(dettaglio, "persone:", "") - - if disp != "" { - // Validate date format - _, err := time.Parse("02/01", disp) - if err != nil { - disp_time = nil - } else { - p := fmt.Sprintf("%s/%d", disp, time.Now().Year()) - disp_time = &p - } - } - if perm != "" { - parts := strings.Split(perm, "-") - for _, part := range parts { - part = strings.TrimSpace(part) - if part != "" { - num, err := strconv.Atoi(part) - if err == nil { - if perm_arr == nil { - perm_arr = &[]int{} - } - *perm_arr = append(*perm_arr, num) - } - } - } - } - if persone != "" { - parts := strings.Split(persone, "/") - for _, part := range parts { - part = strings.TrimSpace(part) - if part != "" { - if persone_arr == nil { - persone_arr = &[]string{} - } - *persone_arr = append(*persone_arr, part) - } - } - } - return - -} - -func getKeyValueUpToXorWhitespace(input, key, stopstring string) string { - input = strings.TrimSpace(input) - i := strings.Index(input, key) - if i == -1 { - return "" - } - start := i + len(key) - rest := strings.TrimSpace(input[start:]) - - // Find stopstring, whitespace, or repeated key - stop_pos := len(rest) - stops := []int{} - - if stopstring != "" { - if j := strings.Index(rest, stopstring); j != -1 { - stops = append(stops, j) - } - } - // Find whitespace - if ws := strings.IndexAny(rest, " \n\r\t"); ws != -1 { - stops = append(stops, ws) - } - // Find repeated key - if k := strings.Index(rest, key); k != -1 { - stops = append(stops, k) - } - // Use the earliest stop - for _, s := range stops { - if s < stop_pos { - stop_pos = s - } - } - - val := strings.TrimSpace(rest[:stop_pos]) - // Only take the first token before any whitespace or another key - if idx := strings.IndexAny(val, " \n\r\t"); idx != -1 { - val = val[:idx] - } - return val -} diff --git a/apps/backend/miogest_test.go b/apps/backend/miogest_test.go deleted file mode 100644 index b0fcaee..0000000 --- a/apps/backend/miogest_test.go +++ /dev/null @@ -1,208 +0,0 @@ -package main - -import ( - "fmt" - "testing" - "time" -) - -func ptr[T any](v T) *T { return &v } - -func TestParseDettaglio(t *testing.T) { - tests := []struct { - input string - wantDisp *string - wantPerm *[]int - wantPers *[]string - }{ - { - input: "disp:01/10 perm:3-6 persone:single", - wantDisp: ptr(fmt.Sprintf("01/10/%d", time.Now().Year())), - wantPerm: ptr([]int{3, 6}), - wantPers: ptr([]string{"single"}), - }, - { - input: "disp: perm: persone:", - wantDisp: nil, - wantPerm: nil, - wantPers: nil, - }, - { - input: "disp: perm:3-6 persone:single", // disp missing - wantDisp: nil, - wantPerm: ptr([]int{3, 6}), - wantPers: ptr([]string{"single"}), - }, - { - input: "disp:01/10 perm: persone:single", // perm missing - wantDisp: ptr(fmt.Sprintf("01/10/%d", time.Now().Year())), - wantPerm: nil, - wantPers: ptr([]string{"single"}), - }, - { - input: "disp:01/10 perm:3-6 persone:", // persone missing - wantDisp: ptr(fmt.Sprintf("01/10/%d", time.Now().Year())), - wantPerm: ptr([]int{3, 6}), - wantPers: nil, - }, - { - input: "disp: perm: persone:", // all fields missing - wantDisp: nil, - wantPerm: nil, - wantPers: nil, - }, - { - input: "disp:bad_date perm:3-6 persone:single", // invalid date - wantDisp: nil, - wantPerm: ptr([]int{3, 6}), - wantPers: ptr([]string{"single"}), - }, - { - input: "disp:01/10 perm:abc-6 persone:single", // non-numeric perm - wantDisp: ptr(fmt.Sprintf("01/10/%d", time.Now().Year())), - wantPerm: ptr([]int{6}), - wantPers: ptr([]string{"single"}), - }, - { - input: "disp:01/10 perm:3- persone:single", // trailing delimiter in perm - wantDisp: ptr(fmt.Sprintf("01/10/%d", time.Now().Year())), - wantPerm: ptr([]int{3}), - wantPers: ptr([]string{"single"}), - }, - { - input: "disp:01/10 perm:-6 persone:single", // leading delimiter in perm - wantDisp: ptr(fmt.Sprintf("01/10/%d", time.Now().Year())), - wantPerm: ptr([]int{6}), - wantPers: ptr([]string{"single"}), - }, - { - input: "disp:01/10 perm:3-6 persone:single/", // trailing delimiter in persone - wantDisp: ptr(fmt.Sprintf("01/10/%d", time.Now().Year())), - wantPerm: ptr([]int{3, 6}), - wantPers: ptr([]string{"single"}), - }, - { - input: "disp:01/10 perm:3-6 persone:/single", // leading delimiter in persone - wantDisp: ptr(fmt.Sprintf("01/10/%d", time.Now().Year())), - wantPerm: ptr([]int{3, 6}), - wantPers: ptr([]string{"single"}), - }, - { - input: "disp:01/10 perm:3-6 persone:single//coppia", // double delimiter in persone - wantDisp: ptr(fmt.Sprintf("01/10/%d", time.Now().Year())), - wantPerm: ptr([]int{3, 6}), - wantPers: ptr([]string{"single", "coppia"}), - }, - { - input: "perm:3-6 persone:single", // disp missing - wantDisp: nil, - wantPerm: ptr([]int{3, 6}), - wantPers: ptr([]string{"single"}), - }, - { - input: "disp:01/10 perm:3-6", // persone missing - wantDisp: ptr(fmt.Sprintf("01/10/%d", time.Now().Year())), - wantPerm: ptr([]int{3, 6}), - wantPers: nil, - }, - { - input: "disp:01/10 perm: persone:", // perm empty - wantDisp: ptr(fmt.Sprintf("01/10/%d", time.Now().Year())), - wantPerm: nil, - wantPers: nil, - }, - { - input: "", // completely empty string - wantDisp: nil, - wantPerm: nil, - wantPers: nil, - }, - { - input: "disp: 01/10 perm: 3-6 persone: single ", - wantDisp: ptr(fmt.Sprintf("01/10/%d", time.Now().Year())), - wantPerm: ptr([]int{3, 6}), - wantPers: ptr([]string{"single"}), - }, - { - input: "persone:single perm:3-6 disp:01/10", - wantDisp: ptr(fmt.Sprintf("01/10/%d", time.Now().Year())), - wantPerm: ptr([]int{3, 6}), - wantPers: ptr([]string{"single"}), - }, - { - input: "disp:01/10 perm:3--6 persone:single", - wantDisp: ptr(fmt.Sprintf("01/10/%d", time.Now().Year())), - wantPerm: ptr([]int{3, 6}), - wantPers: ptr([]string{"single"}), - }, - { - input: "disp:01/10\tperm:3-6\tpersone:single", - wantDisp: ptr(fmt.Sprintf("01/10/%d", time.Now().Year())), - wantPerm: ptr([]int{3, 6}), - wantPers: ptr([]string{"single"}), - }, - { - input: "disp:01/10 perm:3-6 persone:single/coppia!", - wantDisp: ptr(fmt.Sprintf("01/10/%d", time.Now().Year())), - wantPerm: ptr([]int{3, 6}), - wantPers: ptr([]string{"single", "coppia!"}), - }, - { - input: "disp:01/10 disp:02/11 perm:3-6 persone:single", - wantDisp: ptr(fmt.Sprintf("01/10/%d", time.Now().Year())), // Only first disp: is parsed - wantPerm: ptr([]int{3, 6}), - wantPers: ptr([]string{"single"}), - }, - { - input: "disp:01/10perm:3-6persone:single", - wantDisp: ptr(fmt.Sprintf("01/10/%d", time.Now().Year())), - wantPerm: ptr([]int{3, 6}), - wantPers: ptr([]string{"single"}), - }, - } - - for _, tt := range tests { - disp_time, perm_arr, persone_arr, _ := ParseDettaglio(tt.input) - - //Handle *string comparison, including nil - if tt.wantDisp != nil { - if disp_time == nil { - t.Errorf("disp_time: got nil, want %v, input: %v", *tt.wantDisp, tt.input) - } else if *disp_time != *tt.wantDisp { - t.Errorf("disp_time: got %v, want %v, input: %v", *disp_time, *tt.wantDisp, tt.input) - } - } else if disp_time != nil { - t.Errorf("disp_time: got %v, want nil, input: %v", *disp_time, tt.input) - } - - // Handle *[]int comparison, including nil - if tt.wantPerm != nil { - if perm_arr == nil || len(*perm_arr) != len(*tt.wantPerm) { - t.Errorf("perm_arr: got %v, want %v, input: %v", perm_arr, *tt.wantPerm, tt.input) - } else { - for i := range *perm_arr { - if (*perm_arr)[i] != (*tt.wantPerm)[i] { - t.Errorf("perm_arr[%d]: got %v, want %v, input: %v", i, (*perm_arr)[i], (*tt.wantPerm)[i], tt.input) - } - } - } - } else if perm_arr != nil && len(*perm_arr) != 0 { - t.Errorf("perm_arr: got %v, want nil, input: %v", perm_arr, tt.input) - } - - // Handle *[]string comparison, including nil - if tt.wantPers != nil { - if persone_arr == nil || len(*persone_arr) != len(*tt.wantPers) { - t.Errorf("persone_arr: got %v, want %v, input: %v", persone_arr, *tt.wantPers, tt.input) - } else { - for i := range *persone_arr { - if (*persone_arr)[i] != (*tt.wantPers)[i] { - t.Errorf("persone_arr[%d]: got %v, want %v, input: %v", i, (*persone_arr)[i], (*tt.wantPers)[i], tt.input) - } - } - } - } else if persone_arr != nil && len(*persone_arr) != 0 { - t.Errorf("persone_arr: got %v, want nil, input: %v", persone_arr, tt.input) - } - } -} diff --git a/apps/backend/parser/parser.go b/apps/backend/parser/parser.go new file mode 100644 index 0000000..b5a80a0 --- /dev/null +++ b/apps/backend/parser/parser.go @@ -0,0 +1,187 @@ +package parser + +import ( + "backend/utils" + "fmt" + "regexp" + "strconv" + "strings" + "time" +) + +func ParseDettaglio(dettaglio string) (disp_time *string, perm_arr *[]int, persone_arr *[]string, err error) { + + dettaglio = strings.TrimSpace(dettaglio) + if dettaglio == "" { + return + } + + disp := getKeyValueUpToXorWhitespace(dettaglio, "disp:", "perm:") + + perm := getKeyValueUpToXorWhitespace(dettaglio, "perm:", "persone:") + + persone := getKeyValueUpToXorWhitespace(dettaglio, "persone:", "") + + if disp != "" { + // Validate date format + _, err := time.Parse("02/01", disp) + if err != nil { + disp_time = nil + } else { + p := fmt.Sprintf("%s/%d", disp, time.Now().Year()) + disp_time = &p + } + } + if perm != "" { + parts := strings.Split(perm, "-") + for _, part := range parts { + part = strings.TrimSpace(part) + if part != "" { + num, err := strconv.Atoi(part) + if err == nil { + if perm_arr == nil { + perm_arr = &[]int{} + } + *perm_arr = append(*perm_arr, num) + } + } + } + } + if persone != "" { + parts := strings.Split(persone, "/") + for _, part := range parts { + part = strings.TrimSpace(part) + if part != "" { + if persone_arr == nil { + persone_arr = &[]string{} + } + *persone_arr = append(*persone_arr, part) + } + } + } + return + +} + +func getKeyValueUpToXorWhitespace(input, key, stopstring string) string { + input = strings.TrimSpace(input) + i := strings.Index(input, key) + if i == -1 { + return "" + } + start := i + len(key) + rest := strings.TrimSpace(input[start:]) + + // Find stopstring, whitespace, or repeated key + stop_pos := len(rest) + stops := []int{} + + if stopstring != "" { + if j := strings.Index(rest, stopstring); j != -1 { + stops = append(stops, j) + } + } + // Find whitespace + if ws := strings.IndexAny(rest, " \n\r\t"); ws != -1 { + stops = append(stops, ws) + } + // Find repeated key + if k := strings.Index(rest, key); k != -1 { + stops = append(stops, k) + } + // Use the earliest stop + for _, s := range stops { + if s < stop_pos { + stop_pos = s + } + } + + val := strings.TrimSpace(rest[:stop_pos]) + // Only take the first token before any whitespace or another key + if idx := strings.IndexAny(val, " \n\r\t"); idx != -1 { + val = val[:idx] + } + return val +} + +var mesiMapping = map[int]string{ + 1: "Gennaio", + 2: "Febbraio", + 3: "Marzo", + 4: "Aprile", + 5: "Maggio", + 6: "Giugno", + 7: "Luglio", + 8: "Agosto", + 9: "Settembre", + 10: "Ottobre", + 11: "Novembre", + 12: "Dicembre", +} + +func ParseConsegna(original *string) *int { + currentMonth := int(time.Now().Month()) + if original == nil { + return nil + } + if *original == "Libero" { + return ¤tMonth + } else { + for k, v := range mesiMapping { + if *original == v { + return &k + } + } + } + return ¤tMonth +} + +func ParsePrezzo(input *string) int { + if input == nil || len(*input) == 0 { + return 0 + } + s := *input + s = strings.ReplaceAll(s, "€", "") + s = strings.ReplaceAll(s, " ", "") + s = strings.ReplaceAll(s, ".", "") + s = strings.ReplaceAll(s, ",", ".") + re := regexp.MustCompile(`\d+(\.\d+)?`) + match := re.FindString(s) + if match == "" { + return 0 + } + f, err := strconv.ParseFloat(match, 64) + if err != nil { + return 0 + } + return int(f * 100) +} + +func ParseTipologia(t1, t2 *string) (tipo *string) { + + if t1 == nil { + return utils.Ptr("Errore") + } + + if *t1 == "V" { + return utils.Ptr("Vendita") + } + + if *t1 == "I" { + return utils.Ptr("Cessione") + } + + if *t1 == "A" { + if t2 != nil { + normalized := strings.TrimSpace(*t2) + normalized = strings.ToLower(normalized) + switch normalized { + case "residenziale", "turistico", "stanza": + return utils.Ptr("Transitorio") + } + } + return utils.Ptr("Stabile") + } + return utils.Ptr("Errore") + +} diff --git a/apps/backend/parser/parser_test.go b/apps/backend/parser/parser_test.go new file mode 100644 index 0000000..2ed1acf --- /dev/null +++ b/apps/backend/parser/parser_test.go @@ -0,0 +1,418 @@ +package parser_test + +import ( + "backend/parser" + "backend/utils" + "fmt" + "testing" + "time" +) + +func TestParseDettaglio(t *testing.T) { + tests := []struct { + input string + wantDisp *string + wantPerm *[]int + wantPers *[]string + }{ + { + input: "disp:01/10 perm:3-6 persone:single", + wantDisp: utils.Ptr(fmt.Sprintf("01/10/%d", time.Now().Year())), + wantPerm: utils.Ptr([]int{3, 6}), + wantPers: utils.Ptr([]string{"single"}), + }, + { + input: "disp: perm: persone:", + wantDisp: nil, + wantPerm: nil, + wantPers: nil, + }, + { + input: "disp: perm:3-6 persone:single", // disp missing + wantDisp: nil, + wantPerm: utils.Ptr([]int{3, 6}), + wantPers: utils.Ptr([]string{"single"}), + }, + { + input: "disp:01/10 perm: persone:single", // perm missing + wantDisp: utils.Ptr(fmt.Sprintf("01/10/%d", time.Now().Year())), + wantPerm: nil, + wantPers: utils.Ptr([]string{"single"}), + }, + { + input: "disp:01/10 perm:3-6 persone:", // persone missing + wantDisp: utils.Ptr(fmt.Sprintf("01/10/%d", time.Now().Year())), + wantPerm: utils.Ptr([]int{3, 6}), + wantPers: nil, + }, + { + input: "disp: perm: persone:", // all fields missing + wantDisp: nil, + wantPerm: nil, + wantPers: nil, + }, + { + input: "disp:bad_date perm:3-6 persone:single", // invalid date + wantDisp: nil, + wantPerm: utils.Ptr([]int{3, 6}), + wantPers: utils.Ptr([]string{"single"}), + }, + { + input: "disp:01/10 perm:abc-6 persone:single", // non-numeric perm + wantDisp: utils.Ptr(fmt.Sprintf("01/10/%d", time.Now().Year())), + wantPerm: utils.Ptr([]int{6}), + wantPers: utils.Ptr([]string{"single"}), + }, + { + input: "disp:01/10 perm:3- persone:single", // trailing delimiter in perm + wantDisp: utils.Ptr(fmt.Sprintf("01/10/%d", time.Now().Year())), + wantPerm: utils.Ptr([]int{3}), + wantPers: utils.Ptr([]string{"single"}), + }, + { + input: "disp:01/10 perm:-6 persone:single", // leading delimiter in perm + wantDisp: utils.Ptr(fmt.Sprintf("01/10/%d", time.Now().Year())), + wantPerm: utils.Ptr([]int{6}), + wantPers: utils.Ptr([]string{"single"}), + }, + { + input: "disp:01/10 perm:3-6 persone:single/", // trailing delimiter in persone + wantDisp: utils.Ptr(fmt.Sprintf("01/10/%d", time.Now().Year())), + wantPerm: utils.Ptr([]int{3, 6}), + wantPers: utils.Ptr([]string{"single"}), + }, + { + input: "disp:01/10 perm:3-6 persone:/single", // leading delimiter in persone + wantDisp: utils.Ptr(fmt.Sprintf("01/10/%d", time.Now().Year())), + wantPerm: utils.Ptr([]int{3, 6}), + wantPers: utils.Ptr([]string{"single"}), + }, + { + input: "disp:01/10 perm:3-6 persone:single//coppia", // double delimiter in persone + wantDisp: utils.Ptr(fmt.Sprintf("01/10/%d", time.Now().Year())), + wantPerm: utils.Ptr([]int{3, 6}), + wantPers: utils.Ptr([]string{"single", "coppia"}), + }, + { + input: "perm:3-6 persone:single", // disp missing + wantDisp: nil, + wantPerm: utils.Ptr([]int{3, 6}), + wantPers: utils.Ptr([]string{"single"}), + }, + { + input: "disp:01/10 perm:3-6", // persone missing + wantDisp: utils.Ptr(fmt.Sprintf("01/10/%d", time.Now().Year())), + wantPerm: utils.Ptr([]int{3, 6}), + wantPers: nil, + }, + { + input: "disp:01/10 perm: persone:", // perm empty + wantDisp: utils.Ptr(fmt.Sprintf("01/10/%d", time.Now().Year())), + wantPerm: nil, + wantPers: nil, + }, + { + input: "", // completely empty string + wantDisp: nil, + wantPerm: nil, + wantPers: nil, + }, + { + input: "disp: 01/10 perm: 3-6 persone: single ", + wantDisp: utils.Ptr(fmt.Sprintf("01/10/%d", time.Now().Year())), + wantPerm: utils.Ptr([]int{3, 6}), + wantPers: utils.Ptr([]string{"single"}), + }, + { + input: "persone:single perm:3-6 disp:01/10", + wantDisp: utils.Ptr(fmt.Sprintf("01/10/%d", time.Now().Year())), + wantPerm: utils.Ptr([]int{3, 6}), + wantPers: utils.Ptr([]string{"single"}), + }, + { + input: "disp:01/10 perm:3--6 persone:single", + wantDisp: utils.Ptr(fmt.Sprintf("01/10/%d", time.Now().Year())), + wantPerm: utils.Ptr([]int{3, 6}), + wantPers: utils.Ptr([]string{"single"}), + }, + { + input: "disp:01/10\tperm:3-6\tpersone:single", + wantDisp: utils.Ptr(fmt.Sprintf("01/10/%d", time.Now().Year())), + wantPerm: utils.Ptr([]int{3, 6}), + wantPers: utils.Ptr([]string{"single"}), + }, + { + input: "disp:01/10 perm:3-6 persone:single/coppia!", + wantDisp: utils.Ptr(fmt.Sprintf("01/10/%d", time.Now().Year())), + wantPerm: utils.Ptr([]int{3, 6}), + wantPers: utils.Ptr([]string{"single", "coppia!"}), + }, + { + input: "disp:01/10 disp:02/11 perm:3-6 persone:single", + wantDisp: utils.Ptr(fmt.Sprintf("01/10/%d", time.Now().Year())), // Only first disp: is parsed + wantPerm: utils.Ptr([]int{3, 6}), + wantPers: utils.Ptr([]string{"single"}), + }, + { + input: "disp:01/10perm:3-6persone:single", + wantDisp: utils.Ptr(fmt.Sprintf("01/10/%d", time.Now().Year())), + wantPerm: utils.Ptr([]int{3, 6}), + wantPers: utils.Ptr([]string{"single"}), + }, + } + + for _, tt := range tests { + disp_time, perm_arr, persone_arr, _ := parser.ParseDettaglio(tt.input) + + //Handle *string comparison, including nil + if tt.wantDisp != nil { + if disp_time == nil { + t.Errorf("disp_time: got nil, want %v, input: %v", *tt.wantDisp, tt.input) + } else if *disp_time != *tt.wantDisp { + t.Errorf("disp_time: got %v, want %v, input: %v", *disp_time, *tt.wantDisp, tt.input) + } + } else if disp_time != nil { + t.Errorf("disp_time: got %v, want nil, input: %v", *disp_time, tt.input) + } + + // Handle *[]int comparison, including nil + if tt.wantPerm != nil { + if perm_arr == nil || len(*perm_arr) != len(*tt.wantPerm) { + t.Errorf("perm_arr: got %v, want %v, input: %v", perm_arr, *tt.wantPerm, tt.input) + } else { + for i := range *perm_arr { + if (*perm_arr)[i] != (*tt.wantPerm)[i] { + t.Errorf("perm_arr[%d]: got %v, want %v, input: %v", i, (*perm_arr)[i], (*tt.wantPerm)[i], tt.input) + } + } + } + } else if perm_arr != nil && len(*perm_arr) != 0 { + t.Errorf("perm_arr: got %v, want nil, input: %v", perm_arr, tt.input) + } + + // Handle *[]string comparison, including nil + if tt.wantPers != nil { + if persone_arr == nil || len(*persone_arr) != len(*tt.wantPers) { + t.Errorf("persone_arr: got %v, want %v, input: %v", persone_arr, *tt.wantPers, tt.input) + } else { + for i := range *persone_arr { + if (*persone_arr)[i] != (*tt.wantPers)[i] { + t.Errorf("persone_arr[%d]: got %v, want %v, input: %v", i, (*persone_arr)[i], (*tt.wantPers)[i], tt.input) + } + } + } + } else if persone_arr != nil && len(*persone_arr) != 0 { + t.Errorf("persone_arr: got %v, want nil, input: %v", persone_arr, tt.input) + } + } +} + +func TestParseConsegna(t *testing.T) { + tests := []struct { + input *string + wantConsegna *int + }{ + // Test all Italian month names + { + input: utils.Ptr("Gennaio"), + wantConsegna: utils.Ptr(1), + }, + { + input: utils.Ptr("Febbraio"), + wantConsegna: utils.Ptr(2), + }, + { + input: utils.Ptr("Marzo"), + wantConsegna: utils.Ptr(3), + }, + { + input: utils.Ptr("Aprile"), + wantConsegna: utils.Ptr(4), + }, + { + input: utils.Ptr("Maggio"), + wantConsegna: utils.Ptr(5), + }, + { + input: utils.Ptr("Giugno"), + wantConsegna: utils.Ptr(6), + }, + { + input: utils.Ptr("Luglio"), + wantConsegna: utils.Ptr(7), + }, + { + input: utils.Ptr("Agosto"), + wantConsegna: utils.Ptr(8), + }, + { + input: utils.Ptr("Settembre"), + wantConsegna: utils.Ptr(9), + }, + { + input: utils.Ptr("Ottobre"), + wantConsegna: utils.Ptr(10), + }, + { + input: utils.Ptr("Novembre"), + wantConsegna: utils.Ptr(11), + }, + { + input: utils.Ptr("Dicembre"), + wantConsegna: utils.Ptr(12), + }, + // Test "Libero" (should return current month) + { + input: utils.Ptr("Libero"), + wantConsegna: utils.Ptr(int(time.Now().Month())), + }, + // Test nil input + { + input: nil, + wantConsegna: nil, + }, + // Test empty string + { + input: utils.Ptr(""), + wantConsegna: utils.Ptr(int(time.Now().Month())), + }, + // Test lowercase (should return current month as default) + { + input: utils.Ptr("gennaio"), + wantConsegna: utils.Ptr(int(time.Now().Month())), + }, + // Test with spaces + { + input: utils.Ptr("Gennaio "), + wantConsegna: utils.Ptr(int(time.Now().Month())), + }, + { + input: utils.Ptr(" Gennaio"), + wantConsegna: utils.Ptr(int(time.Now().Month())), + }, + // Test with special characters + { + input: utils.Ptr("Gennaio!"), + wantConsegna: utils.Ptr(int(time.Now().Month())), + }, + // Test abbreviated forms + { + input: utils.Ptr("Gen"), + wantConsegna: utils.Ptr(int(time.Now().Month())), + }, + { + input: utils.Ptr("Feb"), + wantConsegna: utils.Ptr(int(time.Now().Month())), + }, + // Test English month names + { + input: utils.Ptr("January"), + wantConsegna: utils.Ptr(int(time.Now().Month())), + }, + // Test numeric strings + { + input: utils.Ptr("1"), + wantConsegna: utils.Ptr(int(time.Now().Month())), + }, + // Test mixed cases + { + input: utils.Ptr("GEnNaIo"), + wantConsegna: utils.Ptr(int(time.Now().Month())), + }, + // Test completely unrelated strings + { + input: utils.Ptr("NotAMonth"), + wantConsegna: utils.Ptr(int(time.Now().Month())), + }, + } + + for _, tt := range tests { + gotConsegna := parser.ParseConsegna(tt.input) + + if tt.wantConsegna != nil { + if gotConsegna == nil { + t.Errorf("got nil, want %v, input: %v", *tt.wantConsegna, tt.input) + } else if *gotConsegna != *tt.wantConsegna { + t.Errorf("got %v, want %v, input: %v", *gotConsegna, *tt.wantConsegna, tt.input) + } + } else if gotConsegna != nil { + t.Errorf("got %v, want nil, input: %v", *gotConsegna, tt.input) + } + } +} + +func TestParsePrezzo(t *testing.T) { + tests := []struct { + input *string + want int + }{ + {input: utils.Ptr("100"), want: 10000}, + {input: utils.Ptr("€ 100"), want: 10000}, + {input: utils.Ptr("100€"), want: 10000}, + {input: utils.Ptr("€100"), want: 10000}, + {input: utils.Ptr("100 €"), want: 10000}, + {input: utils.Ptr("€ 100,50"), want: 10050}, + {input: utils.Ptr("100,50€"), want: 10050}, + {input: utils.Ptr("€100,50"), want: 10050}, + {input: utils.Ptr("100,50 €"), want: 10050}, + {input: utils.Ptr("€ 1.000"), want: 100000}, + {input: utils.Ptr("1.000€"), want: 100000}, + {input: utils.Ptr("€1.000"), want: 100000}, + {input: utils.Ptr("1.000 €"), want: 100000}, + {input: utils.Ptr("€ 1.000,50"), want: 100050}, + {input: utils.Ptr("1.000,50€"), want: 100050}, + {input: utils.Ptr("€1.000,50"), want: 100050}, + {input: utils.Ptr("1.000,50 €"), want: 100050}, + } + + for _, tt := range tests { + got := parser.ParsePrezzo(tt.input) + + if got != tt.want { + inputStr := "" + if tt.input != nil { + inputStr = *tt.input + } + t.Errorf("got %v, want %v, input: %v", got, tt.want, inputStr) + } + } +} + +func TestProcessTipologia(t *testing.T) { + tests := []struct { + t1, t2 *string + want *string + }{ + {t1: utils.Ptr("V"), want: utils.Ptr("Vendita")}, + {t1: utils.Ptr("I"), want: utils.Ptr("Cessione")}, + {t1: utils.Ptr("A"), t2: utils.Ptr("Residenziale"), want: utils.Ptr("Transitorio")}, + {t1: utils.Ptr("A"), t2: utils.Ptr("Turistico"), want: utils.Ptr("Transitorio")}, + {t1: utils.Ptr("A"), t2: utils.Ptr("Stanza"), want: utils.Ptr("Transitorio")}, + {t1: utils.Ptr("A"), t2: nil, want: utils.Ptr("Stabile")}, + {t1: nil, want: utils.Ptr("Errore")}, + // Unknown t1 value + {t1: utils.Ptr("X"), want: utils.Ptr("Errore")}, + // Lowercase t1 value + {t1: utils.Ptr("v"), want: utils.Ptr("Errore")}, + // t1 is empty string + {t1: utils.Ptr(""), want: utils.Ptr("Errore")}, + // t2 is empty string with t1 == "A" + {t1: utils.Ptr("A"), t2: utils.Ptr(""), want: utils.Ptr("Stabile")}, + // t2 is unknown value with t1 == "A" + {t1: utils.Ptr("A"), t2: utils.Ptr("Commerciale"), want: utils.Ptr("Stabile")}, + // t2 is lowercase valid value with t1 == "A" + {t1: utils.Ptr("A"), t2: utils.Ptr("residenziale"), want: utils.Ptr("Transitorio")}, + // t2 is uppercase valid value with t1 == "A" + {t1: utils.Ptr("A"), t2: utils.Ptr("RESIDENZIALE"), want: utils.Ptr("Transitorio")}, + // t2 is "Stanza" with extra spaces + {t1: utils.Ptr("A"), t2: utils.Ptr(" Stanza "), want: utils.Ptr("Transitorio")}, + } + + for _, tt := range tests { + got := parser.ParseTipologia(tt.t1, tt.t2) + + if got == nil || *got != *tt.want { + t.Errorf("got %v, want %v, input t1: %v, t2: %v", *got, *tt.want, *tt.t1, *tt.t2) + } + } +} diff --git a/apps/backend/utils.go b/apps/backend/utils.go deleted file mode 100644 index 5ccb103..0000000 --- a/apps/backend/utils.go +++ /dev/null @@ -1,122 +0,0 @@ -package main - -import ( - "encoding/xml" - "fmt" - "io" - "log" - "net/http" - "os" - "path/filepath" - "reflect" - "strings" -) - -func MakeUppercase(s string) string { - if s != "" && reflect.TypeOf(s).Kind() == reflect.String && len(s) > 0 { - return strings.ToUpper(string(s[0])) + s[1:] - } - return "" -} - -func CheckStringIfPresent(value *string) string { - if value != nil { - return strings.TrimSpace(*value) - } - return "" -} - -func CheckStringIfPresentBool(value string) bool { - if value != "" { - return true - } else { - return false - } -} - -func fetchXML(url string) ([]byte, error) { - resp, err := http.Get(url) - if err != nil { - return []byte{}, fmt.Errorf("GET error: %v", err) - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - return []byte{}, fmt.Errorf("status error: %v", resp.StatusCode) - } - - data, err := io.ReadAll(resp.Body) - if err != nil { - return []byte{}, fmt.Errorf("read body: %v", err) - } - - return data, nil -} - -func GetDataXML[T any](url string) T { - xmlBytes, err := fetchXML(url) - if err != nil { - log.Fatalf("Failed to get XML: %v", err) - panic(err) - } - var result T - err = xml.Unmarshal(xmlBytes, &result) - if err != nil { - log.Fatalf("Failed to unmarshal XML: %v", err) - panic(err) - } - return result -} - -func GetPathsArray(path string) ([]string, error) { - var paths []string - - err := filepath.Walk(path, func(path string, info os.FileInfo, err error) error { - if err != nil { - return err - } - if !info.IsDir() { - paths = append(paths, path) - } - return nil - }) - - if err != nil { - return nil, err - } - - return paths, nil -} - -func GetPathDirsArray(path string) []string { - var folderNames []string - files, err := os.ReadDir(path) - if err != nil { - log.Fatal(err) - } - - for _, file := range files { - if file.IsDir() { - folderNames = append(folderNames, file.Name()) - } - } - return folderNames -} - -func GetStringValue(strPtr *string) string { - if strPtr != nil { - return *strPtr - } - return "" -} -func RemoveWhitespace(str string) string { - return strings.ReplaceAll(str, " ", "") -} - -func CommaToDot(str *string) *string { - if str != nil { - result := strings.ReplaceAll(*str, ",", ".") - return &result - } - return nil -}