Skip to content

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 →