From a71f16f2f213d7c8882633c3211bc58d98be130c Mon Sep 17 00:00:00 2001 From: Stephen von Takach Date: Wed, 11 Jul 2018 22:26:21 +1000 Subject: [PATCH] initial commit --- .gitignore | 6 ++ .travis.yml | 1 + LICENSE | 21 +++++++ README.md | 42 +++++++++++++ shard.yml | 2 + spec/bisect_spec.cr | 144 ++++++++++++++++++++++++++++++++++++++++++++ src/bisect.cr | 109 +++++++++++++++++++++++++++++++++ 7 files changed, 325 insertions(+) create mode 100644 .gitignore create mode 100644 .travis.yml create mode 100644 LICENSE create mode 100644 README.md create mode 100644 shard.yml create mode 100644 spec/bisect_spec.cr create mode 100644 src/bisect.cr diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0792935 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +doc +lib +.crystal +.shards +app +*.dwarf diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..ffc7b6a --- /dev/null +++ b/.travis.yml @@ -0,0 +1 @@ +language: crystal diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..a9b28ef --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2018 Stephen von Takach + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..7be5b9e --- /dev/null +++ b/README.md @@ -0,0 +1,42 @@ +# Crystal Lang Bisect + +Provides helpers for dealing with sorted Arrays. +It uses binary search to reduce the number of comparisons. + + +Usage +===== + +There are two primary functions that you need to know about `Bisect.insort` and `Bisect.bisect`. + +`Bisect.insort` adds a new element to the Array, but keeps the Array sorted: + +```ruby +require 'bisect' +a = [1, 2, 4] +Bisect.insort(a, 3) +a == [1, 2, 3, 4] +``` + +`Bisect.bisect` gives you the index at which the element would have been inserted: + +```ruby +require 'bisect' +a = ['a', 'b', 'd'] +Bisect.bisect(a, 'c') == 2 +``` + +If there are equal elements in the Array then `insort` will insert the element after the last equal element. Similarly `bisect` will return the index one higher than the last equal element. If you'd like to add new elements before equal elements, use `insort_left` and `bisect_left`. If you need to be explicit then `insort_right` and `bisect_right` are aliases for `insort` and `bisect`. + + +Core ext +======== + +These methods are also available directly on Arrays + +```ruby +require 'bisect' +a = [1, 2, 4] +a.insort(3) +a == [1, 2, 3, 4] +``` diff --git a/shard.yml b/shard.yml new file mode 100644 index 0000000..2914b2e --- /dev/null +++ b/shard.yml @@ -0,0 +1,2 @@ +name: bisect +version: 1.0.0 diff --git a/spec/bisect_spec.cr b/spec/bisect_spec.cr new file mode 100644 index 0000000..e6c576e --- /dev/null +++ b/spec/bisect_spec.cr @@ -0,0 +1,144 @@ +require "spec" +require "../src/bisect" + +class Tests + def self.values + [ + {:right, [] of Int32, 1, 0}, + {:right, [1], 0, 0}, + {:right, [1], 1, 1}, + {:right, [1], 2, 1}, + {:right, [1, 1], 0, 0}, + {:right, [1, 1], 1, 2}, + {:right, [1, 1], 2, 2}, + {:right, [1, 1, 1], 0, 0}, + {:right, [1, 1, 1], 1, 3}, + {:right, [1, 1, 1], 2, 3}, + {:right, [1, 1, 1, 1], 0, 0}, + {:right, [1, 1, 1, 1], 1, 4}, + {:right, [1, 1, 1, 1], 2, 4}, + {:right, [1, 2], 0, 0}, + {:right, [1, 2], 1, 1}, + {:right, [1, 2], 1.5, 1}, + {:right, [1, 2], 2, 2}, + {:right, [1, 2], 3, 2}, + {:right, [1, 1, 2, 2], 0, 0}, + {:right, [1, 1, 2, 2], 1, 2}, + {:right, [1, 1, 2, 2], 1.5, 2}, + {:right, [1, 1, 2, 2], 2, 4}, + {:right, [1, 1, 2, 2], 3, 4}, + {:right, [1, 2, 3], 0, 0}, + {:right, [1, 2, 3], 1, 1}, + {:right, [1, 2, 3], 1.5, 1}, + {:right, [1, 2, 3], 2, 2}, + {:right, [1, 2, 3], 2.5, 2}, + {:right, [1, 2, 3], 3, 3}, + {:right, [1, 2, 3], 4, 3}, + {:right, [1, 2, 2, 3, 3, 3, 4, 4, 4, 4], 0, 0}, + {:right, [1, 2, 2, 3, 3, 3, 4, 4, 4, 4], 1, 1}, + {:right, [1, 2, 2, 3, 3, 3, 4, 4, 4, 4], 1.5, 1}, + {:right, [1, 2, 2, 3, 3, 3, 4, 4, 4, 4], 2, 3}, + {:right, [1, 2, 2, 3, 3, 3, 4, 4, 4, 4], 2.5, 3}, + {:right, [1, 2, 2, 3, 3, 3, 4, 4, 4, 4], 3, 6}, + {:right, [1, 2, 2, 3, 3, 3, 4, 4, 4, 4], 3.5, 6}, + {:right, [1, 2, 2, 3, 3, 3, 4, 4, 4, 4], 4, 10}, + {:right, [1, 2, 2, 3, 3, 3, 4, 4, 4, 4], 5, 10}, + + {:left, [] of Int32, 1, 0}, + {:left, [1], 0, 0}, + {:left, [1], 1, 0}, + {:left, [1], 2, 1}, + {:left, [1, 1], 0, 0}, + {:left, [1, 1], 1, 0}, + {:left, [1, 1], 2, 2}, + {:left, [1, 1, 1], 0, 0}, + {:left, [1, 1, 1], 1, 0}, + {:left, [1, 1, 1], 2, 3}, + {:left, [1, 1, 1, 1], 0, 0}, + {:left, [1, 1, 1, 1], 1, 0}, + {:left, [1, 1, 1, 1], 2, 4}, + {:left, [1, 2], 0, 0}, + {:left, [1, 2], 1, 0}, + {:left, [1, 2], 1.5, 1}, + {:left, [1, 2], 2, 1}, + {:left, [1, 2], 3, 2}, + {:left, [1, 1, 2, 2], 0, 0}, + {:left, [1, 1, 2, 2], 1, 0}, + {:left, [1, 1, 2, 2], 1.5, 2}, + {:left, [1, 1, 2, 2], 2, 2}, + {:left, [1, 1, 2, 2], 3, 4}, + {:left, [1, 2, 3], 0, 0}, + {:left, [1, 2, 3], 1, 0}, + {:left, [1, 2, 3], 1.5, 1}, + {:left, [1, 2, 3], 2, 1}, + {:left, [1, 2, 3], 2.5, 2}, + {:left, [1, 2, 3], 3, 2}, + {:left, [1, 2, 3], 4, 3}, + {:left, [1, 2, 2, 3, 3, 3, 4, 4, 4, 4], 0, 0}, + {:left, [1, 2, 2, 3, 3, 3, 4, 4, 4, 4], 1, 0}, + {:left, [1, 2, 2, 3, 3, 3, 4, 4, 4, 4], 1.5, 1}, + {:left, [1, 2, 2, 3, 3, 3, 4, 4, 4, 4], 2, 1}, + {:left, [1, 2, 2, 3, 3, 3, 4, 4, 4, 4], 2.5, 3}, + {:left, [1, 2, 2, 3, 3, 3, 4, 4, 4, 4], 3, 3}, + {:left, [1, 2, 2, 3, 3, 3, 4, 4, 4, 4], 3.5, 6}, + {:left, [1, 2, 2, 3, 3, 3, 4, 4, 4, 4], 4, 6}, + {:left, [1, 2, 2, 3, 3, 3, 4, 4, 4, 4], 5, 10}, + ] + end +end + +describe Bisect do + it "should bisect an array" do + Tests.values.each do |test| + direction, array, element, result = test + + case direction + when :left + Bisect.bisect_left(array, element).should eq(result) + when :right + Bisect.bisect_right(array, element).should eq(result) + else + raise "wtf?!" + end + end + end + + it "should insort a sorted array" do + array = [] of Int32 + 500.times do + digit = rand(10) + if digit % 2 == 0 + Bisect.insort_left(array, digit) + else + Bisect.insort_right(array, digit) + end + array.should eq(array.sort) + end + end + + it "should insort directly on an array" do + array = [] of Int32 + 500.times do + digit = rand(10) + if digit % 2 == 0 + array.insort_left(digit) + else + array.insort_right(digit) + end + array.should eq(array.sort) + end + + Tests.values.each do |test| + direction, array, element, result = test + + case direction + when :left + array.bisect_left(element).should eq(result) + when :right + array.bisect_right(element).should eq(result) + else + raise "wtf?!" + end + end + end +end diff --git a/src/bisect.cr b/src/bisect.cr new file mode 100644 index 0000000..86eb8e9 --- /dev/null +++ b/src/bisect.cr @@ -0,0 +1,109 @@ +# A direct port of the Python bisect standard library. +# +# http://svn.python.org/view/python/branches/py3k/Lib/bisect.py?view=markup&pathrev=70846 +# +module Bisect + extend self + + # Insert item x in list a, and keep it sorted assuming a is sorted. + # + # If x is already in a, insert it to the right of the rightmost x. + # + # Optional args lo (default 0) and hi (default len(a)) bound the + # slice of a to be searched. + def insort_right(a, x, lo = 0, hi = a.size) + index = bisect_right(a, x, lo, hi) + a.insert(index, x) + end + + def insort(a, x, lo = 0, hi = a.size) + insort_right(a, x, lo, hi) + end + + # Return the index where to insert item x in list a, assuming a is sorted. + # + # The return value i is such that all e in a[:i] have e <= x, and all e in + # a[i:] have e > x. So if x already appears in the list, a.insert(x) will + # insert just after the rightmost x already there. + # + # Optional args lo (default 0) and hi (default len(a)) bound the + # slice of a to be searched. + def bisect_right(a, x, lo = 0, hi = a.size) + raise ArgumentError.new("lo must be non-negative") if lo < 0 + + while lo < hi + mid = (lo + hi) / 2 + if x < a[mid] + hi = mid + else + lo = mid + 1 + end + end + + lo + end + + def bisect(a, x, lo = 0, hi = a.size) + bisect_right(a, x, lo, hi) + end + + # Insert item x in list a, and keep it sorted assuming a is sorted. + # + # If x is already in a, insert it to the left of the leftmost x. + # + # Optional args lo (default 0) and hi (default len(a)) bound the + # slice of a to be searched. + def insort_left(a, x, lo = 0, hi = a.size) + index = bisect_left(a, x, lo, hi) + a.insert(index, x) + end + + # Return the index where to insert item x in list a, assuming a is sorted. + # + # The return value i is such that all e in a[:i] have e < x, and all e in + # a[i:] have e >= x. So if x already appears in the list, a.insert(x) will + # insert just before the leftmost x already there. + # + # Optional args lo (default 0) and hi (default len(a)) bound the + # slice of a to be searched. + def bisect_left(a, x, lo = 0, hi = a.size) + raise ArgumentError.new("lo must be non-negative") if lo < 0 + + while lo < hi + mid = (lo + hi) / 2 + if a[mid] < x + lo = mid + 1 + else + hi = mid + end + end + + lo + end +end + +class Array + def insort_left(x, lo = 0, hi = self.size) + Bisect.insort_left(self, x, lo, hi) + end + + def insort_right(x, lo = 0, hi = self.size) + Bisect.insort_right(self, x, lo, hi) + end + + def insort(x, lo = 0, hi = self.size) + Bisect.insort_right(self, x, lo, hi) + end + + def bisect_left(x, lo = 0, hi = self.size) + Bisect.bisect_left(self, x, lo, hi) + end + + def bisect_right(x, lo = 0, hi = self.size) + Bisect.bisect_right(self, x, lo, hi) + end + + def bisect(x, lo = 0, hi = self.size) + Bisect.bisect_right(self, x, lo, hi) + end +end