journal/Journal.Core/Services/Entries/EntrySearchService.cs

112 lines
4.5 KiB
C#

using Journal.Core.Dtos;
using System.Globalization;
namespace Journal.Core.Services.Entries;
public class EntrySearchService : IEntrySearchService
{
public Task<IReadOnlyList<EntrySearchResultDto>> SearchEntriesAsync(EntrySearchRequestDto request)
{
ArgumentNullException.ThrowIfNull(request);
if (string.IsNullOrWhiteSpace(request.DataDirectory))
throw new ArgumentException("Data directory is required.", nameof(request.DataDirectory));
if (!Directory.Exists(request.DataDirectory))
return Task.FromResult<IReadOnlyList<EntrySearchResultDto>>([]);
var hasQuery = !string.IsNullOrWhiteSpace(request.Query);
var query = request.Query?.Trim() ?? "";
var hasSectionFilter = !string.IsNullOrWhiteSpace(request.Section);
var section = request.Section?.Trim() ?? "";
var typeSet = NormalizeSet(request.Types);
var tagSet = NormalizeSet(request.Tags);
var checkedSet = NormalizeSet(request.Checked);
var uncheckedSet = NormalizeSet(request.Unchecked);
var hasFragmentFilters = typeSet.Count > 0 || tagSet.Count > 0;
var hasCheckboxFilters = checkedSet.Count > 0 || uncheckedSet.Count > 0;
var startDate = ParseOptionalDate(request.StartDate, nameof(request.StartDate));
var endDate = ParseOptionalDate(request.EndDate, nameof(request.EndDate));
if (startDate.HasValue && endDate.HasValue && startDate.Value > endDate.Value)
throw new ArgumentException("startDate cannot be after endDate.");
var results = new List<EntrySearchResultDto>();
foreach (var filePath in Directory.GetFiles(request.DataDirectory, "*.md")
.OrderBy(Path.GetFileName, StringComparer.Ordinal))
{
var fileName = Path.GetFileName(filePath);
if (EntryFileNaming.IsTemplateFileName(fileName))
continue;
var fileStem = Path.GetFileNameWithoutExtension(filePath);
var rawContent = File.ReadAllText(filePath);
var entry = JournalParser.ParseJournalContent(rawContent, fileStem);
if (startDate.HasValue || endDate.HasValue)
{
if (!DateOnly.TryParseExact(entry.Date, "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.None, out var entryDate))
continue;
if (startDate.HasValue && entryDate < startDate.Value)
continue;
if (endDate.HasValue && entryDate > endDate.Value)
continue;
}
var contentMatch = true;
if (hasQuery)
{
var haystack = hasSectionFilter ? entry.GetSection(section) : entry.RawContent;
contentMatch = haystack.IndexOf(query, StringComparison.OrdinalIgnoreCase) >= 0;
}
if (!contentMatch)
continue;
var fragmentMatch = !hasFragmentFilters || entry.Fragments.Any(fragment =>
(typeSet.Count == 0 || typeSet.Contains(fragment.Type)) &&
(tagSet.Count == 0 || fragment.Tags.Any(tagSet.Contains)));
if (!fragmentMatch)
continue;
var checkboxMatch = !hasCheckboxFilters || entry.Sections.Values.Any(sectionValue =>
sectionValue.Checkboxes.Any(checkbox =>
(checkedSet.Count > 0 && checkbox.Value && checkedSet.Contains(checkbox.Key)) ||
(uncheckedSet.Count > 0 && !checkbox.Value && uncheckedSet.Contains(checkbox.Key))));
if (!checkboxMatch)
continue;
results.Add(new EntrySearchResultDto(fileName, entry.ToDto()));
}
return Task.FromResult<IReadOnlyList<EntrySearchResultDto>>(results);
}
private static HashSet<string> NormalizeSet(IReadOnlyList<string>? values)
{
if (values is null || values.Count == 0)
return [];
var set = new HashSet<string>(StringComparer.Ordinal);
foreach (var value in values)
{
if (string.IsNullOrWhiteSpace(value))
continue;
set.Add(value.Trim());
}
return set;
}
private static DateOnly? ParseOptionalDate(string? raw, string argumentName)
{
if (string.IsNullOrWhiteSpace(raw))
return null;
if (DateOnly.TryParseExact(raw.Trim(), "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.None, out var date))
return date;
throw new ArgumentException($"Invalid {argumentName} value. Expected yyyy-MM-dd.");
}
}