From e2b35650e9674cd4fcaf7a75c7110436ff79678b Mon Sep 17 00:00:00 2001 From: Jon Date: Fri, 27 Apr 2018 19:43:18 -0400 Subject: [PATCH] [stdlib] Add `Range` type for representing intervals of values. The `Range` type is particularly useful for working with and enforcing boundaries of acceptable values. It can also be used as a shorthand for creating linear enumerations, though it is not optimized for this case. --- spec/myst/range_spec.mt | 90 +++++++++++++++++++++++++++++++ spec/myst/spec.mt | 1 + stdlib/integer.mt | 14 +++++ stdlib/prelude.mt | 1 + stdlib/range.mt | 89 ++++++++++++++++++++++++++++++ stdlib/spec/describe_container.mt | 4 +- 6 files changed, 197 insertions(+), 2 deletions(-) create mode 100644 spec/myst/range_spec.mt create mode 100644 stdlib/range.mt diff --git a/spec/myst/range_spec.mt b/spec/myst/range_spec.mt new file mode 100644 index 0000000..f307589 --- /dev/null +++ b/spec/myst/range_spec.mt @@ -0,0 +1,90 @@ +require "stdlib/spec.mt" + + +describe("Range") do + range = %Range{0, 10} + + describe("#first") do + it("returns the lower bound of the Range") do + assert(range.first).equals(0) + end + end + + describe("#last") do + it("returns the upper bound of the Range") do + assert(range.last).equals(10) + end + end + + + describe("#each") do + it("calls the block for every element in the Range's interval") do + range = %Range{5, 10} + counter = 0 + range.each{ |_| counter += 1 } + # This is 6 instead of 5 because the last element is included as part of + # the range. 5, 6, 7, 8, 9, 10. + assert(counter).equals(6) + end + + it("iterates forward through the interval, in order") do + range = %Range{5, 10} + list = [] + range.each{ |e| list.push(e) } + + assert(list).equals([5, 6, 7, 8, 9, 10]) + end + end + + describe("#reverse_each") do + it("calls the block for every element in the Range's interval") do + range = %Range{5, 10} + counter = 0 + range.reverse_each{ |_| counter += 1 } + # This is 6 instead of 5 because the last element is included as part of + # the range. 5, 6, 7, 8, 9, 10. + assert(counter).equals(6) + end + + it("iterates backward through the interval, in order") do + range = %Range{5, 10} + list = [] + range.reverse_each{ |e| list.push(e) } + + assert(list).equals([10, 9, 8, 7, 6, 5]) + end + end + + + describe("#includes?") do + range = %Range{0, 10} + + it("returns true when the value is within the Range's interval") do + assert(range.includes?(5)).is_true + end + + it("returns false when the value is above the Range's interval") do + assert(range.includes?(11)).is_false + end + + it("returns false when the value is below the Range's interval") do + assert(range.includes?(-1)).is_false + end + + it("returns true when the value is the Range's lower bound") do + assert(range.includes?(0)).is_true + end + + it("returns true when the value is the Range's upper bound") do + assert(range.includes?(10)).is_true + end + end + + + describe("#to_s") do + it("returns a string representing the bounds of the interval") do + range = %Range{0, 10} + assert(range.to_s).equals("(0..10)") + end + end +end diff --git a/spec/myst/spec.mt b/spec/myst/spec.mt index 337b3de..0bfabeb 100644 --- a/spec/myst/spec.mt +++ b/spec/myst/spec.mt @@ -16,6 +16,7 @@ require "./map_spec.mt" require "./nil_spec.mt" require "./not_spec.mt" require "./random_spec.mt" +require "./range_spec.mt" require "./string_spec.mt" require "./symbol_spec.mt" require "./type_spec.mt" diff --git a/stdlib/integer.mt b/stdlib/integer.mt index 5eb4011..90e1a03 100644 --- a/stdlib/integer.mt +++ b/stdlib/integer.mt @@ -11,4 +11,18 @@ deftype Integer self end + + #doc successor -> integer + #| Returns the next smallest integer that is greater than this integer. This + #| method is primarily intended for use with the `Range` type. + def successor + self + 1 + end + + #doc predeccesor -> integer + #| Returns the next largest integer that is smaller than this integer. This + #| method is primarily intended for use with the `Range` type. + def predecessor + self - 1 + end end diff --git a/stdlib/prelude.mt b/stdlib/prelude.mt index 5390cb9..f9a4c78 100644 --- a/stdlib/prelude.mt +++ b/stdlib/prelude.mt @@ -16,5 +16,6 @@ require "./integer.mt" require "./io.mt" require "./list.mt" require "./map.mt" +require "./range.mt" require "./string.mt" require "./time.mt" diff --git a/stdlib/range.mt b/stdlib/range.mt new file mode 100644 index 0000000..09e205a --- /dev/null +++ b/stdlib/range.mt @@ -0,0 +1,89 @@ +#doc Range +#| The `Range` type represents the interval between two values. By default, +#| Ranges are _inclusive_, meaning both the first and last values are included +#| as part of the Range. +#| +#| The only values stored by a Range are the lower and upper bounds. The values +#| within the interval are calculated lazily, and only when necessary. With the +#| exceptions of `#each` and `#reverse_each`, most methods are implemented using +#| only comparisons between these bounds. +#| +#| As Myst does not currently provide a literal syntax for Ranges, creating a +#| new Range is done with normal type instantiation, providing the first and +#| last values of the interval as arguments: `%Range{10, 20}`. +#| +#| Any value type can be used in a Range so long as it implements the `<` and +#| `<=` comparison operators. However, to enable iterating through the Range, +#| value types must also implement a `#successor` that returns the next element +#| of the interval. +#| +#| Ranges can also be used in reverse (e.g., with `#reverse_each`) if the +#| value type defines a `#predecessor` method returning the previous element +#| of the interval. +#| +#| `Range` includes `Enumerable`, so all of Enumerable's methods can be used +#| directly on Ranges. Where possible, Range provides optimized implementations +#| of Enumerable methods to avoid having to iterate all values in the interval +#| (e.g., `#includes?`). +deftype Range + # Range defines `#each`, so any type that satisfies the conditions of Range + # can also be used as an Enumerable. + include Enumerable + + #doc initialize + #| Creates a new range for the interval `[first, last]`. + def initialize(@first, @last); end + + #doc first -> element + #| Returns the first element of this Range; the lower bound. + def first; @first; end + #doc last -> element + #| Returns the last element of this Range; the upper bound. + def last; @last; end + + + #doc each(&block) -> self + #| Iterates forward through the Range (starting at `first` and incrementing + #| to `last`), calling the block for every element in the interval. + def each(&block) + current = @first + while current <= @last + block(current) + current = current.successor + end + + self + end + + #doc reverse_each(&block) -> self + #| Iterates backward through the Range (starting at `last` and decrementing + #| to `first`), calling the block for every element in the interval. + def reverse_each(&block) + current = @last + while @first <= current + block(current) + current = current.predecessor + end + + self + end + + + #doc includes?(value) -> boolean + #| Returns true if `value` exists within the interval of this Range. + #| + #| This method has an `O(1)` implementation using only comparisons with the + #| bounds of the interval. + def includes?(value) + !!(@first <= value && value <= @last) + end + + + #doc to_s -> string + #| Returns an abstract string representation of the interval covered by this + #| Range. Note that this does _not_ return a string of all the values in the + #| interval. + def to_s + "(<(@first)>..<(@last)>)" + end +end diff --git a/stdlib/spec/describe_container.mt b/stdlib/spec/describe_container.mt index e2a708a..4711431 100644 --- a/stdlib/spec/describe_container.mt +++ b/stdlib/spec/describe_container.mt @@ -1,14 +1,14 @@ defmodule Spec deftype DescribeContainer def initialize(name : String) - @name = name + @name = name end def name; @name; end def get_path(current : String, stack_index) when !describe_stack.empty? && next_describe = describe_stack[stack_index] - return describe_stack[stack_index].get_path("<(@name)> <(current)>", stack_index - 1) + return describe_stack[stack_index].get_path("<(current)>", stack_index - 1) else "<(@name)> <(current)>" end