using MP.Data.DbModels; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace MP.Data.Services { public class TimeSeriesUtils { /// /// Riduce una lista di oggetti FLModel mantenendo una spaziatura temporale uniforme. /// Gli oggetti vengono raggruppati in intervalli temporali e viene selezionato un rappresentante per intervallo. /// /// La lista originale di oggetti FLModel. /// Il numero massimo di oggetti desiderato nella lista risultante. /// Una nuova lista di FLModel con una distribuzione temporale più uniforme, /// o la lista originale se già minore o uguale a maxNum. public static List DownsampleFluxModels(List data, int maxNum) { // Caso base: se la lista è vuota o già abbastanza piccola, la restituiamo così com'è. if (data == null || data.Count <= maxNum) { return data; } // 1. Ordina la lista per dtEvento. Questo è FONDAMENTALE per il campionamento temporale. // Se la lista non è ordinata, il downsampling non funzionerà correttamente. var sortedData = data.OrderBy(m => m.dtEvento).ToList(); // Determina l'intervallo temporale totale dei dati. DateTime startTime = sortedData.First().dtEvento; DateTime endTime = sortedData.Last().dtEvento; // Calcola la durata totale in millisecondi (o tick per maggiore precisione). long totalDurationTicks = endTime.Ticks - startTime.Ticks; // Se tutti gli eventi avvengono nello stesso istante, non possiamo distribuirli. if (totalDurationTicks == 0) { return sortedData.Take(maxNum).ToList(); } // Inizializza la lista per i risultati campionati. var sampledList = new List(); // 2. Calcola la dimensione di ciascun bucket temporale. // Dividiamo la durata totale per il numero massimo desiderato di elementi. // Questo ci dà la durata approssimativa di ciascun "bucket" temporale. long ticksPerBucket = totalDurationTicks / maxNum; // Garantisce che ticksPerBucket sia almeno 1 per evitare divisioni per zero o bucket infiniti. if (ticksPerBucket == 0) { ticksPerBucket = 1; } // 3. Itera attraverso i dati e seleziona un rappresentante per ogni bucket. // Useremo il primo elemento trovato in un nuovo bucket come rappresentante. // Un'alternativa potrebbe essere prendere l'elemento più vicino al centro del bucket, // o l'ultimo elemento del bucket. Per semplicità, scegliamo il primo. // Inizializza il "target time" per il primo bucket. DateTime currentBucketStartTime = startTime; FLModel currentBucketRepresentative = null; foreach (var item in sortedData) { // Se l'elemento corrente cade nel bucket attuale o in uno precedente, // lo consideriamo per il bucket corrente. // Aggiorniamo il rappresentante solo se è il primo trovato o se si vuole una logica diversa (es. ultimo elemento nel bucket). if (item.dtEvento.Ticks >= currentBucketStartTime.Ticks && item.dtEvento.Ticks < currentBucketStartTime.Ticks + ticksPerBucket) { if (currentBucketRepresentative == null) { currentBucketRepresentative = item; } // Altre logiche per scegliere il rappresentante del bucket (es. ultimo, medio, ecc.) // currentBucketRepresentative = item; // Se vuoi l'ultimo elemento del bucket } else { // Abbiamo superato il bucket corrente. // Aggiungiamo il rappresentante del bucket precedente, se ne abbiamo trovato uno. if (currentBucketRepresentative != null) { sampledList.Add(currentBucketRepresentative); } // Sposta al prossimo bucket. Dobbiamo farlo in un loop per coprire eventuali "salti" // nel caso in cui non ci siano dati per diversi bucket consecutivi. while (item.dtEvento.Ticks >= currentBucketStartTime.Ticks + ticksPerBucket) { currentBucketStartTime = new DateTime(currentBucketStartTime.Ticks + ticksPerBucket); // Se non ci sono dati per un bucket, potremmo voler inserire un placeholder // o semplicemente saltarlo. Qui lo saltiamo implicitamente. } // Inizia il nuovo bucket con l'elemento corrente come primo rappresentante. currentBucketRepresentative = item; } // Se abbiamo già raggiunto il numero massimo desiderato, possiamo fermarci. if (sampledList.Count >= maxNum) { break; } } // Aggiungi l'ultimo rappresentante del bucket, se presente e se non abbiamo ancora raggiunto maxNum. if (currentBucketRepresentative != null && sampledList.Count < maxNum) { sampledList.Add(currentBucketRepresentative); } // In alcuni rari casi (es. dati molto sparsi all'inizio o alla fine, o ticksPerBucket molto grandi) // potremmo non raggiungere maxNum elementi ma comunque voler restituire i dati campionati. // Se la lista campionata è ancora troppo piccola (meno di maxNum), // potremmo voler applicare un'ulteriore logica di selezione o padding. // Per questo problema specifico, l'algoritmo tende a dare al massimo `maxNum` elementi. // Se il numero di bucket generati è maggiore di maxNum (potrebbe succedere con ticksPerBucket=1 e molti dati) // la condizione `if (sampledList.Count >= maxNum)` sopra limiterà l'aggiunta. return sampledList; } } }