blob: 20d31f3052bfb187b311cad04daaf228a78928b1 [file] [log] [blame]
// Copyright 2021 The LUCI Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package copyonwrite providers helpers for modifying slices in Copy-on-Write way.
package copyonwrite
import (
"fmt"
"sort"
)
// Slice abstracts out copy-on-write slices of pointers to objects.
type Slice interface {
Len() int
At(index int) interface{}
Append(v interface{}) Slice
// CloneShallow returns new slice of the given capacity with elements copied
// from [:length] of this slice.
CloneShallow(length, capacity int) Slice
}
// SortedSlice is a sorted Slice.
type SortedSlice interface {
Slice
sort.Interface
// LessElements returns true if element `a` must be before `b`.
//
// Unlike sort.Interface's Less, this compares arbitrary elements.
//
// Why not use sort.Interface's Less?
// Because during merge phase of (newly created, updated) slices, it's
// necessary to compare elements from different slices. OK, OK, it's possible
// to move both elements to one slice and re-use Less. For example, a temp
// slice of 2 elements can be always allocated for this very purpose, or one
// can smartly re-use resulting slice for this. However, either way it's
// slower and more complicated than requiring boilerplate in each SortedSlice
// type, which can be re-used in Less() implementation.
LessElements(a, b interface{}) bool
}
// Deletion is special return value for Modifier to imply deletion.
var Deletion = deletion{}
type deletion struct{}
// Modifier takes a pointer to an existing COW object and returns:
// * special DeleteMe, if this object should be deleted from Slice;
// * the same object, if there are no modifications;
// * new object, if there were modifications. If working with SortedSlice,
// the new object must have the same sorting key.
type Modifier func(interface{}) interface{}
// Update modifies existing elements and adds new ones to Slice.
//
// If given Slice implements SortedSlice, assumes and preserves its sorted
// order.
//
// COWModifier, if not nil, is called on each existing object to modify or
// possible delete it.
//
// Sorts toAdd slice if using SortedSlice.
//
// Returns (new slice, true) if it was modified.
// Returns (old slice, false) otherwise.
func Update(in Slice, modifier Modifier, toAdd Slice) (Slice, bool) {
inLen := 0
if in != nil {
inLen = in.Len()
}
if modifier == nil {
modifier = noopModifier
}
cLen := 0
if toAdd != nil {
cLen = toAdd.Len()
}
_, isSorted := in.(SortedSlice)
if isSorted && cLen > 0 {
sortable, ok := toAdd.(SortedSlice)
if !ok {
panic(fmt.Errorf("Different types for in and toAdd slices: %T vs %T", in, toAdd))
}
sort.Sort(sortable)
}
switch {
case inLen == 0 && cLen == 0:
return in, false
case inLen == 0:
return toAdd, true
case cLen == 0:
return modifyRemaining(in, 0, modifier, nil)
case !isSorted:
return modifyRemaining(in, 0, modifier, toAdd)
}
// Merge two sorted sequences into `out` while modifying `in` elements at the
// same time.
less := in.(SortedSlice).LessElements
out := in.CloneShallow(0, inLen+cLen)
i, c := 0, 0
for {
if c == cLen {
return modifyRemaining(in, i, modifier, out)
}
if i == inLen {
for ; c < cLen; c++ {
out = out.Append(toAdd.At(c))
}
return out, true
}
iOld := in.At(i)
cNew := toAdd.At(c)
if less(cNew, iOld) {
c++
out = out.Append(cNew)
continue
}
i++
if iNew := modifier(iOld); iNew != Deletion {
out = out.Append(iNew)
}
}
}
func noopModifier(v interface{}) interface{} { return v }
func modifyRemaining(in Slice, i int, modifier Modifier, out Slice) (Slice, bool) {
l := in.Len()
for ; i < l; i++ {
before := in.At(i)
after := modifier(before)
if out == nil && after == before {
continue // no changes so far
}
if out == nil {
out = in.CloneShallow(i, l)
}
if after != Deletion {
out = out.Append(after)
}
}
if out == nil {
return in, false
}
return out, true
}