Word & Character Counter — Go source
Live word, character, sentence, and paragraph counts plus reading-time estimate as you type.
This is the Go implementation — the same logic the interactive tool runs, in a shareable, citable form.
// Package wordcounter is the Go twin of CosmoDev's src/lib/wordCount.ts (dual
// source: the web lib is TypeScript, the CLI lib is Go — kept in lock-step).
// Pure + deterministic, never panics. The table-driven tests in
// wordcounter_test.go share vectors with src/lib/wordCount.test.ts so the two
// implementations are held to the same contract.
//
// Each exported function mirrors its TS counterpart 1:1: CountWords,
// CountChars, CountCharsNoSpaces, CountSentences, CountParagraphs,
// ReadingTimeMin. Character counts are measured in UTF-16 code units to match
// JavaScript's string .length, so a supplementary-plane rune (e.g. an emoji)
// counts as 2 — exactly as the TS lib reports.
package wordcounter
import (
"math"
"regexp"
"strings"
"unicode"
)
// A sentence is a run of non-terminal characters terminated by one or more of
// '.', '!', '?' — mirrors /[^.!?]+[.!?]+/g in the TS lib.
var sentenceRe = regexp.MustCompile(`[^.!?]+[.!?]+`)
// Paragraphs are separated by runs of two or more newlines — mirrors /\n{2,}/
// in the TS lib.
var paraSepRe = regexp.MustCompile(`\n{2,}`)
// utf16Len reports the length of s in UTF-16 code units: 1 per BMP rune, 2 per
// supplementary-plane rune. This is exactly what JavaScript's string .length
// returns, so CountChars/CountCharsNoSpaces agree with the TS lib on emoji and
// other astral characters (which JS counts as a 2-code-unit surrogate pair).
func utf16Len(s string) int {
n := 0
for _, r := range s {
if r >= 0x10000 {
n += 2
} else {
n++
}
}
return n
}
// CountWords returns the number of whitespace-separated words. Empty or
// whitespace-only input returns 0. It is the Go twin of countWords(): the TS
// lib does t.trim().split(/\s+/).filter(Boolean).length, which is exactly what
// strings.Fields yields (Unicode-whitespace tokenization with empties dropped).
func CountWords(t string) int {
return len(strings.Fields(t))
}
// CountChars returns the total character count including whitespace, measured
// in UTF-16 code units (JavaScript .length). It is the Go twin of countChars().
func CountChars(t string) int {
return utf16Len(t)
}
// CountCharsNoSpaces returns the character count excluding all whitespace,
// measured in UTF-16 code units. It is the Go twin of countCharsNoSpaces(): the
// TS lib does t.replace(/\s/g, '').length.
func CountCharsNoSpaces(t string) int {
n := 0
for _, r := range t {
if unicode.IsSpace(r) {
continue
}
if r >= 0x10000 {
n += 2
} else {
n++
}
}
return n
}
// CountSentences returns the number of sentences: non-empty text terminated by
// one or more of '.', '!', '?'. Text without terminal punctuation returns 0.
// It is the Go twin of countSentences().
func CountSentences(t string) int {
return len(sentenceRe.FindAllString(t, -1))
}
// CountParagraphs returns the number of non-empty paragraphs separated by one
// or more blank lines. Empty or whitespace-only input returns 0. It is the Go
// twin of countParagraphs(): the TS lib splits on /\n{2,}/ and keeps chunks
// that are non-empty after trimming.
func CountParagraphs(t string) int {
n := 0
for _, chunk := range paraSepRe.Split(t, -1) {
if strings.TrimSpace(chunk) != "" {
n++
}
}
return n
}
// ReadingTimeMin returns the estimated reading time in minutes at 200 words
// per minute. It returns 0 for zero or negative word counts, otherwise at least
// 1. Rounding is half-up for positive values, matching JavaScript's Math.round.
// It is the Go twin of readingTimeMin().
func ReadingTimeMin(words int) int {
if words <= 0 {
return 0
}
return max(1, int(math.Round(float64(words)/200.0)))
}
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 →