Files
renamer/internal/insert/positions.go

112 lines
3.0 KiB
Go

package insert
import (
"errors"
"fmt"
"strings"
"unicode/utf8"
)
// Position represents a resolved rune index relative to the filename stem.
type Position struct {
Index int // zero-based index where insertion should occur
}
// ResolvePosition interprets a position token (`^`, `$`, forward indexes, suffix offsets like `N$`, or legacy negative values) against the stem length.
func ResolvePosition(token string, stemLength int) (Position, error) {
if token == "" {
return Position{}, errors.New("position token cannot be empty")
}
switch token {
case "^":
return Position{Index: 0}, nil
case "$":
return Position{Index: stemLength}, nil
}
if strings.HasPrefix(token, "^") {
trimmed := token[1:]
if trimmed == "" {
return Position{Index: 0}, nil
}
value, err := parseInt(trimmed)
if err != nil {
return Position{}, fmt.Errorf("invalid position token %q: %w", token, err)
}
if value < 0 {
return Position{}, fmt.Errorf("invalid position token %q: cannot be negative", token)
}
if value > stemLength {
return Position{}, fmt.Errorf("position %d out of range for %d-character stem", value, stemLength)
}
return Position{Index: value}, nil
}
if strings.HasSuffix(token, "$") {
trimmed := token[:len(token)-1]
if trimmed == "" {
return Position{Index: stemLength}, nil
}
value, err := parseInt(trimmed)
if err != nil {
return Position{}, fmt.Errorf("invalid position token %q: %w", token, err)
}
if value < 0 {
return Position{}, fmt.Errorf("invalid position token %q: cannot be negative", token)
}
if value > stemLength {
return Position{}, fmt.Errorf("position %d out of range for %d-character stem", value, stemLength)
}
return Position{Index: stemLength - value}, nil
}
// Try parsing as integer (positive or negative).
idx, err := parseInt(token)
if err != nil {
return Position{}, fmt.Errorf("invalid position token %q: %w", token, err)
}
if idx > 0 {
if idx > stemLength {
return Position{}, fmt.Errorf("position %d out of range for %d-character stem", idx, stemLength)
}
return Position{Index: idx}, nil
}
// Negative index counts backward from end (e.g., -1 inserts before last rune).
offset := stemLength + idx
if offset < 0 || offset > stemLength {
return Position{}, fmt.Errorf("position %d out of range for %d-character stem", idx, stemLength)
}
return Position{Index: offset}, nil
}
// CountRunes returns the number of Unicode code points in name.
func CountRunes(name string) int {
return utf8.RuneCountInString(name)
}
// parseInt is declared separately for test stubbing.
var parseInt = func(token string) (int, error) {
var sign int = 1
switch token[0] {
case '+':
token = token[1:]
case '-':
sign = -1
token = token[1:]
}
if token == "" {
return 0, errors.New("missing digits")
}
var value int
for _, r := range token {
if r < '0' || r > '9' {
return 0, errors.New("non-numeric character in token")
}
value = value*10 + int(r-'0')
}
return sign * value, nil
}