Skip to content

Word & Character Counter — Zig source

Live word, character, sentence, and paragraph counts plus reading-time estimate as you type.

This is the Zig implementation — the same logic the interactive tool runs, in a shareable, citable form.

// word-counter — Zig port: word / character / sentence / paragraph counts + 200-wpm reading-time estimate.
// Display snippet: ports the counters from the TypeScript lib (src/lib/wordCount.ts). Slices count
// bytes, so multi-byte UTF-8 diverges from the TS reference's UTF-16 units; whitespace is the ASCII
// subset of the JS /\s/ class.
const std = @import("std");

fn isSpace(c: u8) bool {
    return c == ' ' or c == '\t' or c == '\n' or c == 0x0B or c == 0x0C or c == '\r';
}

/// Words: runs of non-whitespace. Empty or whitespace-only input -> 0.
pub fn countWords(t: []const u8) usize {
    var n: usize = 0;
    var in_word = false;
    for (t) |c| {
        if (isSpace(c)) { in_word = false; } else if (!in_word) { in_word = true; n += 1; }
    }
    return n;
}

/// Total character count, whitespace included (bytes).
pub fn countChars(t: []const u8) usize {
    return t.len;
}

/// Character count with every whitespace byte removed.
pub fn countCharsNoSpaces(t: []const u8) usize {
    var n: usize = 0;
    for (t) |c| { if (!isSpace(c)) n += 1; }
    return n;
}
/// Sentences: non-terminator runs closed by one or more of . ! ? — the TS /[^.!?]+[.!?]+/g.
pub fn countSentences(t: []const u8) usize {
    var n: usize = 0;
    var body = false;
    for (t) |c| {
        if (c == '.' or c == '!' or c == '?') {
            if (body) { n += 1; body = false; }
        } else body = true;
    }
    return n;
}
/// Paragraphs: segments split by runs of 2+ newlines; blank segments don't count.
pub fn countParagraphs(t: []const u8) usize {
    var n: usize = 0;
    var run: usize = 0;
    var blank = true;
    for (t) |c| {
        if (c == '\n') {
            run += 1;
            if (run == 2) { if (!blank) n += 1; blank = true; } // blank line closes a paragraph
            continue;
        }
        run = 0;
        if (!isSpace(c)) blank = false;
    }
    return if (blank) n else n + 1;
}

/// Reading time at 200 wpm: 0 for empty, else >= 1 min; (w + 100) / 200 == Math.round.
pub fn readingTimeMin(words: i64) i64 {
    if (words <= 0) return 0;
    const m = @divTrunc(words + 100, 200);
    return if (m < 1) 1 else m;
}

pub fn main() !void {
    var bw = std.io.bufferedWriter(std.io.getStdOut().writer());
    const out = bw.writer();
    const text = "CosmoDev counts words. Live totals update as you type!\n\n" ++
        "Second paragraph? Two sentences - each with words. Zero logins needed.";
    const w = countWords(text);
    try out.print("words:       {d}\n", .{w});
    try out.print("chars:       {d}\n", .{countChars(text)});
    try out.print("no spaces:   {d}\n", .{countCharsNoSpaces(text)});
    try out.print("sentences:   {d}\n", .{countSentences(text)});
    try out.print("paragraphs:  {d}\n", .{countParagraphs(text)});
    try out.print("reading min: {d}\n", .{readingTimeMin(@intCast(w))});
    try bw.flush();
}

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 →