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 }