January 19, 2022
On 1/19/22 04:51, Salih Dincer wrote:

> Is it okay to swap places instead of throwing an error?

I would be happier if my potential mistake is caught instead of the library doing something on its own.

> Let's also
> implement BidirectionalRange, if okay.

I had started experimenting with that as well. The implementation below does not care if popFront() or popBack() are called on empty ranges. The programmer must be careful. :)

> "Does it reverse the result
> in case ```a > b``` like we
> did with foreach_reverse()"

No, it should not reverse the direction that way because we already have retro. :)

  inclusiveRange(1, 10).retro;

Improving the range as a BidirectionalRange requires three more functions: save(), back(), and popBack().

I am also removing the silly 'const' qualifiers from member functions because a range object is supposed to be mutated. I came to that conclusion after realizing that save() cannot be 'const' because it break isForwardRange (which is required by isBidirectionalRange). Ok, I will change all 'const's to 'inout's in case someone passes an object to a function that takes by 'const' and just applies e.g. empty() on it.

And adding length() was easy as well.

Finally, I have provided property functions instead of allowing direct access to members.

struct InclusiveRange(T) {
  import std.format : format;

  T first_;
  T last_;
  bool empty_;

  this(U)(in U first, in U last)
  in (first <= last, format!"Invalid range: [%s,%s]."(first, last)) {
    this.first_ = first;
    this.last_ = last;
    this.empty_ = false;
  }

  T front() inout {
    return first_;
  }

  bool empty() inout {
    return empty_;
  }

  void popFront() {
    if (first_ == last_) {
      empty_ = true;

    } else {
      ++first_;
    }
  }

  auto save() inout {
    return this;
  }

  T back() inout {
    return last_;
  }

  void popBack() {
    if (first_ == last_) {
      empty_ = true;

    } else {
      --last_;
    }
  }

  size_t length() inout {
    return last_ - first_ + 1 - empty_;
  }
}

auto inclusiveRange(T)(in T first, in T last) {
  return InclusiveRange!T(first, last);
}

unittest {
  // Invalid range should throw
  import std.exception : assertThrown;

  assertThrown!Error(inclusiveRange(2, 1));
}

unittest {
  // Should not be possible to have an empty range
  import std.algorithm : equal;

  auto r = inclusiveRange(42, 42);
  assert(!r.empty);
  assert(r.equal([42]));
}

unittest {
  // Should be able to represent all values of a type
  import std.range : ElementType;
  import std.algorithm : sum;

  auto r = inclusiveRange(ubyte.min, ubyte.max);
  static assert(is(ElementType!(typeof(r)) == ubyte));

  assert(r.sum == (ubyte.max * (ubyte.max + 1)) / 2);
}

unittest {
  // Should produce the last value
  import std.algorithm : sum;

  assert(inclusiveRange(1, 10).sum == 55);
}

unittest {
  // Should work with negative values
  import std.algorithm : equal;
  assert(inclusiveRange(-3, 3).equal([-3, -2, -1, 0, 1, 2, 3]));
  assert(inclusiveRange(-30, -27).equal([-30, -29, -28, -27]));
}

unittest {
  // length should be correct
  import std.range : enumerate, iota;

  enum first = 5;
  enum last = 42;
  auto r = inclusiveRange(first, last);

  // Trusting iota's implementation
  size_t expectedLength = iota(first, last).length + 1;
  size_t i = 0;
  do {
    assert(r.length == expectedLength);
    r.popFront();
    --expectedLength;
  } while (!r.empty);
}

unittest {
  // Should provide the BidirectionalRange interface
  import std.range : retro;
  import std.algorithm : equal;

  auto r = inclusiveRange(1, 10);
  assert(!r.save.retro.equal(r.save));
  assert(r.save.retro.retro.equal(r.save));
}

void main() {
  import std.stdio;
  import std.range;

  writeln(inclusiveRange(1, 10));
  writeln(inclusiveRange(1, 10).retro);

  auto r = inclusiveRange(1, 11);
  while (true) {
    writefln!"%s .. %s  length: %s"(r.front, r.back, r.length);
    r.popFront();
    if (r.empty) {
      break;
    }
    r.popBack();
    if (r.empty) {
      break;
    }
  }
}

Ali

January 20, 2022

Hi,

It looks so delicious. 😀 Thank you.

On Wednesday, 19 January 2022 at 18:59:10 UTC, Ali Çehreli wrote:

>

And adding length() was easy as well.

Finally, I have provided property functions instead of allowing direct access to members.

It doesn't matter as we can't use a 3rd parameter. But it doesn't work for any of these types: real, float, double.

My solution:

  size_t length() inout {
    //return last_ - first_ + 1 - empty_;/*
    auto len = 1 + last_ - first_;
    return cast(size_t)len;//*/
  }

But it only works on integers. In this case, we have two options! The first is to require the use of integers, other 3 parameter usage:

// ...
  size_t length() inout {
    auto len = 1 + (last - front) / step;
    return cast(size_t)len;
  }
} unittest {
  enum { ira = 0.1,
         irb = 2.09,
         irc = 0.11
       }
  auto test = inclusiveRange(ira, irb, irc);
  assert(test.count == 19);

  auto arr = iota(ira, irb, irc).array;
  assert(test.length == arr.length);
}

Salih

January 20, 2022
On 1/19/22 21:24, Salih Dincer wrote:

> ```d
>    size_t length() inout {
>      //return last_ - first_ + 1 - empty_;/*
>      auto len = 1 + last_ - first_;
>      return cast(size_t)len;//*/
>    }
> ```

Good catch but we can't ignore '- empty_'. Otherwise an empty range will return 1.

> But it only works on integers.

After fixing the size_t issue, it should work on user-defined types as well. In fact, it is better to leave the return type as auto so that it works with user-defined types that support the length expression but is a different type like e.g. MyDiffType.

Having said that, floating point types don't make sense with the semantics of a *bidirectional and inclusive* range. :) Let's see how it looks for ranges where the step size is 0.3:

import std.stdio;

void main() {
  float beg = 0.0;
  float end = 1.0;
  float step = 0.3;

  writeln("\nIncrementing:");
  for (float f = beg; f <= end; f += step) {
    report(f);
  }

  writeln("\nDecrementing:");
  for (float f = end; f >= beg; f -= step) {
    report(f);
  }
}

void report(float f) {
  writefln!"%.16f"(f);
}

Here is the output:

Incrementing:
0.0000000000000000
0.3000000119209290
0.6000000238418579
0.9000000357627869
                <-- Where is 1.0?
Decrementing:
1.0000000000000000
0.6999999880790710
0.3999999761581421
0.0999999642372131
                <-- Where is 0.0?

So if we add the 1.0 value after 0.9000000357627869 to be *inclusive*, then that last step would not be 0.3 anymore. (Thinking about it, step would mess up things for integral types as well; so, it must be checked during construction.)

The other obvious issue in the output is that a floating point iota cannot be bidirectional because the element values would be different.

Ali

January 21, 2022

On Thursday, 20 January 2022 at 16:33:20 UTC, Ali Çehreli wrote:

>

So if we add the 1.0 value after 0.9000000357627869 to be inclusive, then that last step would not be 0.3 anymore. (Thinking about it, step would mess up things for integral types as well; so, it must be checked during construction.)

The other obvious issue in the output is that a floating point iota cannot be bidirectional because the element values would be different.

The test that did not pass now passes. There is the issue of T.min being excluded from the property list for the double type. I tried to solve it, how is it?

Salih

auto inclusiveRange(T)(T f, T l, T s = cast(T)0)
in(!isBoolean!T) {
  static assert(!isBoolean!T, "\n
      Cannot be used with bool type\n");
  if(!s) s++;
  return InclusiveRange!T(f, l, s);
}

struct InclusiveRange(T) {
  private:
    T first, last;
    bool empty_;

  public:
    T step;

  this(U)(in U first, in U last, in U step)
  in (first <= last, format!"\n
      Invalid range:[%s,%s]."(first, last))
  {
    this.first = first;
    this.last = last;
    this.step = step;
    this.empty_ = false;
  }

  bool opBinaryRight(string op:"in")(T rhs) {
    foreach(r; this) {
      if(r == rhs) return true;
    }
    return false;
  }

  auto save() inout { return this; }
  bool empty() inout { return empty_; }
  T front() inout { return first; }
  T back() inout { return last; }

  void popFront() {
    if(!empty) {
      if(last >= first + step) {
        first += step;
      } else {
        empty_ = true;
        if(T.max <= first + step) {
          first += step;
        }
      }
    }
  }

  void popBack() {
    if(!empty) {
      if(first <= last-step) {
        last -= step;
      } else {
        empty_ = true;
        if(!T.max >= last - step) {
          last -= step;
        }
      }
    }
  }

  size_t length() inout {
    auto len = 1 + (last - first) / step;
    return cast(size_t)len;
  }
}

import std.algorithm, std.math;
import std.range, std.traits;
import std.stdio, std.format, std.conv;

void main() {
  // Pi Number Test
  auto GregorySeries = inclusiveRange!double(1, 0x1.0p+27, 2);
  double piNumber = 0;
  foreach(e, n; GregorySeries.enumerate) {
    if(e & 1) piNumber -= 1/n;
    else piNumber += 1/n;
  }
  writefln!"%.21f (constant)"(PI);
  writefln!"%.21f (calculated)"(piNumber * 4);

} unittest {
  // Should not be possible to have an empty range

  auto r = inclusiveRange(ubyte.min, ubyte.max);
  static assert(is(ElementType!(typeof(r)) == ubyte));

  assert(r.sum == (ubyte.max * (ubyte.max + 1)) / 2);
}
January 21, 2022
On 1/21/22 08:58, Salih Dincer wrote:

> ```d
> auto inclusiveRange(T)(T f, T l, T s = cast(T)0)
> in(!isBoolean!T) {

'in' contracts are checked at runtime. The one above does not make sense because you already disallow compilation for 'bool' below.

You could add a template constraint there:

auto inclusiveRange(T)(T f, T l, T s = cast(T)0)
if(!isBoolean!T) {

(Note 'if' vs. 'in'.)

However, people who instantiate the struct template directly would bypass that check anyway.

>    static assert(!isBoolean!T, "\n
>        Cannot be used with bool type\n");
>    if(!s) s++;
>    return InclusiveRange!T(f, l, s);
> }

>    bool opBinaryRight(string op:"in")(T rhs) {
>      foreach(r; this) {
>        if(r == rhs) return true;
>      }
>      return false;
>    }

Ouch! I tried the following code, my laptop got very hot, it's been centuries, and it's still running! :p

  auto looong = inclusiveRange(ulong.min, ulong.max);
  foreach (l; looong) {
    assert(l in looong);
  }

>    size_t length() inout {
>      auto len = 1 + (last - first) / step;
>      return cast(size_t)len;
>    }

Does that not return 1 for an empty range?

Additionally, just because we *provide* a step, now we *require* division from all types (making it very cumbersome for user-defined types).

>    // Pi Number Test
>    auto GregorySeries = inclusiveRange!double(1, 0x1.0p+27, 2);

Very smart! ;) So, this type can support floating point values if we use that syntax.

Ali

January 21, 2022

On Friday, 21 January 2022 at 17:25:20 UTC, Ali Çehreli wrote:

>

Ouch! I tried the following code, my laptop got very hot, it's been centuries, and it's still running! :p

:)

>
   size_t length() inout {
     auto len = 1 + (last - first) / step;
     return cast(size_t)len;
   }

Does that not return 1 for an empty range?

Yes, but it will never return an empty range:

  enum e = 1;
  auto o = inclusiveRange(e, e); // only one element
  assert(!o.empty);
  assert(o.length == e);
  assert(o.equal([e]));
>

Additionally, just because we provide a step, now we require division from all types (making it very cumbersome for user-defined types).

I don't quite understand what you mean?

Salih

January 21, 2022
> On Friday, 21 January 2022 at 17:25:20 UTC, Ali Çehreli wrote:
[...]
> > Additionally, just because we *provide* a step, now we *require* division from all types (making it very cumbersome for user-defined types).
[...]

It doesn't have to be this way. We could just use DbI to inspect whether the incoming type supports division; if it does, we provide stepping, otherwise, just plain ole iteration.

DbI rocks.


T

-- 
Doubt is a self-fulfilling prophecy.
1 2 3
Next ›   Last »