Skip to content

Word & Character Counter — Swift source

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

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

// word-counter — Swift port: word / character / sentence / paragraph counts + 200-wpm reading-time estimate.
//
// Display snippet: ports the counters from the TypeScript lib (src/lib/wordCount.ts).
// String.count counts Characters (grapheme clusters); the TS reference counts
// UTF-16 code units — the two agree for all BMP text.
import Foundation

enum WordCounter {
    // Words: split on whitespace-and-newlines, dropping empty pieces.
    // CharacterSet.whitespacesAndNewlines covers the JS /\s/ class.
    static func countWords(_ t: String) -> Int {
        t.components(separatedBy: .whitespacesAndNewlines)
            .filter { !$0.isEmpty }
            .count
    }

    // Total character count, whitespace included.
    static func countChars(_ t: String) -> Int { t.count }

    // Character count with all whitespace removed.
    static func countCharsNoSpaces(_ t: String) -> Int {
        t.filter { !$0.isWhitespace }.count
    }

    // Sentences: maximal runs of non-terminators each closed by one or more
    // of . ! ? — the TS regex /[^.!?]+[.!?]+/g, as a single scan.
    static func countSentences(_ t: String) -> Int {
        var n = 0
        var body = false
        for ch in t {
            if ch == "." || ch == "!" || ch == "?" {
                if body { n += 1; body = false }
            } else {
                body = true
            }
        }
        return n
    }

    // Paragraphs: segments split by runs of 2+ newlines; blank segments
    // don't count (the TS split(/\n{2,}/).filter(s => s.trim())).
    static func countParagraphs(_ t: String) -> Int {
        var n = 0
        var run = 0
        var blank = true
        for ch in t {
            if ch == "\n" {
                run += 1
                if run == 2 {  // blank line closes the paragraph
                    if !blank { n += 1 }
                    blank = true
                }
                continue
            }
            run = 0
            if !ch.isWhitespace { blank = false }
        }
        return blank ? n : n + 1
    }

    // Reading time at 200 wpm: 0 for empty input, otherwise >= 1 minute.
    // (words + 100) / 200 reproduces round(words / 200.0).
    static func readingTimeMin(_ words: Int) -> Int {
        words <= 0 ? 0 : max(1, (words + 100) / 200)
    }
}

let text = "CosmoDev counts words. Live totals update as you type!\n\n" +
    "Second paragraph? Two sentences - each with words. Zero logins needed."
print("words:       \(WordCounter.countWords(text))")          // 20
print("chars:       \(WordCounter.countChars(text))")          // 126
print("no spaces:   \(WordCounter.countCharsNoSpaces(text))")  // 106
print("sentences:   \(WordCounter.countSentences(text))")      // 5
print("paragraphs:  \(WordCounter.countParagraphs(text))")     // 2
print("reading min: \(WordCounter.readingTimeMin(WordCounter.countWords(text)))") // 1

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 →