216 lines
6.8 KiB
C#
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
|
|
}
|
|
}
|