Add trim selector parsing

This commit is contained in:
2026-05-08 14:41:47 +00:00
parent d865bda4a9
commit 2c82f8bf5c
2 changed files with 283 additions and 0 deletions

156
internal/trim/selector.go Normal file
View File

@@ -0,0 +1,156 @@
package trim
import (
"fmt"
"regexp"
"sort"
"strconv"
"strings"
)
var selectorElementPattern = regexp.MustCompile(`^([+-]?\d+)(?:\s*-\s*([+-]?\d+))?$`)
// Selector represents a normalized union of segment IDs.
type Selector struct {
ranges []idRange
}
type idRange struct {
start int
end int
}
// ParseSelector parses an inline segment selector expression.
func ParseSelector(input string) (Selector, error) {
if strings.TrimSpace(input) == "" {
return Selector{}, fmt.Errorf("selector cannot be empty")
}
parts := strings.Split(input, ",")
ranges := make([]idRange, 0, len(parts))
for index, raw := range parts {
element := strings.TrimSpace(raw)
if element == "" {
return Selector{}, fmt.Errorf("selector element %d cannot be empty", index+1)
}
rangeValue, err := parseElement(element)
if err != nil {
return Selector{}, fmt.Errorf("selector element %d %q: %w", index+1, element, err)
}
ranges = append(ranges, rangeValue)
}
normalized := normalizeRanges(ranges)
if len(normalized) == 0 {
return Selector{}, fmt.Errorf("selector cannot be empty")
}
return Selector{ranges: normalized}, nil
}
// Contains returns true when id is included by this selector.
func (s Selector) Contains(id int) bool {
if id <= 0 {
return false
}
index := sort.Search(len(s.ranges), func(i int) bool {
return s.ranges[i].end >= id
})
if index == len(s.ranges) {
return false
}
rangeValue := s.ranges[index]
return id >= rangeValue.start && id <= rangeValue.end
}
// IDs returns a deterministic ascending list of unique segment IDs.
func (s Selector) IDs() []int {
total := 0
for _, rangeValue := range s.ranges {
total += rangeValue.end - rangeValue.start + 1
}
ids := make([]int, 0, total)
for _, rangeValue := range s.ranges {
for id := rangeValue.start; id <= rangeValue.end; id++ {
ids = append(ids, id)
}
}
return ids
}
func parseElement(element string) (idRange, error) {
matches := selectorElementPattern.FindStringSubmatch(element)
if matches == nil {
return idRange{}, fmt.Errorf("malformed element")
}
start, err := parseID(matches[1])
if err != nil {
return idRange{}, err
}
if matches[2] == "" {
return idRange{start: start, end: start}, nil
}
end, err := parseID(matches[2])
if err != nil {
return idRange{}, fmt.Errorf("invalid range end: %w", err)
}
if start > end {
return idRange{}, fmt.Errorf("descending range %d-%d is invalid", start, end)
}
return idRange{start: start, end: end}, nil
}
func parseID(value string) (int, error) {
value = strings.TrimSpace(value)
if value == "" {
return 0, fmt.Errorf("missing segment ID")
}
id, err := strconv.Atoi(value)
if err != nil {
return 0, fmt.Errorf("segment ID must be an integer")
}
if id <= 0 {
return 0, fmt.Errorf("segment ID must be positive")
}
return id, nil
}
func normalizeRanges(in []idRange) []idRange {
if len(in) == 0 {
return nil
}
sorted := make([]idRange, len(in))
copy(sorted, in)
sort.Slice(sorted, func(i, j int) bool {
if sorted[i].start == sorted[j].start {
return sorted[i].end < sorted[j].end
}
return sorted[i].start < sorted[j].start
})
merged := make([]idRange, 0, len(sorted))
for _, next := range sorted {
if len(merged) == 0 {
merged = append(merged, next)
continue
}
last := &merged[len(merged)-1]
if next.start <= last.end+1 {
if next.end > last.end {
last.end = next.end
}
continue
}
merged = append(merged, next)
}
return merged
}