// Package textutil holds the small string operations that every layer of this
// service needs and none of them owns.
package textutil

import "unicode/utf8"

// Truncate cuts s to at most max bytes, on a rune boundary.
//
// Byte-counted because the limits it enforces are byte limits: a database
// column, an issue tracker's field cap, a message a card must still render. But
// cutting a byte at a time slices multi-byte runes in half, and alert text is
// routinely Chinese — a summary is a few hundred characters long before it is
// anywhere near a two-thousand-byte cap, so the cut lands mid-character far more
// often than not. What comes out is not valid UTF-8, and a strict-mode TiDB
// refuses to store it: the write fails with "Incorrect string value", the caller
// retries with the same bytes, and the result is a value that can never be
// written at all. Dropping the partial rune costs at most three bytes and turns
// that into a clean truncation.
//
// A non-positive max returns the empty string, so a caller that computes its
// limit cannot accidentally ask for a negative slice.
func Truncate(s string, max int) string {
	if max <= 0 {
		return ""
	}
	if len(s) <= max {
		return s
	}
	// Walk back off the trailing continuation bytes. At most three steps: a
	// UTF-8 rune is four bytes at the widest.
	for max > 0 && !utf8.RuneStart(s[max]) {
		max--
	}
	return s[:max]
}
