FancySpec describe: Fancy Enumerable with: {
  it: "selects the right element based on a comparison Block and an optional selection Block" with: 'superior_by:taking: when: {
    [1,2,3,4,5] superior_by: '> . is: 5
    [1,2,3,4,5] superior_by: '< . is: 1
    [[1,2], [2,3,4], [], [1]] superior_by: '> taking: 'size . is: [2,3,4]
    [[1,2], [2,3,4], [-1]] superior_by: '< taking: 'first . is: [-1]
  }

  it: "returns an Array of mapped values" with: 'map: when: {
    [] map: 'identity . is: []
    (1,2,3) map: 'squared . is: [1,4,9]
    "foo" map: 'upcase . is: ["F", "O", "O"]
  }

  it: "returns an Array of mapped values by calling a block with each element and its index" with: 'map_with_index: when: {
    [] map_with_index: 'identity . is: []
    [1,2,3] map_with_index: |x i| { i * 2 } . is: [0,2,4]
  }

  it: "chain-maps all blocks on all values" with: 'map_chained: when: {
    (1,2,3) map_chained: ('doubled, 'squared, 'to_s) . is: ["4", "16", "36"]
    (1,2,3) map_chained: (@{ + 1 }, 'to_s) . is: ["2", "3", "4"]
    [] map_chained: ('class, 'name) . is: []
    [1, "foo", 'hello] map_chained: ('class, 'name) . is: ["Fixnum", "String", "Symbol"]
  }

  it: "maps over its elements with their index" with: 'map_with_index: when: {
    (1,2,3) map_with_index: |x i| { x + i } . is: [1,3,5]

    [1,2,3,4] map_with_index: |_ i| { i } . is: [0,1,2,3]

    [] map_with_index: |_ i| { i } . is: []
  }

  it: "returns a flattened Array of mapped values" with: 'flat_map: when: {
    [] flat_map: 'identity . is: []
    (1,2,3) flat_map: 'identity . is: [1,2,3]
    [(1,2,3), (4,5,6)] flat_map: 'sum . is: [6, 15]
    [[1,2,3], [4,5,6]] flat_map: 'identity . is: [1,2,3,4,5,6]
  }

  it: "counts the amount of elements for which a block yields true" with: 'count: when: {
    [1,2,3] count: @{ even? } . is: 1
    (0..10) count: @{ even? } . is: 6
    (0..10) count: @{ odd? } . is: 5
    "foo" count: @{ == "o" } . is: 2
    "foo" count: @{ != "o" } . is: 1
    (1,2,3) count: @{ < 2 } . is: 1
    [] count: { true } . is: 0
    [1] count: { true } . is: 1
    [1] count: { false } . is: 0
  }

  it: "returns a string concatenation of all elements in self" with: 'to_s when: {
    (1,2,3) to_s is: "123"
    [1,2,3] to_s is: "123"
    "foo" to_s is: "foo"
    [] to_s is: ""

    class MyCollection {
      def each: block {
        (0..5) each: block
      }
      include: Fancy Enumerable
    }
    MyCollection new to_s is: "012345"
  }

  it: "returns the array in groups of 3" with: 'in_groups_of: when: {
    ['a,'b,'c] in_groups_of: 1 . is: [['a],['b],['c]]
    array = 1 upto: 10
    array in_groups_of: 3 . is: [[1,2,3], [4,5,6], [7,8,9], [10]]

    (20,30,40) in_groups_of: 2 . is: [[20,30], [40]]

    [1,2,3] in_groups_of: -1 . is: []
    [1,2,3] in_groups_of: 0 . is: []
    [1,2,3] in_groups_of: 1 . is: [[1],[2],[3]]

    [] in_groups_of: -1 . is: []
    [] in_groups_of: 0 . is: []
    [] in_groups_of: 1 . is: []
  }

  it: "indicates if it's sorted" with: 'sorted? when: {
    [] sorted? is: true
    [1] sorted? is: true
    [1,2] sorted? is: true
    [2,1] sorted? is: false

    (1,2,3) sorted? is: true
    (1,3,2) sorted? is: false

    "" sorted? is: true
    "a" sorted? is: true
    "abc" sorted? is: true
    "fabc" sorted? is: false
  }

  it: "sorts by a given block" with: 'sort_by: when: {
    [[1,2], [1,2,3], [1], []] sort_by: @{ size } . is: [[], [1], [1,2], [1,2,3]]
    [[1,2], [1,2,3], [1], []] sort_by: 'size . is: [[], [1], [1,2], [1,2,3]]
    ["abc", "abcd", "ab", "a", ""] sort_by: @{ size } . is: ["", "a", "ab", "abc", "abcd"]
  }

  it: "splits a collection at an index" with: 'split_at: when: {
    [] split_at: 0 . is: [[],[]]
    [1] split_at: 0 . is: [[],[1]]
    [1] split_at: 1 . is: [[1],[]]

    (1,2,3) split_at: 1 . is: [[1], [2,3]]
    (1,2,3) split_at: 2 . is: [[1,2], [3]]

    "foo" split_at: 1 . is: [["f"], ["o", "o"]]
  }

  it: "splits a collection based on a predicate block" with: 'split_with: when: {
    [] split_with: @{ true } . is: [[],[]]
    [] split_with: @{ false } . is: [[],[]]
    [1] split_with: @{ true } . is: [[1], []]
    [1] split_with: @{ false } . is: [[], [1]]
    [1,2,3] split_with: @{ < 2 } . is: [[1], [2,3]]
    [1,2,3] split_with: @{ <= 2 } . is: [[1,2], [3]]

    "" split_with: @{ true } . is: [[], []]
    "a" split_with: @{ true } . is: [["a"], []]
    "a" split_with: @{ false } . is: [[], ["a"]]
    "abcde" split_with: @{ <= "c" } . is: [["a", "b", "c"], ["d", "e"]]
  }

  it: "takes elements from itself as long a block yields true" with: 'take_while: when: {
    (1..15) take_while: |x| { x < 10 } . is: (1 upto: 9)
  }

  it: "drops elements from itself as long a block yields true" with: 'drop_while: when: {
    (1..15) drop_while: |x| { x < 10 } . is: (10 upto: 15)
    "aaabcd" drop_while: /a/ . is: ["b","c","d"]
  }

  it: "returns the first n elements" with: 'first: when: {
    (1,2,3) first: 0 . is: []
    (1,2,3) first: 1 . is: [1]
    (1,2,3) first: 2 . is: [1,2]
    (1,2,3) first: 3 . is: [1,2,3]
    (1,2,3) first: 4 . is: [1,2,3]
  }

  it: "returns the last n elements" with: 'last: when: {
    (1,2,3) last: 0 . is: []
    (1,2,3) last: 1 . is: [3]
    (1,2,3) last: 2 . is: [2,3]
    (1,2,3) last: 3 . is: [1,2,3]
    (1,2,3) last: 4 . is: [1,2,3]
  }

  it: "returns the elements for which a pattern matches" with: 'grep: when: {
    "hello world" grep: /[a-h]/ . is: ["h", "e", "d"]
    ["hello", "world", 1, 2, 3] grep: String . is: ["hello", "world"]
    (1,2,3,4,5) grep: @{ < 2 } . is: [1] # blocks can be used as patterns, too :)
    (1,2,3) grep: String . is: []
    (1,2,3,4) grep: (2..3) . is: [2, 3]
  }

  it: "returns the values of calling a block for all elements for which a pattern matches" with: 'grep:taking: when: {
    "hello world" grep: /[a-h]/ taking: 'upcase . is: ["H", "E", "D"]
    ["hello", "world", 1, 2, 3] grep: String taking: 'upcase . is: ["HELLO", "WORLD"]
    (1,2,3,4,5) grep: @{ < 2 } taking: 'doubled . is: [2]
    (1,2,3) grep: String taking: 'to_s . is: []
    (1,2,3,4) grep: (2..3) taking: 'to_s . is: ["2", "3"]
  }

  it: "joins all elements by a block" with: 'join_by: when: {
    (1,2,3) join_by: '+ . is: $ (1,2,3) sum
    ("foo", "bar", "baz") join_by: '+ . is: "foobarbaz"
    [NoMethodError, IOError, ZeroDivisionError] join_by: '>< . is: (NoMethodError >< IOError >< ZeroDivisionError)
    [NoMethodError, IOError, ZeroDivisionError] join_by: '<> . is: (NoMethodError <> IOError <> ZeroDivisionError)
  }

  it: "returns the element if found" with: 'find: when: {
    [1,2,3] tap: @{
      find: 0 . is: nil
      find: 1 . is: 1
      find: 2 . is: 2
      find: 3 . is: 3
      find: 4 . is: nil
    }
  }

  it: "calls a block with the element if found" with: 'find:do: when: {
    found = []
    [1,2,3] tap: @{
      insert = |x| { found << x }
      find: 0 do: insert
      find: 1 do: insert
      find: 2 do: insert
      find: 3 do: insert
      find: 4 do: insert
    }
    found is: [1,2,3]
  }

  it: "calls a block with the element and its index if found" with: 'find_with_index:do: when: {
    found = []
    [1,2,3] tap: @{
      insert = |x i| { found << (x,i) }
      find_with_index: 0 do: insert
      find_with_index: 1 do: insert
      find_with_index: 2 do: insert
      find_with_index: 3 do: insert
      find_with_index: 4 do: insert
    }
    found is: [(1,0), (2,1), (3,2)]
  }

  it: "calls a block with an element and its indexes" with: 'for_every:with_index_do: when: {
    found = []
    [1,2,3,2,1] tap: @{
      insert = |x i| { found << (x,i) }
      for_every: 0 with_index_do: insert
      for_every: 1 with_index_do: insert
      for_every: 2 with_index_do: insert
      for_every: 3 with_index_do: insert
      for_every: 4 with_index_do: insert
    }
    found is: [(1,0), (1,4), (2,1), (2,3), (3,2)]
  }

  it: "calls a block with an element for every occurance" with: 'for_every:do: when: {
    arr = [1,2,3,2,1]

    c = 0
    arr for_every: 0 do: { c = c + 1 }
    c is: 0

    arr for_every: 1 do: { c = c + 1 }
    c is: 2

    c = 0
    arr for_every: 2 do: { c = c + 1 }
    c is: 2

    c = 0
    arr for_every: 2 do: { c = c + 1 }
    c is: 2

    c = 0
    arr for_every: 3 do: { c = c + 1 }
    c is: 1

    arr for_every: 1 do: @{ is: 1 }
    arr for_every: 2 do: @{ is: 2 }
    arr for_every: 3 do: @{ is: 3 }
  }

  it: "returns the last index of an element or nil" with: 'last_index_of: when: {
    [] last_index_of: nil . is: nil
    [] last_index_of: 1 . is: nil
    [1,2,1,2] last_index_of: 1 . is: 2
    [1,2,1,2] last_index_of: 2 . is: 3
    [1,2,1,2] last_index_of: 3 . is: nil
  }

  it: "is selected from it with each index" with: 'select_with_index: when: {
    ["yooo",2,3,1,'foo,"bar"] select_with_index: |x i| { x is_a?: Fixnum } . is: [[2,1], [3,2], [1,3]]
  }

  it: "returns a chunked array based on a given block" with: 'chunk_by: when: {
    [] chunk_by: @{ nil? } . is: []
    [1] chunk_by: @{ nil? } . is: [[false, [1]]]
    [1,2] chunk_by: @{ odd? } . is: [
      [true, [1]],
      [false, [2]]
    ]

    [1,3,4,5,7,2,6,8,10,9] do: @{
      chunk_by: 'even? . is: [
        [false, [1,3]],
        [true, [4]],
        [false, [5,7]],
        [true, [2,6,8,10]],
        [false, [9]]
      ]

      chunk_by: 'odd? . is: [
        [true, [1,3]],
        [false, [4]],
        [true, [5,7]],
        [false, [2,6,8,10]],
        [true, [9]]
      ]

      chunk_by: 'nil? . is: [
        [false, [1,3,4,5,7,2,6,8,10,9]]
      ]
    }
  }

  it: "returns a hash grouped by the value returned by a block called with the elements" with: 'group_by: when: {
    [] group_by: 'nil? . is: <[]>
    [1,3,5] group_by: @{ odd? } . is: <[true => [1,3,5]]>
    (0,1,2,3) group_by: 'even? . is: <[false => [1,3], true => [0,2]]>
    ('foo, 1, 2, 'bar) group_by: @{ class } . is: <[Symbol => ['foo, 'bar], Fixnum => [1,2]]>
    "FooBar" group_by: @{ uppercase? } . is: <[true => ["F", "B"], false => ["o", "o", "a", "r"]]>
  }

  it: "returns its min and max value" with: 'min_max when: {
    [] min_max is: (nil, nil)
    [1] min_max is: (1, 1)
    (1,2,3,4) min_max is: (1, 4)
    (4,2,1,3) min_max is: (1, 4)
  }

  it: "returns its min and max value based on a given block" with: 'min_max_by: when: {
    [] min_max_by: 'nil . is: (nil, nil)
    ["foo"] min_max_by: 'size . is: ("foo", "foo")
    ["a", "bc", "def"] min_max_by: @{ size } . is: ("a", "def")
    ["long", "a", "bc", "def"] min_max_by: @{ size } . is: ("a", "long")
  }

  it: "returns the minimum" with: 'min when: {
    [] min is: nil
    [100] min is: 100
    [0, 1] min is: 0
    [-100, 100] min is: -100
    [-4,-3,-2,-1,0,1,2,3,4,5] min is: -4
  }

  it: "returns the minimum based on a block" with: 'min_by: when: {
    [] min_by: 'size . is: nil
    [[1, 2, 3]] min_by: 'size . is: [1, 2, 3]
    ["foo", "foobar"] min_by: @{ size } . is: "foo"
    [[1, 2], ["foo", "bar", "baz"]] min_by: @{ size } . is: [1, 2]
  }

  it: "returns the maximum" with: 'max when: {
    [] max is: nil
    [100] max is: 100
    [0, 1] max is: 1
    [-100, 100] max is: 100
    [-4,-3,-2,-1,0,1,2,3,4,5] max is: 5
  }

  it: "returns the maximum based on a block" with: 'max_by: when: {
    [] max_by: 'size . is: nil
    [[1, 2, 3]] max_by: 'size . is: [1, 2, 3]
    ["foo", "foobar"] max_by: @{ size } . is: "foobar"
    [[1, 2], ["foo", "bar", "baz"]] max_by: @{ size } . is: ["foo", "bar", "baz"]
  }

  it: "returns true if exactly one element yields true with a given block" with: 'one?: when: {
    [] one?: 'nil? . is: false
    (1,2) one?: 'odd? . is: true
    (1,2,3) one?: 'odd? . is: false
    "fooBar" one?: 'uppercase? . is: true
  }

  it: "returns true if a given block yields true for none of the elements" with: 'none?: when: {
    [] none?: 'nil? . is: true
    [1] none?: 'odd? . is: false
    [1] none?: 'even? . is: true
    [1,3,5] none?: 'even? . is: true
    "foobarBaz" none?: 'uppercase? . is: false
  }

  it: "returns the sum of all the elements" with: 'sum when: {
    [] sum is: 0
    [1] sum is: 1
    [-1,0,1] sum is: 0
    (1,2,3) sum is: 6
  }

  it: "returns the product of all the elements" with: 'product when: {
    [] product is: 1
    [1] product is: 1
    [1,2] product is: 2
    [1,2,3] product is: 6
    [-1,5,2] product is: -10
    [1,0,1] product is: 0
  }

  it: "retuns the average value of all elements" with: 'average when: {
    [] average is: 0
    [1] average is: 1
    [1,2] average is: 1.5
    [2,4,6] average is: 4
    [-1,0,1] average is: 0
    [-1,1,2] average is: $ 2.0 / 3
  }
}