From 6702d50231831fa86d1c8f5e70f0425a5f50ad22 Mon Sep 17 00:00:00 2001 From: Daniel Gil Date: Tue, 29 May 2018 16:08:17 +0200 Subject: [PATCH] Added inclusive / exclusive functionality --- .gitignore | 2 +- example/data.txt | 1 + example/main.go | 140 ++++++++++++++++++++++++++++++++++++++++------- find.go | 12 +++- gaps.go | 32 ++++++++--- get.go | 3 + inclusives.go | 37 +++++++++++++ intervals.go | 36 +++++++++--- merge.go | 61 ++++++++++++++++----- overlap.go | 35 ++++++++---- sort.go | 1 + utils.go | 52 +++++++++++++++++- 12 files changed, 345 insertions(+), 67 deletions(-) create mode 100644 inclusives.go diff --git a/.gitignore b/.gitignore index fc7d8ea..491fc64 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,4 @@ debug.test example/debug -example/out.png +example/*.png diff --git a/example/data.txt b/example/data.txt index 7e1af8a..45d63cf 100644 --- a/example/data.txt +++ b/example/data.txt @@ -6,4 +6,5 @@ 20,30 25,28 30,32 +0,1 10,12 \ No newline at end of file diff --git a/example/main.go b/example/main.go index 4ab97ad..d609922 100644 --- a/example/main.go +++ b/example/main.go @@ -21,6 +21,26 @@ const ( MaxX = 40 ) +type PlotType int + +const ( + PlotTypeIntervals PlotType = iota + PlotTypeGaps + PlotTypeOverlapped + PlotTypeMerged +) + +var ( + openBracket = "[" + closeBracket = "]" + openParenthesis = "(" + closeParenthesis = ")" + intervalOpening = openBracket + intervalClosing = closeBracket + lowInclusive bool + highInclusive bool +) + func main() { filename := "data.txt" xys, err := readData(filename) @@ -47,12 +67,17 @@ func readData(path string) ([]xy, error) { // read line by line using a scanner (because we don't know if the file will be huge) s := bufio.NewScanner(f) for s.Scan() { - var x, y int - _, err := fmt.Sscanf(s.Text(), "%d,%d", &x, &y) + var low, high int + _, err := fmt.Sscanf(s.Text(), "%d,%d", &low, &high) if err != nil { log.Printf("discarding bad data point %v: %v", s.Text(), err) + continue } - xys = append(xys, xy{x, y}) + if low > high { + log.Printf("discarding bad data point (low, high)=(%v): low can not be greater than high", s.Text()) + continue + } + xys = append(xys, xy{low, high}) } if err := s.Err(); err != nil { return nil, fmt.Errorf("could not scan: %v", err) @@ -61,7 +86,24 @@ func readData(path string) ([]xy, error) { } func initIntervals(xys []xy) interval.Intervals { - intervals := interval.NewIntervals(MinX, MaxX) + intervals := interval.NewIntervals(MinX, MaxX, true, true) + + if intervals.IsLowInclusive() { + intervalOpening = "[" + lowInclusive = true + } else { + intervalOpening = "(" + lowInclusive = false + } + + if intervals.IsHighInclusive() { + intervalClosing = "]" + highInclusive = true + } else { + intervalClosing = ")" + highInclusive = false + } + for _, xy := range xys { intervals.Add(&interval.Interval{Low: xy.x, High: xy.y}) } @@ -93,7 +135,7 @@ func alignPlots(plotItems []*plot.Plot) *vgimg.Canvas { } } - img := vgimg.New(vg.Points(512), vg.Points(float64(128*rows))) + img := vgimg.New(vg.Points(512), vg.Points(float64(200*rows))) dc := draw.New(img) t := draw.Tiles{ @@ -129,7 +171,7 @@ func createFileFromCanvas(path string, img *vgimg.Canvas) error { return nil } -func createPlot(title string, xys plotter.XYs) (*plot.Plot, error) { +func createPlot(title string, xys plotter.XYs, plotType PlotType) (*plot.Plot, error) { p, err := plot.New() if err != nil { return nil, fmt.Errorf("could not create plot: %v", err) @@ -142,7 +184,7 @@ func createPlot(title string, xys plotter.XYs) (*plot.Plot, error) { // p.X.Label.Text = "values" p.X.Padding = vg.Length(5) p.Y.Padding = vg.Length(20) - plotIntervals(p, xys) + plotIntervals(p, plotType, xys) return p, nil } @@ -151,7 +193,7 @@ func plotData(path string, intervals interval.Intervals) error { // create Intervals plot xysIntervals := convertToPlotterXYs(intervals.Get()) - p1, err := createPlot("Intervals", xysIntervals) + p1, err := createPlot("Intervals", xysIntervals, PlotTypeIntervals) if err != nil { return fmt.Errorf("could not create plot: %v", err) } @@ -159,7 +201,7 @@ func plotData(path string, intervals interval.Intervals) error { // create Gaps plot xysGaps := convertToPlotterXYs(intervals.Gaps()) - p2, err := createPlot("Gaps", xysGaps) + p2, err := createPlot("Gaps", xysGaps, PlotTypeGaps) if err != nil { return fmt.Errorf("could not create plot: %v", err) } @@ -167,7 +209,7 @@ func plotData(path string, intervals interval.Intervals) error { // create Overlapped `plot xysOverlapped := convertToPlotterXYs(intervals.Overlapped()) - p3, err := createPlot("Overlapped", xysOverlapped) + p3, err := createPlot("Overlapped", xysOverlapped, PlotTypeOverlapped) if err != nil { return fmt.Errorf("could not create plot: %v", err) } @@ -175,7 +217,7 @@ func plotData(path string, intervals interval.Intervals) error { // create Merged plot xysMerged := convertToPlotterXYs(intervals.Merge()) - p4, err := createPlot("Merged", xysMerged) + p4, err := createPlot("Merged", xysMerged, PlotTypeMerged) if err != nil { return fmt.Errorf("could not create plot: %v", err) } @@ -190,16 +232,71 @@ func plotData(path string, intervals interval.Intervals) error { return nil } -func plotIntervals(p *plot.Plot, xys plotter.XYs) error { +func plotIntervals(p *plot.Plot, plotType PlotType, xys plotter.XYs) error { var ps []plot.Plotter colors := getColors() numColors := len(colors) + + crossShape := draw.CrossGlyph{} + ringShape := draw.RingGlyph{} + legendWithCrossShape := false + legendWithRingShape := false for i, xy := range xys { - label := fmt.Sprintf("(%v,%v)", xy.X, xy.Y) - pXYs := plotter.XYs{{xy.X, float64(i)}, {xy.Y, float64(i)}} + pXYLow := struct{ X, Y float64 }{xy.X, float64(i)} + pXYHigh := struct{ X, Y float64 }{xy.Y, float64(i)} + pXYs := plotter.XYs{pXYLow, pXYHigh} color := colors[i%numColors] - s, err := plotter.NewScatter(pXYs) + var s1, s2 *plotter.Scatter + var err error + label := "" + if plotType == PlotTypeIntervals { + label = fmt.Sprintf("%s%v,%v%s", intervalOpening, xy.X, xy.Y, intervalClosing) + s1, err = plotter.NewScatter(plotter.XYs{pXYLow}) + if err != nil { + return fmt.Errorf("unable to create new scatter: %v", err) + } + if !lowInclusive { + s1.GlyphStyle.Shape = crossShape + if !legendWithCrossShape { + p.Legend.Add("Exclusive", s1) + legendWithCrossShape = true + } + } else { + s1.GlyphStyle.Shape = ringShape + if !legendWithRingShape { + p.Legend.Add("Inclusive", s1) + legendWithRingShape = true + } + } + + s2, err = plotter.NewScatter(plotter.XYs{pXYHigh}) + if err != nil { + return fmt.Errorf("unable to create new scatter: %v", err) + } + if !highInclusive { + s2.GlyphStyle.Shape = crossShape + if !legendWithCrossShape { + p.Legend.Add("Exclusive", s2) + legendWithCrossShape = true + } + } else { + s2.GlyphStyle.Shape = ringShape + if !legendWithRingShape { + p.Legend.Add("Inclusive", s2) + legendWithRingShape = true + } + } + } else { + label = fmt.Sprintf("%s%v,%v%s", openBracket, xy.X, xy.Y, closeBracket) + s1, err = plotter.NewScatter(pXYs) + if err != nil { + return fmt.Errorf("unable to create new scatter: %v", err) + } + if !lowInclusive && !highInclusive { + s1.GlyphStyle.Shape = crossShape + } + } if xy.X != xy.Y { l, err := plotter.NewLine(pXYs) @@ -211,15 +308,20 @@ func plotIntervals(p *plot.Plot, xys plotter.XYs) error { ps = append(ps, l) p.Legend.Add(label, l) } else { - s.Color = color - p.Legend.Add(label, s) + s1.Color = color + if s2 != nil { + s2.Color = color + } + p.Legend.Add(label, s1) } if err != nil { return fmt.Errorf("could not create a new scatter: %v", err) } - ps = append(ps, s) - + ps = append(ps, s1) + if s2 != nil { + ps = append(ps, s2) + } } p.Legend.Left = false p.Add(ps...) diff --git a/find.go b/find.go index 52b25d7..424c0c2 100644 --- a/find.go +++ b/find.go @@ -1,14 +1,20 @@ package interval func (intvls *intervals) FindIntervalsForValue(value int) []*Interval { + // sort intervals (if necessary) intvls.Sort() + var matches []*Interval for _, intvl := range intvls.Intervals { - if intvl.Low > value { - // due to the intervals are sorted, we can confirm that we will not find more matches + // convert if necessary exclusive low/high values into inclusive ones + low, high := intvls.getInclusives(intvl.Low, intvl.High) + + // check if we have to stop searching + if low > value { + // due to the intervals are sorted byLow, we can confirm that we will not find more matches break } - if inBetweenInclusive(value, intvl.Low, intvl.High) { + if inBetweenInclusive(value, low, high) { matches = append(matches, intvl) } } diff --git a/gaps.go b/gaps.go index 19ffecc..35cbccd 100644 --- a/gaps.go +++ b/gaps.go @@ -16,19 +16,33 @@ func (intvls *intervals) Gaps() []*Interval { } func (intvls *intervals) calculateGaps() []*Interval { + list := []*Interval{} + if len(intvls.Intervals) == 0 { + return list + } + + // sort intervals (if necessary) intvls.Sort() - gaps := []*Interval{} - lastMaxHigh := intvls.MinLow + + gapThreshold := intvls.MinLow for _, intvl := range intvls.Intervals { - if intvl.Low > lastMaxHigh { - gaps = append(gaps, &Interval{Low: lastMaxHigh, High: intvl.Low - 1}) + // convert if necessary exclusive low/high values into inclusive ones + low, high := intvls.getInclusives(intvl.Low, intvl.High) + + // if the current Low is higher than the last maximal High, means that there is a gap so we add this gap to the list + if low > gapThreshold { + list = append(list, &Interval{Low: gapThreshold, High: low - 1}) } - if intvl.High >= lastMaxHigh { - lastMaxHigh = intvl.High + 1 + + // update if necessary the threshold for the next gap + if high >= gapThreshold { + gapThreshold = high + 1 } } - if lastMaxHigh < intvls.MaxHigh { - gaps = append(gaps, &Interval{Low: lastMaxHigh, High: intvls.MaxHigh}) + + // if intvls.Intervals haven't covered all the range until the end, we need to fill the rest until the end as a gap + if gapThreshold < intvls.MaxHigh { + list = append(list, &Interval{Low: gapThreshold, High: intvls.MaxHigh}) } - return gaps + return list } diff --git a/get.go b/get.go index 4085c92..06f5df7 100644 --- a/get.go +++ b/get.go @@ -1,6 +1,9 @@ package interval func (intvls *intervals) Get() []*Interval { + // sort intervals (if necessary) intvls.Sort() + + // return the intervals sorted return intvls.Intervals } diff --git a/inclusives.go b/inclusives.go new file mode 100644 index 0000000..340fdb0 --- /dev/null +++ b/inclusives.go @@ -0,0 +1,37 @@ +package interval + +func (intvls *intervals) getInclusives(intervalLow, intervalHigh int) (int, int) { + low := intvls.getInclusiveLow(intervalLow) + high := intvls.getInclusiveHigh(intervalHigh) + if high < low { + high = low + } + if low > high { + low = high + } + return low, high +} + +func (intvls *intervals) getInclusiveLow(value int) int { + if intvls.LowInclusive { + return value + } + // here the low is exclusive, we have to take the next value + newLow := value + 1 + if newLow > intvls.MaxHigh { + return intvls.MaxHigh + } + return newLow +} + +func (intvls *intervals) getInclusiveHigh(value int) int { + if intvls.HighInclusive { + return value + } + // here the high is exclusive, we have to take the previous value + newHigh := value - 1 + if newHigh < intvls.MinLow { + return intvls.MinLow + } + return newHigh +} diff --git a/intervals.go b/intervals.go index 3778b15..34a91d3 100644 --- a/intervals.go +++ b/intervals.go @@ -5,8 +5,10 @@ import ( ) const ( - defaultMinLow = 0 - defaultMaxHigh = math.MaxInt64 + defaultMinLow = 0 + defaultMaxHigh = math.MaxInt64 + defaultLowInclusive = true + defaultHighInclusive = true ) // Intervals is an interface to handle Interval structures discovering the existence of gaps or overlays @@ -39,6 +41,12 @@ type Intervals interface { // Report first sorts (if necessary) and then creates a report of the interval sequence Report() string + + // IsLowInclusive indicates if the Low part of the interval is included, e. g. (3,5) --> the 3 is included as part of the interval + IsLowInclusive() bool + + // IsHighInclusive indicates if the High part of the interval is included, e. g. (3,5) --> the 5 is included as part of the interval + IsHighInclusive() bool } // intervals implements Intervals interface @@ -50,19 +58,31 @@ type intervals struct { MinLow int MaxHigh int Sorted bool + LowInclusive bool + HighInclusive bool } // NewIntervalsDefault is a constructor that returns an instance of the Intervals interface with default values func NewIntervalsDefault() Intervals { - return NewIntervals(defaultMinLow, defaultMaxHigh) + return NewIntervals(defaultMinLow, defaultMaxHigh, defaultLowInclusive, defaultHighInclusive) } // NewIntervals is a constructor that returns an instance of the Intervals interface -func NewIntervals(minLow int, maxHigh int) Intervals { +func NewIntervals(minLow int, maxHigh int, lowInclusive bool, highInclusive bool) Intervals { return &intervals{ - MinLow: minLow, - MaxHigh: maxHigh, - Intervals: []*Interval{}, - Sorted: false, + MinLow: minLow, + MaxHigh: maxHigh, + Intervals: []*Interval{}, + Sorted: false, + LowInclusive: lowInclusive, + HighInclusive: highInclusive, } } + +func (intvls *intervals) IsLowInclusive() bool { + return intvls.LowInclusive +} + +func (intvls *intervals) IsHighInclusive() bool { + return intvls.HighInclusive +} diff --git a/merge.go b/merge.go index 811fc9f..bfc3b01 100644 --- a/merge.go +++ b/merge.go @@ -8,33 +8,66 @@ func (intvls *intervals) Merge() []*Interval { } func (intvls *intervals) calculateMerged() []*Interval { - intvls.Sort() list := []*Interval{} if len(intvls.Intervals) == 0 { return list } - var lastLow int - var lastHigh int + // sort intervals (if necessary) + intvls.Sort() + + var currentMinLow int + var currentMaxHigh int for i, intvl := range intvls.Intervals { + // convert if necessary exclusive low/high values into inclusive ones + low, high := intvls.getInclusives(intvl.Low, intvl.High) + + // in the first iteration it's for sure that don't exists merge, we just initialize variables if i == 0 { - lastLow = intvl.Low - lastHigh = intvl.High + currentMinLow = low + currentMaxHigh = high continue } - if isLowInBetween(intvl.Low, intvl.High, lastLow, lastHigh) || isHighInBetween(intvl.Low, intvl.High, lastLow, lastHigh) { - if intvl.Low < lastLow { - lastLow = intvl.Low + + // in case the current interval is overlapped or consecutive to the previous one, we need to update variables + // and keep going and process the next interval + if areSegmentsConsecutivesOrOverlapped(low, high, currentMinLow, currentMaxHigh) { + // update control variables if necessary + if low < currentMinLow { + currentMinLow = low } - if intvl.High > lastHigh { - lastHigh = intvl.High + if high > currentMaxHigh { + currentMaxHigh = high } continue } - list = append(list, &Interval{Low: lastLow, High: lastHigh}) - lastLow = intvl.Low - lastHigh = intvl.High + + // here the segments are not consecutive or overlapped so we close this merged segment (add to the list) + list = append(list, &Interval{Low: currentMinLow, High: currentMaxHigh}) + + // update control variables + currentMinLow = low + currentMaxHigh = high } - list = append(list, &Interval{Low: lastLow, High: lastHigh}) + // the last segment is pending to be added to the list + list = append(list, &Interval{Low: currentMinLow, High: currentMaxHigh}) return list } + +func areSegmentsOverlapped(low, high, lastLow, lastHigh int) bool { + if isLowInBetweenInclusive(low, high, lastLow, lastHigh) { + return true + } + if isHighInBetweenInclusive(low, high, lastLow, lastHigh) { + return true + } + return false +} + +func areSegmentsConsecutives(low, high, lastLow, lastHigh int) bool { + return ((lastHigh + 1) == low) || ((high + 1) == lastLow) +} + +func areSegmentsConsecutivesOrOverlapped(low, high, lastLow, lastHigh int) bool { + return areSegmentsOverlapped(low, high, lastLow, lastHigh) || areSegmentsConsecutives(low, high, lastLow, lastHigh) +} diff --git a/overlap.go b/overlap.go index 6629f17..1db2b8b 100644 --- a/overlap.go +++ b/overlap.go @@ -18,25 +18,38 @@ func (intvls *intervals) Overlapped() []*Interval { } func (intvls *intervals) calculateOverlapped() []*Interval { - intvls.Sort() list := []*Interval{} + if len(intvls.Intervals) == 0 { + return list + } + + // sort intervals (if necessary) + intvls.Sort() + lastMinLow := math.MaxInt64 lastMaxHigh := math.MinInt64 for i, intvl := range intvls.Intervals { + // convert if necessary exclusive low/high values into inclusive ones + low, high := intvls.getInclusives(intvl.Low, intvl.High) + + // for the first iteration make no sense those operations if i > 0 { - lowInBetween := isLowInBetween(lastMinLow, lastMaxHigh, intvl.Low, intvl.High) - highInBetween := isHighInBetween(lastMinLow, lastMaxHigh, intvl.Low, intvl.High) + // check if the front or back side of the current segment overlaps with the previous one + lowInBetween := isLowInBetweenInclusive(lastMinLow, lastMaxHigh, low, high) + highInBetween := isHighInBetweenInclusive(lastMinLow, lastMaxHigh, low, high) if lowInBetween || highInBetween { - greaterLow := max(intvl.Low, lastMinLow) - lowerHigh := min(intvl.High, lastMaxHigh) - list = append(list, &Interval{Low: greaterLow, High: lowerHigh}) + // extract which part is overlapped, create a new interval and add it to the list + biggestLow := max(low, lastMinLow) + smallestHigh := min(high, lastMaxHigh) + list = append(list, &Interval{Low: biggestLow, High: smallestHigh}) } } - if intvl.Low < lastMinLow { - lastMinLow = intvl.Low + // update control variables (if necessary) + if low < lastMinLow { + lastMinLow = low } - if intvl.High > lastMaxHigh { - lastMaxHigh = intvl.High + if high > lastMaxHigh { + lastMaxHigh = high } } return list @@ -44,7 +57,7 @@ func (intvls *intervals) calculateOverlapped() []*Interval { func (intvls *intervals) valueIsOverlapping(value int, overlapped []*Interval) bool { for _, ovrlp := range overlapped { - if inBetweenInclusive(value, ovrlp.Low, ovrlp.High) { + if inBetween(value, ovrlp.Low, ovrlp.High, intvls.LowInclusive, intvls.HighInclusive) { return true } } diff --git a/sort.go b/sort.go index 19a77a3..0f68038 100644 --- a/sort.go +++ b/sort.go @@ -3,6 +3,7 @@ package interval import "sort" func (intvls *intervals) Sort() { + // sort the intervals slice just if necessary if !intvls.Sorted { sort.Sort(ByLow(intvls.Intervals)) } diff --git a/utils.go b/utils.go index 7199a14..fbad269 100644 --- a/utils.go +++ b/utils.go @@ -13,6 +13,23 @@ func max(a, b int) int { return b } +func inBetween(i, min, max int, lowInclusive, highInclusive bool) bool { + if lowInclusive && highInclusive { + return inBetweenInclusive(i, min, max) + } + if !lowInclusive && !highInclusive { + return inBetweenExclusive(i, min, max) + } + if lowInclusive && !highInclusive { + return inBetweenLowInclusive(i, min, max) + } + if !lowInclusive && highInclusive { + return inBetweenHighInclusive(i, min, max) + } + return false +} + +// both inclusive func inBetweenInclusive(i, min, max int) bool { if (i >= min) && (i <= max) { return true @@ -20,6 +37,23 @@ func inBetweenInclusive(i, min, max int) bool { return false } +// only low is inclusive +func inBetweenLowInclusive(i, min, max int) bool { + if (i >= min) && (i < max) { + return true + } + return false +} + +// only high is inclusive +func inBetweenHighInclusive(i, min, max int) bool { + if (i > min) && (i <= max) { + return true + } + return false +} + +// both exclusive func inBetweenExclusive(i, min, max int) bool { if (i > min) && (i < max) { return true @@ -27,14 +61,28 @@ func inBetweenExclusive(i, min, max int) bool { return false } -func isLowInBetween(low1, high1, low2, high2 int) bool { +func isLowInBetween(low1, high1, low2, high2 int, lowInclusive, highInclusive bool) bool { + if inBetween(low1, low2, high2, lowInclusive, highInclusive) || inBetween(low2, low1, high1, lowInclusive, highInclusive) { + return true + } + return false +} + +func isLowInBetweenInclusive(low1, high1, low2, high2 int) bool { if inBetweenInclusive(low1, low2, high2) || inBetweenInclusive(low2, low1, high1) { return true } return false } -func isHighInBetween(low1, high1, low2, high2 int) bool { +func isHighInBetween(low1, high1, low2, high2 int, lowInclusive, highInclusive bool) bool { + if inBetween(high1, low2, high2, lowInclusive, highInclusive) || inBetween(high2, low1, high1, lowInclusive, highInclusive) { + return true + } + return false +} + +func isHighInBetweenInclusive(low1, high1, low2, high2 int) bool { if inBetweenInclusive(high1, low2, high2) || inBetweenInclusive(high2, low1, high1) { return true }