Text Statistics & Readability — C# source
Count words, sentences, paragraphs, characters, lines, and reading time, plus Flesch Reading Ease and Flesch-Kincaid grade-level readability scores.
This is the C# implementation — the same logic the interactive tool runs, in a shareable, citable form.
// text-stats — text statistics & readability: chars, words, sentences, syllables, Flesch. Language: C# (.NET 7+). Port of src/lib/textStats.ts — string.Length counts UTF-16 code units exactly like the TS reference.
using System;
using System.Globalization;
using System.Linq;
using System.Text.RegularExpressions;
namespace TextStats;
/// <summary>Full analysis — readability stays null when words or sentences are too few.</summary>
public sealed record TextStats(int Characters, int CharactersNoSpaces, int Words, int Sentences,
int Paragraphs, int Lines, int Syllables, int ReadingTimeMs, int SpeakingTimeMs,
double? FleschReadingEase, double? FleschKincaidGrade, string? ReadabilityLabel);
public static class TextStatsTool
{
static readonly Regex WordRe = new("[A-Za-z0-9'’-]+"); // U+2019 keeps contractions whole
static readonly Regex SentenceRe = new("[.!?]+(\\s|$)");
static readonly Regex ParaSepRe = new("\\n{2,}");
static double JsRound(double x) => Math.Floor(x + 0.5); // JS Math.round: ties toward +infinity
/// <summary>Syllables in one word via the vowel-group heuristic.</summary>
public static int CountSyllables(string word)
{
string w = new(word.ToLowerInvariant().Where(c => c is >= 'a' and <= 'z').ToArray());
if (w.Length == 0) return 0;
if (w.Length <= 3) return 1;
string s = Regex.Replace(w, "(?:[^laeiouy]es|ed|[^laeiouy]e)$", ""); // silent trailing e ('~le' keeps its syllable)
s = Regex.Replace(s, "^y", "");
return Math.Max(1, Regex.Matches(s, "[aeiouy]+").Count);
}
public static TextStats Analyze(string? input)
{
string text = input ?? "";
string[] words = WordRe.Matches(text).Select(m => m.Value).ToArray();
int sentences = words.Length == 0 ? 0 : Math.Max(1, SentenceRe.Matches(text).Count);
int paragraphs = text.Trim().Length == 0
? 0 : ParaSepRe.Split(text).Count(p => p.Trim().Length > 0);
int lines = text == "" ? 0 : text.Split('\n').Length;
int syllables = words.Sum(CountSyllables);
int readingMs = (int)JsRound(words.Length / 200.0 * 60000); // 200 wpm
int speakingMs = (int)JsRound(words.Length / 130.0 * 60000); // 130 wpm
double? fre = null, fkg = null; string? label = null;
if (words.Length > 0 && sentences > 0)
{
double wps = (double)words.Length / sentences, spw = (double)syllables / words.Length;
fre = JsRound((206.835 - 1.015 * wps - 84.6 * spw) * 10) / 10;
fkg = JsRound((0.39 * wps + 11.8 * spw - 15.59) * 10) / 10;
label = fre >= 80 ? "Very Easy" : fre >= 70 ? "Easy" : fre >= 60 ? "Standard"
: fre >= 50 ? "Fairly Hard" : fre >= 30 ? "Hard" : "Very Hard";
}
return new(text.Length, Regex.Replace(text, "\\s", "").Length, words.Length, sentences,
paragraphs, lines, syllables, readingMs, speakingMs, fre, fkg, label);
}
}
public class Demo
{
public static void Main()
{
foreach (string text in new[] { "The quick brown fox jumps over the lazy dog.",
"Hi.\n\nMy name is Inigo Montoya. You killed my father; prepare to die!" })
{
TextStats s = TextStatsTool.Analyze(text);
string flesch = s.ReadabilityLabel is null ? "n/a"
: string.Create(CultureInfo.InvariantCulture, $"{s.FleschReadingEase:F1} ({s.ReadabilityLabel})");
string grade = s.FleschKincaidGrade is null ? "n/a"
: string.Create(CultureInfo.InvariantCulture, $"{s.FleschKincaidGrade:F1}");
Console.WriteLine($"chars={s.Characters} nospace={s.CharactersNoSpaces} words={s.Words} " +
$"sentences={s.Sentences} paragraphs={s.Paragraphs} lines={s.Lines} syllables={s.Syllables} " +
$"reading={s.ReadingTimeMs}ms speaking={s.SpeakingTimeMs}ms flesch={flesch} grade={grade}");
}
}
}
Also available in 13 other languages
Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →