PlotDirector/PlotLine/Services/ManuscriptTextStatisticsService.cs

216 lines
6.8 KiB
C#

using PlotLine.Data;
using PlotLine.Models;
namespace PlotLine.Services;
public interface IManuscriptTextStatisticsService
{
ManuscriptTextStatistics Calculate(string? sourceText);
ManuscriptTextStatistics Aggregate(IEnumerable<ManuscriptTextStatistics?> statistics);
Task RecalculateSceneAsync(int sceneId);
Task RecalculateChapterAsync(int chapterId);
Task RecalculateBookAsync(int bookId);
}
public sealed class ManuscriptTextStatisticsService(
IManuscriptTextStatisticsRepository repository,
IOnboardingBuildRepository manuscriptSources) : IManuscriptTextStatisticsService
{
public ManuscriptTextStatistics Calculate(string? sourceText)
{
var paragraphs = StoryIntelligenceParagraphs.Split(sourceText);
var totalWords = 0;
var dialogueWords = 0;
var proseWords = 0;
var dialogueParagraphs = 0;
var proseParagraphs = 0;
var inDialogue = false;
foreach (var paragraph in paragraphs)
{
var paragraphStartInDialogue = inDialogue;
var paragraphHasDialogue = paragraphStartInDialogue;
var paragraphHasProse = false;
var firstContentIndex = FirstNonWhitespaceIndex(paragraph);
var segmentStart = 0;
for (var index = 0; index < paragraph.Length; index++)
{
var quote = QuoteKind(paragraph[index]);
if (quote == ManuscriptQuoteKind.None)
{
continue;
}
if (inDialogue && IsContinuationOpeningQuote(quote, index, firstContentIndex))
{
AddSegment(paragraph, segmentStart, index, inDialogue, ref totalWords, ref dialogueWords, ref proseWords, ref paragraphHasDialogue, ref paragraphHasProse);
segmentStart = index + 1;
continue;
}
AddSegment(paragraph, segmentStart, index, inDialogue, ref totalWords, ref dialogueWords, ref proseWords, ref paragraphHasDialogue, ref paragraphHasProse);
if (quote == ManuscriptQuoteKind.SmartOpen)
{
inDialogue = true;
paragraphHasDialogue = true;
}
else if (quote == ManuscriptQuoteKind.SmartClose)
{
inDialogue = false;
}
else
{
inDialogue = !inDialogue;
if (inDialogue)
{
paragraphHasDialogue = true;
}
}
segmentStart = index + 1;
}
AddSegment(paragraph, segmentStart, paragraph.Length, inDialogue, ref totalWords, ref dialogueWords, ref proseWords, ref paragraphHasDialogue, ref paragraphHasProse);
if (paragraphHasDialogue)
{
dialogueParagraphs++;
}
if (paragraphHasProse)
{
proseParagraphs++;
}
}
return new ManuscriptTextStatistics
{
TotalWordCount = totalWords,
DialogueWordCount = dialogueWords,
ProseWordCount = proseWords,
TotalParagraphCount = paragraphs.Count,
DialogueParagraphCount = dialogueParagraphs,
ProseParagraphCount = proseParagraphs
};
}
public ManuscriptTextStatistics Aggregate(IEnumerable<ManuscriptTextStatistics?> statistics)
{
var rows = statistics.Where(row => row is not null).Select(row => row!).ToList();
return new ManuscriptTextStatistics
{
TotalWordCount = rows.Sum(row => row.TotalWordCount),
DialogueWordCount = rows.Sum(row => row.DialogueWordCount),
ProseWordCount = rows.Sum(row => row.ProseWordCount),
TotalParagraphCount = rows.Sum(row => row.TotalParagraphCount),
DialogueParagraphCount = rows.Sum(row => row.DialogueParagraphCount),
ProseParagraphCount = rows.Sum(row => row.ProseParagraphCount)
};
}
public async Task RecalculateSceneAsync(int sceneId)
{
var source = await repository.GetSourceBySceneAsync(sceneId);
if (source is null)
{
return;
}
await repository.SaveAsync(source.SceneID, Calculate(source.SourceText));
}
public async Task RecalculateChapterAsync(int chapterId)
{
var sources = await manuscriptSources.ListSceneManuscriptSourcesByChapterAsync(chapterId);
foreach (var source in sources)
{
await repository.SaveAsync(source.SceneID, Calculate(source.SourceText));
}
}
public async Task RecalculateBookAsync(int bookId)
{
var sources = await manuscriptSources.ListSceneManuscriptSourcesByBookAsync(bookId);
foreach (var source in sources)
{
await repository.SaveAsync(source.SceneID, Calculate(source.SourceText));
}
}
private static void AddSegment(
string paragraph,
int start,
int end,
bool isDialogue,
ref int totalWords,
ref int dialogueWords,
ref int proseWords,
ref bool paragraphHasDialogue,
ref bool paragraphHasProse)
{
if (end <= start)
{
return;
}
var words = CountWords(paragraph[start..end]);
if (words == 0)
{
return;
}
totalWords += words;
if (isDialogue)
{
dialogueWords += words;
paragraphHasDialogue = true;
}
else
{
proseWords += words;
paragraphHasProse = true;
}
}
private static int CountWords(string value)
=> string.IsNullOrWhiteSpace(value)
? 0
: value.Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries).Length;
private static int FirstNonWhitespaceIndex(string paragraph)
{
for (var index = 0; index < paragraph.Length; index++)
{
if (!char.IsWhiteSpace(paragraph[index]))
{
return index;
}
}
return -1;
}
private static bool IsContinuationOpeningQuote(ManuscriptQuoteKind quote, int index, int firstContentIndex)
=> index == firstContentIndex
&& (quote == ManuscriptQuoteKind.SmartOpen || quote == ManuscriptQuoteKind.Straight);
private static ManuscriptQuoteKind QuoteKind(char value)
=> value switch
{
'"' => ManuscriptQuoteKind.Straight,
'\u201c' => ManuscriptQuoteKind.SmartOpen,
'\u201d' => ManuscriptQuoteKind.SmartClose,
_ => ManuscriptQuoteKind.None
};
private enum ManuscriptQuoteKind
{
None,
Straight,
SmartOpen,
SmartClose
}
}