Thread overview
Creating a custom iota()
May 12, 2022
realhet
May 12, 2022
H. S. Teoh
May 12, 2022
realhet
May 12, 2022
Ali Çehreli
May 12, 2022
ag0aep6g
May 12, 2022
Ali Çehreli
May 12, 2022
realhet
May 12, 2022
realhet
May 12, 2022

Hello,

I have my own DateTime struct.
It has opCmp() and opBinary(), I can do arithmetic with this custom DateTime and the amazing time units of the quantities package.

Now I'm about mo make iterations in a DateTime range:

const
  st = DateTime(UTC, "22.1.1 8:30").utcDayStart,
  en = DateTime(UTC, "22.3.5 19:56").max(now).utcDayStart + (100.0/64)*nano(second);

//this works
for(auto d = cast()st; d < en; d += day) writeln(d);

//this would be nicer, but not works
iota(st, en, day).each!writeln;

My question is, is there a way to 'extend' the functionality of the std.iota() function or it is better to come up with a unique name for this special 'iota' functionality?

I know that if I'm going to patch iota in my module, I have to make ALL the iota() variants plus my special iota variant visible in that module scope. Is this a good practice, or is there a better way?

Or maybe put it inside a DateTimeRange struct, and implement the std range functionality?

May 12, 2022
On Thu, May 12, 2022 at 11:57:54AM +0000, realhet via Digitalmars-d-learn wrote: [...]
> I have my own DateTime struct.
> It has opCmp() and opBinary(), I can do arithmetic with this custom
> DateTime and the amazing time units of the **quantities** package.
> 
> Now I'm about mo make iterations in a DateTime range:
> 
>     const
>       st = DateTime(UTC, "22.1.1 8:30").utcDayStart,
>       en = DateTime(UTC, "22.3.5 19:56").max(now).utcDayStart +
> (100.0/64)*nano(second);
> 
>     //this works
>     for(auto d = cast()st; d < en; d += day) writeln(d);
> 
>     //this would be nicer, but not works
>     iota(st, en, day).each!writeln;
> 
> My question is, is there a way to 'extend' the functionality of the std.iota() function or it is better to come up with a unique name for this special 'iota' functionality?

Custom user types should already be supported by iota; see:

	https://issues.dlang.org/show_bug.cgi?id=10762

Does your DateTime type support the `++` operator?  If not, that's the most likely reason iota failed to instantiate on it.  Just add `opUnary(string op : "++")` to your type, and it should work.


[...]
> Or maybe put it inside a DateTimeRange struct, and implement the std range functionality?
[...]

That would work too.

But I'm curious, why didn't you use std.datetime?  The DateTime type there supports inter-date arithmetic (as far as it makes sense), and can be easily made into a range using std.range.recurrence.


T

-- 
When solving a problem, take care that you do not become part of the problem.
May 12, 2022
On 5/12/22 04:57, realhet wrote:

>     //this would be nicer, but not works
>     iota(st, en, day).each!writeln;

For others, the problem is, iota does have a version that works with user types but not one that parameterizes 'step'. An oversight?

> My question is, is there a way to 'extend' the functionality of the
> std.iota() function or it is better to come up with a unique name for
> this special 'iota' functionality?

I think ioat can be improved to work with user 'step' types as I did below.

> Is this a good practice

I don't care whether it is good practice or not. :) The following is what you meant anyway and seems to work. I added 6 comments:

import std.stdio;
import std.range;
import std.algorithm;

struct Duration {
  ulong value;
}

struct DateTime {
  Duration sinceEpoch;  // Quickest implementation for this post

  auto opOpAssign(string op)(Duration dur)
  if (op == "+") {
    return DateTime(Duration(sinceEpoch.value += dur.value));
  }
}

void main() {
  const st = DateTime(Duration(0));
  const en = DateTime(Duration(10));

  const step = Duration(1);

  // (0) I think D should not insist on 'const'
  // when copying types that have no indirections.
  // We shouldn't need the cast() below in this case.
  //
  // Alternatively, the implementation of iota()
  // could use Unqual after detecting B has no
  // indirections. That would be better for the
  // user but again, the language should copy
  // to non-const by-default. But then, I am
  // sure there would be cases where an unexpected
  // function overload might be selected in some
  // cases.
  iota(cast()st, en, step).each!writeln;
}

// I adapted the following template from my
// /usr/include/dlang/dmd/std/range/package.d
// and then:
//  (1) Added 'S step'
auto iota(B, E, S)(B begin, E end, S step)
// (2) Removed for now
// if (!isIntegral!(CommonType!(B, E)) &&
//     !isFloatingPoint!(CommonType!(B, E)) &&
//     !isPointer!(CommonType!(B, E)) &&
//     is(typeof((ref B b) { ++b; })) &&
//     (is(typeof(B.init < E.init)) || is(typeof(B.init == E.init))) )
{
    static struct Result
    {
        B current;
        E end;
        S step;  // (3) Added

        @property bool empty()
        {
            static if (is(typeof(B.init < E.init)))
                return !(current < end);
            else static if (is(typeof(B.init != E.init)))
                return current == end;
            else
                static assert(0);
        }
        @property auto front() { return current; }
        void popFront()
        {
            assert(!empty);
            // (4) Used += instead of ++current
            // This can be improved to use the other
            // method a.l.a. "design by introspection".
            current += step;
        }
    }
    // (5) Added step
    return Result(begin, end, step);
}

Ali

May 12, 2022
On Thursday, 12 May 2022 at 17:06:39 UTC, Ali Çehreli wrote:
> void main() {
>   const st = DateTime(Duration(0));
[...]
>
>   // (0) I think D should not insist on 'const'
>   // when copying types that have no indirections.
>   // We shouldn't need the cast() below in this case.
[...]
>   iota(cast()st, en, step).each!writeln;
> }
>
[...]
> auto iota(B, E, S)(B begin, E end, S step)
[...]
> {
>     static struct Result
>     {
>         B current;
[...]
>         void popFront()
>         {
[...]
>             current += step;
>         }
>     }
[...]
> }

Mark iota's `begin` parameter as const. Then you don't need the cast, because `B` will be mutable.
May 12, 2022
On Thursday, 12 May 2022 at 16:57:35 UTC, H. S. Teoh wrote:

> Does your DateTime type support the `++` operator?

It can't because I only want to use the quantities.si.Time type to do arithmetic with my DateTime. In my previous DateTime, it was a lot of problem that I was doing math on it's raw internal variable (which was a double where 1.0 meant 1 day.)
In the new version adding int(1) does a compilation error.
I will use 1*second or 1*day or 1*micro(second) instead. I will always state the measurement unit to avoid confusions.


> But I'm curious, why didn't you use std.datetime?

There are many weird reasons:
5 years ago I moved to DLang from Delphi. Since 20 years I always used the old  Borland date system whist stards in 1899, is an ieee double, 1 = 1 day. I only did stuff with local times, it was enough.
Recently I had to work with Python as well, and there I started to have problems with time synchronizations between the 2 systems. I ended up sending the exact Borland time to the Python program and make my little routines to work with them exactly like I do 20 years ago in Delphi, 5 years ago in DLang. But I know it is not nice.
So a week ago I discovered the quantities package and I decided to rewrite my DateTime to use the Time type in that. It solves all my measurement unit madness in a very elegant way. So I also want those measurement units working in all my systems. (I have lengths, frequencies too)
For me std.datetime was not an option because that felt so big I scared of it at the first look. It's 1.5MB, the same size as my complete program I'm working on :D
I also hade a second based time measurement 'system', it used QueryPerformanceCounter. I want do discontinue that as well.

So these are the features of the new DateTime:
- small size: 64 uint is the internal format. (Half of std.systemtime, but it lacks timezone, it's only UTC internally).
- precise: 100.0/64 nanosecond is the smallest unit. It covers 913 years.
- It was built around the new WinAPI GetSystemTimePreciseAsFileTime() It gives me 100ns resolution UTC time in less than 50ns. So that's why I don't need QueryPerformanceCounter anymore.
- The extra 6 bits under the 100ns ticks will be used for time based unique ID generation.  It's better for me than having 60000 years at my hand.
- The integration with quantities.si makes my work much easier. I can't wait to use it in my program, as I knows physics things better than me, haha.


> std.range.recurrence.

Indeed, that's an option as well, I forgot about.

Thank You!



May 12, 2022
On 5/12/22 12:51, ag0aep6g wrote:

>> auto iota(B, E, S)(B begin, E end, S step)
> [...]
>> {
>>     static struct Result
>>     {
>>         B current;
> [...]
>>         void popFront()
>>         {
> [...]
>>             current += step;
>>         }
>>     }
> [...]
>> }
>
> Mark iota's `begin` parameter as const. Then you don't need the cast,
> because `B` will be mutable.

Cool trick! Like this:

auto iota(B, E, S)(const(B) begin, E end, S step)
{
  // ...
}

It works with non-const values as well. So apparently it makes the function parameter const(B) and deduces B to be the non-const version of that, which always produces the non-const version.

And I've been thinking 'iota' may not be as suitable as I thought at first. I like the following even more:

  auto r0 = st
            .by(Duration(2))
            .take(5);

  auto r1 = st
            .by(Duration(2))
            .until(en);

A family of 'by' ovenloads can be defined or it can be templatized to be used as 'by!Duration(2)' etc.:

auto by(DateTime dt, Duration dur) {
  struct Result {
    DateTime front;
    Duration dur;

    enum empty = false;
    void popFront() { front += dur; }
  }

  return Result(dt, dur);
}

Just works. But one may want to provide an accessor for front().

Ali

May 12, 2022
On Thursday, 12 May 2022 at 17:06:39 UTC, Ali Çehreli wrote:
> I don't care whether it is good practice or not. :) The following is what you meant anyway and seems to work.

I restricted the parameter types to the ones I wanted to use.
And for the standard iota behavior I used a public import.

    public import std.range : iota;

    auto iota(in DateTime begin, in DateTime end, in Time step){
      //https://forum.dlang.org/post/ivskeghrhbuhpiytesas@forum.dlang.org -> Ali's solution

      static struct Result{
        DateTime current, end;
        Time step;

        @property bool empty(){ return current >= end; }
        @property auto front(){ return current; }
        void popFront(){ assert(!empty); current += step; }
      }

      return Result(begin, end, step);
    }

    ...

    iota(st, en, day).each!writeln; //works
    iota(1, 10, 0.5).each!writeln;  //also works

It works perfectly, Thank You very much!

Although that general implementation of iota is a bit complex for me, this specialized one is simple.

note(0): no cast() was needed here, worked with a const DateTime{ ulong ticks; ... }
May 12, 2022
On Thursday, 12 May 2022 at 20:12:19 UTC, Ali Çehreli wrote:
> And I've been thinking 'iota' may not be as suitable as I thought at first. I like the following even more:
>
>   auto r0 = st
>             .by(Duration(2))
>             .take(5);

So I wrote this by() for my DateTime and then:

    import quantities.si;

    auto by(in DateTime begin, in Frequency f){
      return begin.by(1/f);
    }

    //This let me do:

    now.by(60*hertz)
       .until!"a>b"(now+1*second)
       .each!writeln;

My mind is blowing! :D