157 lines
3.4 KiB
Go
157 lines
3.4 KiB
Go
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
|
|
}
|