Files
adas-core/adas-core.Application/Services/GroupedObservationService.cs
T

1262 lines
53 KiB
C#

using System.Globalization;
using adas_core.Application.Repositories.Interfaces;
using adas_core.Application.Services.Interfaces;
using adas_core.Application.Subscriptions;
using adas_core.Domain.Enums;
using adas_core.Domain.Models;
using adas_core.Domain.Models.AppSettings;
using adas_core.Domain.Models.GroupedObservations;
using adas_core.Domain.Utils;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using MongoDB.Bson;
using static adas_core.Domain.Models.GroupedObservation;
namespace adas_core.Application.Services;
public class GroupedObservationService : IGroupedObservationService
{
private readonly CacheSettings? _cacheSettings;
private readonly ICacheService _cacheService;
private readonly IConfigObservationService _configObservationService;
private readonly ILogger<GroupedObservationService> _logger;
private readonly IObservationRepository _observationRepository;
public GroupedObservationService(
IObservationRepository observationRepository,
IConfigObservationService configObservationService,
ILogger<GroupedObservationService> logger,
ICacheService cacheService,
IOptions<ApiSettings> apiSettings,
IOptions<CacheSettings> cacheSettings)
{
_observationRepository = observationRepository;
_configObservationService = configObservationService;
_logger = logger;
_cacheService = cacheService;
_cacheSettings = cacheSettings.Value;
}
// PUBLIC API (Use cases)
public async Task<GroupedObservation> GenerateGroupedObservation(
ObjectId patientId,
GroupedField groupedField,
string timeZoneId = "Romance Standard Time",
bool cacheIsChecked = false,
CancellationToken ct = default)
{
// Loader que calcula TODA la lista de observaciones agrupadas
async Task<List<GroupedObservationObs>> ComputeObservationsAsync()
{
var result = await _observationRepository.AggregatedPatientGroupedObservations(patientId, groupedField);
var tz = TimeZoneInfo.FindSystemTimeZoneById(timeZoneId);
result = FillHours(result, groupedField);
if (groupedField.StartTimeShift.Count > 0 &&
groupedField.Regularity == GroupedObservationEnum.Regularity.Shift)
{
var shiftGroupObservations = GenerateShiftObservations(result, groupedField);
result = CalculateShiftObservations(shiftGroupObservations, groupedField);
}
if (groupedField.Result.Contains(GroupedObservationEnum.Result.HalfHour))
{
result = CalculateHalfHourObservations(result);
result = FillHours(result, groupedField);
}
if (groupedField.Result.Contains(GroupedObservationEnum.Result.LastFilled))
result = await CalculateLastFilledObservations(result, groupedField, patientId);
var list = new List<GroupedObservationObs>();
foreach (var it in result.ToList())
{
var id = it.TryGetElement("_id", out var _) ? it.GetElement("_id").Value.AsBsonDocument : null;
var name = id?.Get("name")?.AsString;
if (name is null or "_id")
{
var idValue = id?.Get("value") as BsonDocument;
name = idValue?.Get("name")?.AsString;
}
var uTime = it.Get("time")?.ToUniversalTime();
if (!uTime.HasValue)
{
_logger.LogError("Error getting time to universal time. Bson document: {it}", it);
continue;
}
var obsTime = TimeZoneInfo.ConvertTimeFromUtc(uTime.Value, tz);
obsTime = DateTime.SpecifyKind(obsTime, DateTimeKind.Local);
var obs = new GroupedObservationObs
{
Time = obsTime,
Name = name ?? string.Empty
};
if (it.TryGetValue("shift", out var shiftValue) && it.TryGetValue("day", out _))
{
obs.Shift = shiftValue.ToInt32();
obs.ShiftDate = obs.Time;
}
if (id != null && id.Contains("min")) obs.MinAlert = id.Get("min")?.ToDouble();
if (id != null && id.Contains("max")) obs.MaxAlert = id.Get("max")?.ToDouble();
if (id != null && it.Contains("isFilled")) obs.IsFilled = (bool)it.Get("isFilled");
if (groupedField.Result.Contains(GroupedObservationEnum.Result.First))
{
var g = !it.Contains("first") ? null : it.Get("first")?.AsBsonDocument;
if (g != null)
{
var value = g.Get("value")?.ToObject();
var min = g.Get("min")?.ToDouble();
var max = g.Get("max")?.ToDouble();
var uTim = g.Get("time")?.ToUniversalTime();
if (!uTim.HasValue) { _logger.LogError("Error getting utc time in 'first': {it}", it); goto addObs; }
var time = TimeZoneInfo.ConvertTimeFromUtc(uTim.Value, tz);
time = DateTime.SpecifyKind(time, DateTimeKind.Local);
if (string.IsNullOrEmpty(name) || value == null)
{ _logger.LogError("Invalid 'first' grouped obs (name/value) patient:{patientId}", patientId); goto addObs; }
obs.First = new GroupedObservationObsValue(
value, time,
await GroupedObservationStatus(groupedField, GroupedObservationEnum.Result.First, name, value, min, max)
);
}
}
if (groupedField.Result.Contains(GroupedObservationEnum.Result.Last))
{
var g = !it.Contains("last") ? null : it.Get("last")?.AsBsonDocument;
if (g != null)
{
var value = g.Get("value")?.ToObject();
var min = g.Get("min")?.ToDouble();
var max = g.Get("max")?.ToDouble();
var uTim = g.Get("time")?.ToUniversalTime();
if (!uTim.HasValue) { _logger.LogError("Error getting utc time in 'last': {it}", it); goto addObs; }
var time = TimeZoneInfo.ConvertTimeFromUtc(uTim.Value, tz);
time = DateTime.SpecifyKind(time, DateTimeKind.Local);
if (string.IsNullOrEmpty(name) || value == null)
{ _logger.LogError("Invalid 'last' grouped obs (name/value) patient:{patientId}", patientId); goto addObs; }
obs.Last = new GroupedObservationObsValue(
value, time,
await GroupedObservationStatus(groupedField, GroupedObservationEnum.Result.Last, name, value, min, max)
);
}
}
if (groupedField.Result.Contains(GroupedObservationEnum.Result.Min))
{
var g = !it.Contains("min") ? null : it.Get("min")?.AsBsonDocument;
if (g != null)
{
var value = g.Get("value")?.ToObject();
var min = g.Get("min")?.ToDouble();
var max = g.Get("max")?.ToDouble();
var uTim = g.Get("time")?.ToUniversalTime();
if (!uTim.HasValue) { _logger.LogError("Error getting utc time in 'min': {it}", it); goto addObs; }
var time = TimeZoneInfo.ConvertTimeFromUtc(uTim.Value, tz);
time = DateTime.SpecifyKind(time, DateTimeKind.Local);
if (string.IsNullOrEmpty(name) || value == null)
{ _logger.LogError("Invalid 'min' grouped obs (name/value) patient:{patientId}", patientId); goto addObs; }
obs.Min = new GroupedObservationObsValue(
value, time,
await GroupedObservationStatus(groupedField, GroupedObservationEnum.Result.Min, name, value, min, max)
);
}
}
if (groupedField.Result.Contains(GroupedObservationEnum.Result.Max))
{
var g = !it.Contains("max") ? null : it.Get("max")?.AsBsonDocument;
if (g != null)
{
var value = g.Get("value")?.ToObject();
var min = g.Get("min")?.ToDouble();
var max = g.Get("max")?.ToDouble();
var uTim = g.Get("time")?.ToUniversalTime();
if (!uTim.HasValue) { _logger.LogError("Error getting utc time in 'max': {it}", it); goto addObs; }
var time = TimeZoneInfo.ConvertTimeFromUtc(uTim.Value, tz);
time = DateTime.SpecifyKind(time, DateTimeKind.Local);
if (string.IsNullOrEmpty(name) || value == null)
{ _logger.LogError("Invalid 'max' grouped obs (name/value) patient:{patientId}", patientId); goto addObs; }
obs.Max = new GroupedObservationObsValue(
value, time,
await GroupedObservationStatus(groupedField, GroupedObservationEnum.Result.Max, name, value, min, max)
);
}
}
if (groupedField.Result.Contains(GroupedObservationEnum.Result.Sum))
{
var value = !it.Contains("sum") ? null : it.Get("sum")?.ToObject();
if (value != null && !string.IsNullOrEmpty(name))
{
obs.Sum = new GroupedObservationObsValue(
value, null,
await GroupedObservationStatus(groupedField, GroupedObservationEnum.Result.Sum, name, value, null, null)
);
}
}
if (groupedField.Result.Contains(GroupedObservationEnum.Result.Average))
{
var value = !it.Contains("average") ? null : it.Get("average")?.ToObject();
if (value != null && !string.IsNullOrEmpty(name))
{
obs.Average = new GroupedObservationObsValue(
value, null,
await GroupedObservationStatus(groupedField, GroupedObservationEnum.Result.Average, name, value, null, null)
);
}
}
if (groupedField.Result.Contains(GroupedObservationEnum.Result.Count))
{
var value = !it.Contains("count") ? null : it.Get("count")?.AsNullableInt32;
if (value != null && !string.IsNullOrEmpty(name))
{
obs.Count = new GroupedObservationObsValue(
value, null,
await GroupedObservationStatus(groupedField, GroupedObservationEnum.Result.Count, name, value, null, null)
);
}
}
if (groupedField.Result.Contains(GroupedObservationEnum.Result.LastFilled))
{
var g = !it.Contains("lastfilled") ? null : it.Get("lastfilled")?.AsBsonDocument;
if (g != null)
{
var value = g.Get("value")?.ToObject();
var min = g.Get("min")?.ToDouble();
var max = g.Get("max")?.ToDouble();
var uTim = g.Get("time")?.ToUniversalTime();
if (!uTim.HasValue) { _logger.LogError("Error getting utc time in 'lastfilled': {it}", it); goto addObs; }
var time = TimeZoneInfo.ConvertTimeFromUtc(uTim.Value, tz);
time = DateTime.SpecifyKind(time, DateTimeKind.Local);
if (value != null && !string.IsNullOrEmpty(name))
{
obs.LastFilled = new GroupedObservationObsValue(
value, time,
await GroupedObservationStatus(groupedField, GroupedObservationEnum.Result.LastFilled, name, value, min, max)
);
}
}
}
if (groupedField.Result.Contains(GroupedObservationEnum.Result.HalfHour))
{
var g = !it.Contains("halfhour") ? null : it.Get("halfhour")?.AsBsonDocument;
if (g != null)
{
var value = g.Get("value")?.ToObject();
var min = g.Get("min")?.ToDouble();
var max = g.Get("max")?.ToDouble();
var uTim = g.Get("time")?.ToUniversalTime();
if (!uTim.HasValue) { _logger.LogError("Error getting utc time in 'halfhour': {it}", it); goto addObs; }
var time = TimeZoneInfo.ConvertTimeFromUtc(uTim.Value, tz);
time = DateTime.SpecifyKind(time, DateTimeKind.Local);
if (value != null && !string.IsNullOrEmpty(name))
{
obs.HalfHour = new GroupedObservationObsValue(
value, time,
await GroupedObservationStatus(groupedField, GroupedObservationEnum.Result.Last, name, value, min, max)
);
}
}
}
addObs:
_logger.LogTrace("PatientId: {patientId}. Add to grouped observation: {obs}", patientId, obs);
list.Add(obs);
}
return list;
}
// caché explícita por clave + TTL ---
if (!cacheIsChecked)
{
var (key, ttl) = CacheKeys.GroupedObsKeyWithTtl(_cacheSettings, patientId, groupedField.Name ?? string.Empty);
var observations = await _cacheService.GetOrSetObjectAsync(
key,
ComputeObservationsAsync,
ttl,
ct);
return new GroupedObservation
{
PatientId = patientId,
Group = groupedField.Group,
Name = groupedField.Name,
Observations = observations
};
}
// Re-cálculo forzado (sin tocar caché)
var computed = await ComputeObservationsAsync();
return new GroupedObservation
{
PatientId = patientId,
Group = groupedField.Group,
Name = groupedField.Name,
Observations = computed
};
}
public async Task<GroupedObservation> GenerateGroupedObservation(
ObjectId patientId,
GroupedField groupedField,
List<GroupedObservationObs> wsgLastGroupedObservationObs,
PatientObservation obs,
string timeZoneId = "Romance Standard Time")
{
// Estos resultados requieren recálculo completo (no incremental)
if (groupedField.Result.Contains(GroupedObservationEnum.Result.Average) ||
groupedField.Result.Contains(GroupedObservationEnum.Result.HalfHour) ||
groupedField.Result.Contains(GroupedObservationEnum.Result.Sum))
{
return await GenerateGroupedObservation(patientId, groupedField, timeZoneId, true);
}
// Clave consistente con CacheDispatcher / backends
var cacheKey = $"GroupedObs:{patientId}:{groupedField.Name}";
// Intentar leer caché actual
var cached = await _cacheService.GetObjectAsync<List<GroupedObservationObs>>(cacheKey, true);
if (cached != null)
{
_logger.LogInformation("Return cached grouped obs (incremental)");
// Actualizar lista en memoria
var updated = LocateCurrentObsInGroup(cached, obs, groupedField);
// Persistir nueva lista (SET incremental)
await _cacheService.SetObjectAsync(cacheKey, updated, true);
return new GroupedObservation
{
PatientId = patientId,
Group = groupedField.Group,
Name = groupedField.Name,
Observations = updated
};
}
// Si MISS, recálculo completo y salida
return await GenerateGroupedObservation(patientId, groupedField, timeZoneId, true);
}
public async Task<List<PatientObservation>> FindLastObservations(ObjectId patientId, int num = 2,
List<string>? filterObservations = null)
{
var result = await _observationRepository.AggregatedPatientLastObservations(patientId, num, filterObservations);
return result;
}
public async Task<GroupedObservation> CreateNextEmptyObs(WsSubscriberGrouped ws)
{
var gf = new GroupedField
{
Max = ws.Max,
Since = ws.Since,
StartTimeShift = ws.StartTimeShift,
Regularity = ws.Regularity,
Result = ws.Result,
Names = ws.Names,
Name = ws.Names.First(),
Group = ws.Group.FirstOrDefault().Value
};
// Mantenemos el uso del HashCode
var currentObsList = await _cacheService.GetObjectAsync<List<GroupedObservationObs>>(ws.HashCode, false);
if (currentObsList is { Count: > 0 })
{
var orderCurrentObsList = currentObsList.OrderBy(c => c.Time).ToList();
foreach (var name in gf.Names)
{
var obs = orderCurrentObsList.LastOrDefault(c => c.Name == name)
?? orderCurrentObsList.LastOrDefault();
if (obs != null)
{
var (timesLeft, multiplierFactor) = TimesLeftToCreateEmptyObs(obs, gf);
for (var i = 1; i <= timesLeft; i++)
orderCurrentObsList.Add(CreateNextObs(obs, gf, i * multiplierFactor));
}
}
// Ajuste de ventana temporal a mostrar según Regularity
DateTime dateLimit;
var requireMinutes = false;
var requireSeconds = false;
switch (gf.Regularity)
{
case GroupedObservationEnum.Regularity.Minute:
dateLimit = new DateTime(
DateTime.Now.Year, DateTime.Now.Month, DateTime.Now.Day,
DateTime.Now.Hour, DateTime.Now.Minute, 0
).AddMinutes(gf.Max * -1);
requireMinutes = true;
break;
case GroupedObservationEnum.Regularity.Second:
dateLimit = new DateTime(
DateTime.Now.Year, DateTime.Now.Month, DateTime.Now.Day,
DateTime.Now.Hour, DateTime.Now.Minute, DateTime.Now.Second
).AddSeconds(gf.Max * -1);
requireMinutes = true;
requireSeconds = true;
break;
default:
dateLimit = new DateTime(
DateTime.Now.Year, DateTime.Now.Month, DateTime.Now.Day,
DateTime.Now.Hour, 0, 0
).AddHours(gf.Max * -1);
break;
}
var cacheResult = orderCurrentObsList
.Where(c =>
new DateTime(
c.Time.Year, c.Time.Month, c.Time.Day, c.Time.Hour,
requireMinutes ? c.Time.Minute : 0,
requireSeconds ? c.Time.Second : 0
) >= dateLimit)
.ToList();
// Persistimos la lista incrementada en la misma clave para no romper el flujo actual.
await _cacheService.SetObjectAsync(ws.HashCode, cacheResult, false);
return new GroupedObservation
{
PatientId = ws.PatientId,
Group = gf.Group,
Name = gf.Name,
Observations = cacheResult
};
}
// Si no hay caché o hay MISS → recálculo completo (sin usar caché en esta ruta).
return await GenerateGroupedObservation(ws.PatientId, gf, ws.TimeZoneId, true);
}
// PRIVATE: High-level composition steps
public List<BsonDocument> FillHours(List<BsonDocument> result, GroupedField groupedField)
{
var now = DateTime.UtcNow;
//var resultNumber = groupedField.Max;
var names = groupedField.Names.Count > 1 ? groupedField.Names : [groupedField.Name ?? string.Empty];
if (result.Count >= groupedField.Max * names.Count)
return result; //We have data from each hour/day/minute, dont need to calc.
List<BsonDocument> fillResult = [];
//var first = result.FirstOrDefault();
foreach (var name in names)
{
var filteredDocuments = result.Where(doc => doc["_id"]["name"] == name).ToList();
if (filteredDocuments.Count >= groupedField.Max)
{
filteredDocuments = filteredDocuments.OrderBy(e => e.Get("time")?.ToLocalTime()).ToList();
fillResult.AddRange(filteredDocuments);
continue;
}
var expectedDate = DateTime.MinValue;
var inverseCounter = 0;
for (var i = groupedField.Max; i != 0; i--)
{
var expectedMatch = true;
var value = filteredDocuments.ElementAtOrDefault(inverseCounter);
DateTime? time = null;
if (value == null)
expectedMatch = false;
else
//Estamos recogiendo la hora en UTC
time = value.Get("time")?.ToUniversalTime();
switch (groupedField.Regularity)
{
case GroupedObservationEnum.Regularity.Day:
expectedDate = now.AddDays(-i);
if (!time.HasValue || expectedDate.Day != time.Value.Day) expectedMatch = false;
break;
case GroupedObservationEnum.Regularity.Hour:
case GroupedObservationEnum.Regularity.Shift:
expectedDate = now.AddHours(-i);
if (!time.HasValue || expectedDate.Hour != time.Value.Hour) expectedMatch = false;
break;
case GroupedObservationEnum.Regularity.Minute:
expectedDate = now.AddMinutes(-i);
if (!time.HasValue || expectedDate.Minute != time.Value.Minute) expectedMatch = false;
break;
case GroupedObservationEnum.Regularity.Second:
expectedDate = now.AddSeconds(-i);
if (!time.HasValue || expectedDate.Second != time.Value.Second) expectedMatch = false;
break;
}
if (!expectedMatch)
{
var newEntry = new BsonDocument
{
{
"_id", new BsonDocument
{
{ "year", expectedDate.Year },
{ "month", expectedDate.Month },
{ "day", expectedDate.Day },
{ "hour", expectedDate.Hour },
{ "minute", expectedDate.Minute },
{ "name", groupedField.Name }
}
},
{ "time", new BsonDateTime(expectedDate) }, //TODO loop del result entero
//{ Result.HalfHour.ToString().ToLower(), new BsonDocument { { "value", 0 }, { "min",0 }, { "max",0 }, { "time", new BsonDateTime(expectedDate) } } }
// { groupedField.regularity.ToString().ToLower(), new BsonDocument { { "value", 0 }, { "min",0 }, { "max",0 }, { "time", new BsonDateTime(expectedDate) } } }
{ "isFilled", true } //Marcamos como hora añadida
};
foreach (var r in groupedField.Result)
switch (r)
{
case GroupedObservationEnum.Result.LastFilled:
case GroupedObservationEnum.Result.LastAll:
continue;
case GroupedObservationEnum.Result.Sum:
case GroupedObservationEnum.Result.Average:
case GroupedObservationEnum.Result.Count:
newEntry.Add(new BsonElement(r.ToString().ToLower(), 0));
continue;
case GroupedObservationEnum.Result.HalfHour:
newEntry.Add("all",
new BsonArray
{
new BsonDocument
{
{ "value", 0 }, { "min", 0 }, { "max", 0 },
{ "time", new BsonDateTime(expectedDate) }
}
});
continue;
default:
newEntry.Add(r.ToString().ToLower(),
new BsonDocument
{
{ "value", 0 }, { "min", 0 }, { "max", 0 },
{ "time", new BsonDateTime(expectedDate) }
});
continue;
}
if (groupedField.Result.Count != 1 ||
!groupedField.Result.Contains(GroupedObservationEnum.Result.LastFilled))
filteredDocuments.Add(newEntry);
filteredDocuments = filteredDocuments.OrderBy(e => e.Get("time")?.ToLocalTime()).ToList();
}
inverseCounter++;
}
if (groupedField.Max <= filteredDocuments.Count)
{
var value = filteredDocuments.ElementAtOrDefault(inverseCounter);
if (value == null)
{
var newEntry = new BsonDocument
{
{
"_id", new BsonDocument
{
{ "year", DateTime.UtcNow.Year },
{ "month", DateTime.UtcNow.Month },
{ "day", DateTime.UtcNow.Day },
{ "hour", DateTime.UtcNow.Hour },
{ "minute", DateTime.UtcNow.Minute },
{ "name", groupedField.Name }
}
},
{ "time", new BsonDateTime(DateTime.UtcNow) }, //TODO loop del result entero
{ "isFilled", true } //Marcamos como hora añadida
};
foreach (var r in groupedField.Result)
switch (r)
{
case GroupedObservationEnum.Result.LastFilled:
case GroupedObservationEnum.Result.LastAll:
continue;
case GroupedObservationEnum.Result.Sum:
case GroupedObservationEnum.Result.Average:
case GroupedObservationEnum.Result.Count:
newEntry.Add(new BsonElement(r.ToString().ToLower(), 0));
continue;
case GroupedObservationEnum.Result.HalfHour:
newEntry.Add("all",
new BsonArray
{
new BsonDocument
{
{ "value", 0 }, { "min", 0 }, { "max", 0 },
{ "time", new BsonDateTime(expectedDate) }
}
});
continue;
default:
newEntry.Add(r.ToString().ToLower(),
new BsonDocument
{
{ "value", 0 }, { "min", 0 }, { "max", 0 },
{ "time", new BsonDateTime(expectedDate) }
});
continue;
}
if (groupedField.Result.Count != 1 ||
!groupedField.Result.Contains(GroupedObservationEnum.Result.LastFilled))
filteredDocuments.Add(newEntry);
filteredDocuments = filteredDocuments.OrderBy(e => e.Get("time")?.ToLocalTime()).ToList();
}
fillResult.AddRange(filteredDocuments);
}
}
fillResult = fillResult.OrderBy(e => e.Get("time")?.ToLocalTime()).ToList();
return fillResult;
}
public async Task<List<BsonDocument>> CalculateLastFilledObservations(List<BsonDocument> result,
GroupedField groupedField, ObjectId patientId)
{
List<BsonDocument> resultToReturn = [];
if (groupedField.Names.Any())
foreach (var name in groupedField.Names)
{
var resultFiltedByName = result.FindAll(b => b.Get("_id")?.AsBsonDocument.Get("name") == name);
var resultCalculated = await LastFilledCalc(resultFiltedByName, groupedField, patientId, name);
resultToReturn.AddRange(resultCalculated);
}
else
return await LastFilledCalc(result, groupedField, patientId, groupedField.Name);
return resultToReturn;
}
private async Task<List<BsonDocument>> LastFilledCalc(List<BsonDocument> result, GroupedField groupedField,
ObjectId patientId, string? name)
{
var now = DateTime.UtcNow;
if (result.Count == groupedField.Max)
return result; //We have data from each hour/day/minute, dont need to calc.
var first = result.FirstOrDefault();
object? lastObservationValue = null;
var expectedFirstDate = groupedField.Regularity switch
{
GroupedObservationEnum.Regularity.Day => now.AddDays(-groupedField.Max),
GroupedObservationEnum.Regularity.Hour => now.AddHours(-groupedField.Max),
GroupedObservationEnum.Regularity.Minute => now.AddMinutes(-groupedField.Max),
GroupedObservationEnum.Regularity.Second => now.AddSeconds(-groupedField.Max),
_ => DateTime.MinValue
};
//first > fecha minima tenemos que buscar el ultimo valor
if (first == null || first.Get("time")?.ToUniversalTime().CompareTo(expectedFirstDate) > 0)
{
var lastObservation =
await _observationRepository.FindLastObservationBeforeDate(patientId, groupedField.Name,
expectedFirstDate);
if (lastObservation != null) lastObservationValue = lastObservation.Value;
}
var expectedDate = DateTime.MinValue;
var inverseCounter = 0;
for (var i = groupedField.Max; i != 0; i--)
{
var expectedMatch = true;
var value = result.ElementAtOrDefault(inverseCounter);
DateTime? time = null;
if (value == null)
expectedMatch = false;
else
time = value.Get("time")?.ToUniversalTime();
switch (groupedField.Regularity)
{
case GroupedObservationEnum.Regularity.Day:
expectedDate = now.AddDays(-i);
if (!time.HasValue || expectedDate.Day != time.Value.Day) expectedMatch = false;
break;
case GroupedObservationEnum.Regularity.Hour:
expectedDate = now.AddHours(-i);
if (!time.HasValue || expectedDate.Hour != time.Value.Hour) expectedMatch = false;
break;
case GroupedObservationEnum.Regularity.Minute:
expectedDate = now.AddMinutes(-i);
if (!time.HasValue || expectedDate.Minute != time.Value.Minute) expectedMatch = false;
break;
case GroupedObservationEnum.Regularity.Second:
expectedDate = now.AddSeconds(-i);
if (!time.HasValue || expectedDate.Second != time.Value.Second) expectedMatch = false;
break;
}
if (!expectedMatch)
{
var lastResult = result.ElementAtOrDefault(inverseCounter - 1);
object? lastFilledValue;
if (lastResult == null)
{
lastFilledValue = lastObservationValue;
}
else
{
var lastfilled = GroupedObservationEnum.Result.LastFilled.ToString().ToLower();
lastFilledValue = lastResult.Get(lastfilled)?.AsBsonDocument.Get("value");
}
lastFilledValue ??= 0;
var newEntry = new BsonDocument
{
{
"_id", new BsonDocument
{
{ "year", expectedDate.Year },
{ "month", expectedDate.Month },
{ "day", expectedDate.Day },
{ "hour", expectedDate.Hour },
{ "minute", expectedDate.Minute },
{ "name", name }
}
},
{ "time", new BsonDateTime(expectedDate) },
{
GroupedObservationEnum.Result.LastFilled.ToString().ToLower(),
new BsonDocument
{
{ "value", lastFilledValue.ToString() }, { "min", 0 }, { "max", 0 },
{ "time", new BsonDateTime(expectedDate) }
}
}
};
result.Add(newEntry);
result = result.OrderBy(e => e.Get("time")?.ToLocalTime()).ToList();
}
inverseCounter++;
}
return result;
}
public List<BsonDocument> CalculateShiftObservations(List<BsonDocument> shiftGroupObservations,
GroupedField groupedField)
{
var shiftGrouped = new List<BsonDocument>();
if (groupedField.Result.Contains(GroupedObservationEnum.Result.Sum))
{
var shiftGroups =
shiftGroupObservations.GroupBy(shift => new
{ V1 = shift.GetElement("shift").ToString(), V = shift.GetElement("day").ToString() })
.Select(s => s.ToList())
.ToList();
shiftGroups.ForEach(shiftGroup =>
{
var shiftSum = shiftGroup.Select(shift => shift.Get("sum")?.ToInt32()).Sum();
var shiftToAdd = shiftGroup.FirstOrDefault();
shiftToAdd?.SetElement(new BsonElement("sum", shiftSum));
if (shiftGroup.Select(shift => shift.Get("isFilled")).Any(s => s == false))
shiftToAdd?.SetElement(new BsonElement("isFilled", false));
if (shiftToAdd != null)
shiftGrouped.Add(shiftToAdd);
});
return shiftGrouped;
}
return shiftGroupObservations;
}
//MIGUEL: EJEMPLO SI LAS 12 TIENEN 4 VALORES 12:15 => 5, 12:20 => 10, 12:45 => 20 Y LAS 13 NO TIENE VALOR. 12 DEBERÍA 5 Y 13 DEBERÍA SER
// ACTUALMENTE A LAS 12 MOSTRARIA EL ULTIMO DE LA MEDIA HORA Y A LAS 13?
public List<BsonDocument> CalculateHalfHourObservations(List<BsonDocument> result)
{
//HalfHour añadir que se rellenen las horas que no existen
for (var i = 0; i < result.Count; i++)
{
var document = result[i];
BsonDocument? nextDocument = null;
if (i + 1 < result.Count)
nextDocument = result[i + 1];
document.TryGetValue("all", out var docvalues);
List<BsonValue?> groups = [];
if (docvalues != null)
groups = docvalues.AsBsonArray.ToList().GroupBy(x =>
{
var stamp = x.AsBsonDocument.Get("time")?.ToLocalTime();
if (!stamp.HasValue) return null;
stamp = stamp.Value.AddMinutes(-(stamp.Value.Minute % 30));
stamp = stamp.Value.AddMilliseconds(-stamp.Value.Millisecond - 1000 * stamp.Value.Second);
return stamp;
}).Select(g =>
g.OrderByDescending(c => c.AsBsonDocument.Get("time")?.ToLocalTime()).FirstOrDefault())
.ToList();
if (groups.Count > 0)
{
if (nextDocument == null)
{
document.Set("halfhour", groups.Last());
break;
}
if (groups.Count > 1)
{
var nextDocumentValues = nextDocument.Get("all")?.AsBsonArray.ToList();
if (nextDocumentValues == null || nextDocumentValues.Count == 0
|| (nextDocumentValues.Count == 1 &&
nextDocument.Get("all")?.AsBsonArray.ToList()[0].AsBsonDocument
.Get("value")?.ToString() ==
"0")) //El siguiente registro no tiene tomas
{
nextDocument.Set("halfhour", groups.LastOrDefault());
document.Set("halfhour", groups.FirstOrDefault());
}
else
{
document.Set("halfhour", groups.LastOrDefault());
}
}
else
{
if (document.Get("halfhour")?.AsBsonDocument == null)
document.Set("halfhour", groups.LastOrDefault());
}
}
else
{
if (!document.TryGetValue("halfhour", out _)) document.Set("halfhour", new BsonDocument());
}
}
return result;
}
public List<BsonDocument> GenerateShiftObservations(List<BsonDocument> result, GroupedField groupedField)
{
result.ForEach(item =>
{
DateTime? obsDateTime = null;
var id = item.Get("_id")?.AsBsonDocument;
if (id == null)
return;
var timeStr = id.Get("hour")?.ToString()?.PadLeft(2, '0') ?? "00";
if (id.TryGetValue("minute", out _))
{
timeStr += ":" + id.Get("minute")?.ToString()?.PadLeft(2, '0');
if (DateTime.TryParseExact(timeStr, "HH:mm", CultureInfo.InvariantCulture, DateTimeStyles.None,
out var r1))
obsDateTime = r1;
}
else
{
if (DateTime.TryParseExact(timeStr, "HH", CultureInfo.InvariantCulture, DateTimeStyles.None,
out var r2))
obsDateTime = r2;
}
obsDateTime = obsDateTime?.ToLocalTime();
int shift;
int day;
//Return all dates in shift array before obsDateTime, and take last, that will be the closest shift date.
var expectedHour = obsDateTime?.Hour.ToString().Length < 2
? $"0{obsDateTime.Value.Hour}"
: obsDateTime?.Hour.ToString();
var expectedMinutes = obsDateTime?.Minute.ToString().Length < 2
? $"0{obsDateTime.Value.Minute}"
: obsDateTime?.Minute.ToString();
var shiftSelected = groupedField.StartTimeShift.LastOrDefault
(i => DateTime.ParseExact(i, "HH:mm", CultureInfo.InvariantCulture).Ticks <= DateTime
.ParseExact($"{expectedHour}:{expectedMinutes}", "HH:mm", CultureInfo.InvariantCulture)
.Ticks);
var havemin = id.TryGetValue("minute", out var minuteV);
var year = id.Get("year")?.ToInt32();
var month = id.Get("month")?.ToInt32();
var dayy = id.Get("day")?.ToInt32();
var hour = id.Get("hour")?.ToInt32();
if (!year.HasValue || !month.HasValue || !dayy.HasValue || !hour.HasValue)
{
_logger.LogError("Error getting dateTime from Bson: {id}", id);
return;
}
var dateUtc = new DateTime(year.Value, month.Value, dayy.Value, hour.Value, havemin ? minuteV.ToInt32() : 0,
0);
if (!groupedField.StartTimeShift.Any())
{
_logger.LogError(
"Error generating shift observations. StartTimeShift List in null or empty. {groupedField}",
groupedField);
return;
}
if (shiftSelected == null)
{
day = dateUtc.ToLocalTime().AddDays(-1).Day;
shift = groupedField.StartTimeShift.Count - 1;
}
else
{
day = dateUtc.ToLocalTime().Day;
shift = groupedField.StartTimeShift.IndexOf(shiftSelected);
}
item.Add(new BsonElement("day", day));
item.Add(new BsonElement("shift", shift));
});
return result;
}
// PRIVATE: Incremental item adjustment
private List<GroupedObservationObs> LocateCurrentObsInGroup(List<GroupedObservationObs> group,
PatientObservation obs, GroupedField groupedField)
{
// Check if we need to modify the current value or add new one
var specifyMinutes =
groupedField.Regularity is GroupedObservationEnum.Regularity.Minute
or GroupedObservationEnum.Regularity.Second;
var specifySeconds = groupedField.Regularity == GroupedObservationEnum.Regularity.Second;
var obsTime = new DateTime(obs.Time.Year, obs.Time.Month, obs.Time.Day, obs.Time.Hour,
specifyMinutes ? obs.Time.Minute : 0,
specifySeconds ? obs.Time.Second : 0);
var obsIndexInThisHour = group.FindIndex(c =>
new DateTime(c.Time.Year, c.Time.Month, c.Time.Day, c.Time.Hour,
specifyMinutes ? c.Time.Minute : 0,
specifySeconds ? c.Time.Second : 0) == obsTime && c.Name == obs.Name);
if (obsIndexInThisHour >= 0)
{
// Obs to modify found
var valueIsParsed = double.TryParse(obs.Value.ToString(), out var valueParsed);
if (!valueIsParsed)
return group;
group[obsIndexInThisHour].IsFilled = false;
group[obsIndexInThisHour].Time = obsTime;
foreach (var result in groupedField.Result)
switch (result)
{
case GroupedObservationEnum.Result.Max:
var valueMaxIsParsed = double.TryParse(group[obsIndexInThisHour].Max?.Value.ToString(),
out var valueMaxParsed);
if (!valueMaxIsParsed) continue;
//actual value is higher, is relevant
if (group[obsIndexInThisHour].Max?.Value != null && valueParsed > valueMaxParsed)
{
group[obsIndexInThisHour].Max!.Value = obs.Value;
group[obsIndexInThisHour].Max!.Time = obs.Time;
group[obsIndexInThisHour].Max!.Type = obs.Status;
group[obsIndexInThisHour].Max!.Type = obs.Status;
}
break;
case GroupedObservationEnum.Result.Min:
var valueMinIsParsed = double.TryParse(group[obsIndexInThisHour].Max?.Value.ToString(),
out var valueMinParsed);
if (!valueMinIsParsed) continue;
if (group[obsIndexInThisHour].Max?.Value != null && valueParsed < valueMinParsed)
{
group[obsIndexInThisHour].Min!.Value = obs.Value;
group[obsIndexInThisHour].Min!.Time = obs.Time;
group[obsIndexInThisHour].Min!.Type = obs.Status;
}
break;
case GroupedObservationEnum.Result.Last:
if (group[obsIndexInThisHour].Last?.Time < obs.Time)
{
group[obsIndexInThisHour].Last!.Value = obs.Value;
group[obsIndexInThisHour].Last!.Time = obs.Time;
group[obsIndexInThisHour].Last!.Type = obs.Status;
}
break;
case GroupedObservationEnum.Result.LastFilled:
if (group[obsIndexInThisHour].LastFilled?.Time < obs.Time)
{
group[obsIndexInThisHour].LastFilled!.Value = obs.Value;
group[obsIndexInThisHour].LastFilled!.Time = obs.Time;
group[obsIndexInThisHour].LastFilled!.Type = obs.Status;
}
break;
}
}
else
{
// Delete obs autofilled and in time of the new obs
var obsIndexInThisHourToRemove = group.FindIndex(c =>
new DateTime(c.Time.Year, c.Time.Month, c.Time.Day, c.Time.Hour,
specifyMinutes ? c.Time.Minute : 0,
specifySeconds ? c.Time.Second : 0) == obsTime && c.IsFilled);
if (obsIndexInThisHourToRemove >= 0)
group.RemoveAt(obsIndexInThisHourToRemove);
// Obs to modify not found add it
var ngo = new GroupedObservationObs
{
Name = obs.Name ?? string.Empty,
MinAlert = obs.Min,
MaxAlert = obs.Max,
Time = obsTime,
IsFilled = false,
ShiftDate = obs.Time,
Shift = GetShift(obs.Time, groupedField.StartTimeShift)
};
foreach (var result in groupedField.Result)
switch (result)
{
case GroupedObservationEnum.Result.Max:
ngo.Max = new GroupedObservationObsValue(obs.Value, obs.Time, obs.Status);
break;
case GroupedObservationEnum.Result.Min:
ngo.Min = new GroupedObservationObsValue(obs.Value, obs.Time, obs.Status);
break;
case GroupedObservationEnum.Result.Last:
ngo.Last = new GroupedObservationObsValue(obs.Value, obs.Time, obs.Status);
break;
case GroupedObservationEnum.Result.LastFilled:
ngo.LastFilled = new GroupedObservationObsValue(obs.Value, obs.Time, obs.Status);
break;
}
group.Add(ngo);
}
// Check if we have more obs than we require to show
DateTime dateLimit;
switch (groupedField.Regularity)
{
case GroupedObservationEnum.Regularity.Minute:
dateLimit = new DateTime(obs.Time.Year, obs.Time.Month, obs.Time.Day, obs.Time.Hour, obs.Time.Minute, 0)
.AddMinutes(groupedField.Max * -1);
break;
case GroupedObservationEnum.Regularity.Second:
dateLimit = new DateTime(obs.Time.Year, obs.Time.Month, obs.Time.Day, obs.Time.Hour, obs.Time.Minute,
obs.Time.Second).AddSeconds(groupedField.Max * -1);
break;
default:
dateLimit =
new DateTime(obs.Time.Year, obs.Time.Month, obs.Time.Day, obs.Time.Hour, 0, 0).AddHours(
groupedField.Max * -1);
break;
}
return group.Where(c =>
new DateTime(c.Time.Year, c.Time.Month, c.Time.Day, c.Time.Hour, specifyMinutes ? c.Time.Minute : 0,
specifySeconds ? c.Time.Second : 0) >= dateLimit).ToList();
}
// PRIVATE: Low-level utilities
private int? GetShift(DateTime currentObsDate, List<string> groupedFieldStartTimeShift)
{
if (groupedFieldStartTimeShift.Count == 0) return null;
var listTime = GetTimeSpan(groupedFieldStartTimeShift);
var dateTimeSpan = currentObsDate.TimeOfDay;
for (var i = 0; i < listTime.Count; i++)
{
var nextIndex = (i + 1) % listTime.Count;
var nextTimeSpan = listTime[nextIndex];
var currentTimeSpan = listTime[i];
if (dateTimeSpan >= currentTimeSpan && dateTimeSpan < nextTimeSpan) return i;
}
return -1;
}
private List<TimeSpan> GetTimeSpan(List<string> groupedFieldStartTimeShift)
{
List<TimeSpan> timeSpans = [];
timeSpans.AddRange(groupedFieldStartTimeShift.Select(TimeSpan.Parse));
List<TimeSpan> newTimeSpans = [];
for (var i = 0; i < timeSpans.Count; i++)
{
var nextIndex = (i + 1) % timeSpans.Count;
var nextTimeSpan = timeSpans[nextIndex];
var currentTimeSpan = timeSpans[i];
var addedTimeSpan = currentTimeSpan.Add(TimeSpan.FromMinutes(1));
newTimeSpans.Add(addedTimeSpan < nextTimeSpan ? addedTimeSpan : nextTimeSpan);
}
return newTimeSpans;
}
private async Task<StatusEnum.Type> GroupedObservationStatus(GroupedField groupedField,
GroupedObservationEnum.Result result, string name, object value, double? min, double? max)
{
return await _configObservationService.GroupedObservationStatus(groupedField, result, name, value, min, max);
}
private (int timesLeft, int multiplierFactor) TimesLeftToCreateEmptyObs(GroupedObservationObs obs, GroupedField gf)
{
var currentDateTime = DateTime.Now;
var timeElapsed = currentDateTime - obs.Time;
double timeToCurrent;
var multipliesFactor = 1;
// Calcula el tiempo transcurrido en la unidad deseada
switch (gf.Regularity)
{
case GroupedObservationEnum.Regularity.Second:
timeToCurrent = timeElapsed.TotalSeconds;
break;
case GroupedObservationEnum.Regularity.Minute:
timeToCurrent = timeElapsed.TotalMinutes;
multipliesFactor = 60;
break;
case GroupedObservationEnum.Regularity.Day:
timeToCurrent = timeElapsed.TotalDays;
multipliesFactor = 86400;
break;
default:
timeToCurrent = timeElapsed.TotalHours;
multipliesFactor = 3600;
break;
}
var timesLeft = (int)Math.Ceiling(timeToCurrent);
return (timesLeft, multipliesFactor);
}
private GroupedObservationObs CreateNextObs(GroupedObservationObs sourceObs, GroupedField gf, int seconsToAdd)
{
var lastObs = new GroupedObservationObs
{
Time = sourceObs.Time.AddSeconds(seconsToAdd),
Name = sourceObs.Name,
IsFilled = true
};
//Log.Debug("Result list:", string.Join(',', Result));
//Se rellena el tiempo con la hora de la obs anterior por que no hay dato de la actual
GroupedObservationObsValue defaultValue = new(0, sourceObs.Time);
foreach (var r in gf.Result)
{
//Log.Debug("Processing {r}", r.ToString());
//var props = typeof(GroupedObservationObs).GetProperties();
//Log.Debug("GroupedObservationObs Poperties list: {list}", string.Join(',', props.ToList()));
var propInfo = typeof(GroupedObservationObs).GetProperty(r.ToString());
if (propInfo == null)
{
_logger.LogError("Error Creating Next Obs. Getting property: {R} not found. SourceObs:{Obs}",
r.ToString(), sourceObs);
continue;
}
propInfo.SetValue(lastObs, defaultValue);
var gObsValue = propInfo.GetValue(sourceObs);
if (gObsValue == null)
{
_logger.LogWarning("Error Creating Next Obs.Getting property value: {R} not found. SourceObs:{Obs}",
r.ToString(), sourceObs);
continue;
}
var prevValue = (GroupedObservationObsValue)gObsValue;
prevValue.Time = sourceObs.Time;
propInfo.SetValue(sourceObs, prevValue);
}
if (gf.Regularity == GroupedObservationEnum.Regularity.Shift)
{
lastObs.ShiftDate = lastObs.Time;
lastObs.Shift = GetShift(lastObs.ShiftDate, gf.StartTimeShift);
}
return lastObs;
}
}