Calcoli & formule

Algoritmi business estratti dal codice Spring, replicabili byte-per-byte

1. Calcolo germinabilità media (cuore del software)

Metodo: AnalisiServiceImpl.generaConta(AnalisiBO bo), righe 377-482 del file AnalisiServiceImpl.java.

Input

Algoritmo — pseudo-codice esatto

mapVigore = {SCARSO=0, MEDIO=1, OTTIMO=2, null=0}
mapVigoreI = {0=SCARSO, 1=MEDIO, 2=OTTIMO}

germinato1 = 0; germinato2 = 0
duro1 = 0; duro2 = 0
fresco1 = 0; fresco2 = 0
anormale1 = 0; anormale2 = 0
morto1 = 0; morto2 = 0
vigore1 = 0; vigore2 = 0

# --- accumulo per tutte le conte ---
for conta in conte:              # tipicamente 4 ripetizioni
    germinato1 += conta.germinati_1
    germinato2 += conta.germinati_2
    duro1 += conta.duri_1
    duro2 += conta.duri_2
    fresco1 += conta.freschi_1
    fresco2 += conta.freschi_2
    anormale1 += conta.anormali_1
    anormale2 += conta.anormali_2
    morto1 += conta.morti_1
    morto2 += conta.morti_2
    vigore1 += mapVigore[conta.vigore_1]
    # per vigore2: se germinato2 > 0 usa vigore_2, altrimenti usa vigore_1
    if germinato2 > 0:
        vigore2 += mapVigore[conta.vigore_2]
    else:
        vigore2 += mapVigore[conta.vigore_1]

# --- medie (arrotondamento verso zero) ---
size = len(conte)
germinato1I = int(germinato1 / size)
germinato2I = int(germinato2 / size)
duro1I      = int(duro1 / size);       duro2I     = int(duro2 / size)
fresco1I    = int(fresco1 / size);     fresco2I   = int(fresco2 / size)
anormale1I  = int(anormale1 / size);   anormale2I = int(anormale2 / size)
morto1I     = int(morto1 / size);      morto2I    = int(morto2 / size)

# --- normalizzazione: somma totale deve essere 100 ---
sommaI  = germinato1I + duro1I + fresco1I + anormale1I + morto1I
sommaII = germinato2I + duro2I + fresco2I + anormale2I + morto2I

if sommaII > 0:
    sommaTotale = sommaI + sommaII
    if sommaTotale > 100:
        germinato2I -= (sommaTotale - 100)     # sottrai l'eccesso
    elif sommaTotale < 100:
        germinato2I += (100 - sommaTotale)     # aggiungi la differenza

# --- germinabilità finale (valore stored in DB) ---
germinabilita = germinato1I + germinato2I

# --- vigore finale ---
vigoreI = int((vigore1 + vigore2) / (size * 2))
vigore = mapVigoreI[vigoreI]
⚠ Due cose non ovvie
  1. Arrotondamento Java (int) = truncate (verso zero), NON round. Es. (int)(99.9/4) = 24, non 25.
  2. La normalizzazione "ruba" al germinato2: il secondo valore di germinati è il "campo sacrificabile" per far quadrare il 100%. Se la somma fa 101, viene tolto 1 da germinati_2. Se fa 99, ne viene aggiunto 1.

Esempio numerico

4 ripetizioni con questi valori:

Ripetiz.g1d1f1a1m1g2d2f2a2m2
A802300101211
B82121092111
C783310111201
D812200102111
Σ32181020406634
media (int)802200101101

SommaI = 80+2+2+0+0 = 84. SommaII = 10+1+1+0+1 = 13. Totale = 97 < 100.

Normalizzazione: germinati_2 += (100 - 97) = 3 → germinati_2 = 13.

Germinabilità finale = 80 + 13 = 93%

2. Generazione FT (fattura tecnica)

Metodo: AnalisiServiceImpl.nuova(LottoBO lotto), righe 94-110.

Formato

NNN/YY dove NNN è progressivo globale nell'anno, YY sono le ultime 2 cifre dell'anno.

Algoritmo

anno = formato(now(), "yy")   // es. "26" per 2026

# Query Hibernate Criteria (DAO):
ultimoFT = SELECT ft
           FROM semiorto_analisi
           WHERE ft LIKE '%/26'
           ORDER BY id DESC
           LIMIT 1

if ultimoFT == null or ultimoFT == "" or "/" not in ultimoFT:
    numero = 1
else:
    numero = int(ultimoFT.split("/")[0]) + 1

ft = f"{numero}/{anno}"   // es. "3562/26"
⚠ Race condition teorica

Il codice non usa lock DB. Se due tecnici creano contemporaneamente nuova analisi con FT, è possibile collisione. In pratica non è mai successo perché Semiorto ha 4 tecnici e il carico è basso.

In TopSeed suggeriamo di usare SELECT ... FOR UPDATE o sequence DB dedicata.

3. Conta pre-popolata alla creazione analisi

Quando il tecnico apre "Nuova Analisi", il sistema crea una ContaBO A con valori pre-compilati usando i parametri della specie del lotto:

c = new ContaBO()
c.init()    # azzera tutti i campi a 0
c.getGiorni().clear()
c.getGiorni().add( specie.giorni1Conta )                       # es. 5
c.getGiorni().add( specie.giorniTConta - specie.giorni1Conta ) # es. 10 - 5 = 5
c.setGiorniT( specie.giorniTConta )                            # es. 10

Quindi per una specie con giorni1Conta=5, giorniTConta=10:

4. Prossima operazione (badge lista "In corso")

Metodo: AnalisiServiceImpl.inCorso(), righe 122-151.

Per ogni analisi "in corso", calcola quando è la prossima operazione:

c = Calendar.getInstance()
c.setTime(analisi.dataInizio)

if analisi.semiortoContes.isEmpty():
    # nessuna conta ancora → prossima è la 1ª conta
    c.add(DAY, specie.giorni1conta)
    prossimaOperazione = "(1°)"
    dataProssimaOperazione = c.getTime()
else:
    # almeno una conta fatta → prossima è la 2ª (o finale)
    c.add(DAY, specie.giorniTconta)
    prossimaOperazione = "(2°)"
    dataProssimaOperazione = c.getTime()

5. Generazione codice lotto (dead code?)

Metodo LottoServiceImpl.generaLotto(LottoBO bo), righe 170-190. Esiste ma non viene invocato da nessun controller della nostra copia sorgenti.

Algoritmo

s = ""

# Lettera anno: 'A'=2025, 'B'=2026, 'C'=2027, ...
# Formula: (anno in 2 cifre + 64) come carattere ASCII
data = int(format(dataArrivo, "yy")) + 64
s += chr(data)
# Esempio: 2020 → 20+64=84 → 'T'
# Esempio: 2025 → 25+64=89 → 'Y' ??? (si aspetta 'A')
# NOTA: la formula potrebbe essere sbagliata, o presupporre un anno base
# diverso (es. anno - 1985 + 64 = offset per A=2025?)

# Codice specie (uppercase)
s += toUpper(specie.codice)

# Codice varietà (uppercase)
s += toUpper(varieta.codice)

# Codice produttore (uppercase)
s += toUpper(produttore.codice)

return s
Analisi critica

Tre problemi con questa funzione:

  1. Non viene chiamata dal controller. La JSP di creazione lotto chiede al tecnico di inserire il codice manualmente.
  2. La formula lettera-anno è sospetta. 20+64=84 è 'T', non 'A'. Bisognerebbe chiedere a Diego se intendeva anno - 1960 o simile.
  3. È plausibile che sia codice incompleto abbandonato: il dev lo ha scritto per un'altra feature (codice auto) poi non completata.

Nella migrazione TopSeed: ignorare, mantenere input manuale del codice lotto come nel comportamento attuale.

6. Check "lotto già esistente"

Metodo LottoServiceImpl.lottoDisponibile(LottoBO bo).

def lottoDisponibile(bo):
    return !lottoDao.isLottoExists(bo.lotto, bo.id)

# DAO:
def isLottoExists(codiceLotto, idEscluso):
    SELECT COUNT(*)
    FROM semiorto_lotto
    WHERE id = :codiceLotto
      AND (id_lotto != :idEscluso OR :idEscluso IS NULL)

Usato nel controller LottoController.update(): se il codice esiste E l'utente non ha spuntato forza=true, mostra warning.

7. Password hashing

Classe: Spring Security ShaPasswordEncoder (SHA-1).

encoder = new ShaPasswordEncoder()

# Hashing nuova password
hash = encoder.encodePassword(rawPassword, null)   # salt = null!

# Verifica (automatico da Spring Security in login)
matches = encoder.isPasswordValid(storedHash, rawPassword, null)
🔒 Sicurezza deprecata

SHA-1 senza salt è deprecato. Attacchi rainbow table comuni.

Nella migrazione TopSeed:

  1. Importa hash come legacy_sha_hash
  2. Al primo login dopo migrazione, se password digitata matcha SHA-1 legacy → rehash a bcrypt e salva in password_hash
  3. Dopo X mesi, force reset per chi non ha ancora fatto login

8. Placeholder certificato DOCX

Metodo AnalisiController.certificato(). I 20 placeholder sostituiti nel template:

Placeholder nel DOCXValore JavaFormato
$$_NOME_SPECIE_$$lotto.specie.nome("en")inglese
$$_NOME_LATINO_$$lotto.specie.nome("lat")latino
$$_NOME_VARIETA_$$lotto.varieta.nome
$$_NOME_LOTTO_$$lotto.lottocodice testuale
$$_NOME_CATEGORIA_$$analisi.categoriaSTANDARD / COMMERCIALE
$$_DATA_ARRIVO_$$dataArrivoLaboratoriodd/MM/yyyy
$$_DATA_RICEZIONE_$$dataIniziodd/MM/yyyy
$$_DATA_FINE_$$dataFine (chiusura)dd/MM/yyyy
$$_FT_$$ftes. "3562/26"
$$_SEME_PURO_$$semePuro%
$$_MATERIALE_INERTE_$$materialeInerte%
$$_ALTRI_SEMI_$$"0.0"Hardcoded! Bug noto: non usa il valore DB
$$_NUMERO_GIORNI_$$conta.giorniTint
$$_SEMI_GERMINATI_$$conta.germinatiT (=germ1+germ2)int (0-100)
$$_SEMI_DURI_$$conta.duriTint
$$_SEMI_FRESCHI_$$conta.freschiTint
$$_SEMI_ANORMALI_$$conta.anormaliTint
$$_SEMI_MORTI_$$conta.mortiTint
$$_UMIDITA_$$analisi.ur%
$$_NOME_TECNICO_$$tecnico.nome
$$_DATA_$$now()"April 20, 2026"

9. Anni disponibili (per dropdown cerca)

SELECT YEAR(data) as anno
FROM semiorto_analisi
GROUP BY anno
ORDER BY anno IS NULL DESC, anno DESC

Questo restituisce tutti gli anni in cui esistono analisi, + NULL in cima (rappresentato come "In corso").

10. Calcolo valori totali conte (colonne *T)

Nel BO ContaBO, i campi *T (totali) sono calcolati così:

anormaliT = anormali_1 + anormali_2
duriT     = duri_1 + duri_2
freschiT  = freschi_1 + freschi_2
germinatiT = germinati_1 + germinati_2
mortiT    = morti_1 + morti_2

if germinati_2 != null or anormali_2 != null or duri_2 != null
   or freschi_2 != null or morti_2 != null:
    giorniT = giorni_2
else:
    giorniT = giorni_1  # se non c'è seconda lettura, giorniT = giorno della prima

11. Vigore della conta singola (media di vigore_1 e vigore_2)

vigore = 0
if vigore_1: vigore += mapVigore[vigore_1]   # 0/1/2
if vigore_2: vigore += mapVigore[vigore_2]
vigore = int(vigore / 2)       # media
result = mapVigoreI[vigore]    # converti indice → stringa

Esempio: vigore_1=OTTIMO (2), vigore_2=MEDIO (1) → (2+1)/2 = 1 → MEDIO.