Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Option in TimeUnitField using lazy_static #76

Merged
merged 11 commits into from
Feb 18, 2021

Conversation

koenichiwa
Copy link
Contributor

@koenichiwa koenichiwa commented Feb 17, 2021

Same as #73 Just cleaned up the git mess.

Changes the internals of all TimeUnitFields to be optional. If the TimeUnitField specified, then its populated with an OrdinalSet. If it's not specified (the cron field is *) then it's None. If then the the OrdinalSet is requested, it will simply return a reference to a static OrdinalSet that is filled using [TimeUnitField]::supported_ordinals(). TimeUnitField::all() will create a TimeUnitField that will point to this static set.

TimeUnitSpec gains an extra function: is_specified()

I've chosen to populate this set lazily, but I had to make it statically, because the following code simply doesn't work:

fn ordinals(&self) -> &OrdinalSet {
        &self.0
        match &self.ordinals {
            Some(ordinal_set) => &ordinal_set,
            None => &self::all() // Can't return this reference, needs to be a static reference.
        }
    }

Lastly I cleaned up some small stuff in parsing.rs

Seconds::from_ordinal_set(iter::once(0).collect()) becomes Seconds::from_ordinal(0) (etc.)

@koenichiwa
Copy link
Contributor Author

Decided to write some tests, found a small mistake.

Apparently is_specified was only in TimeUnitField.
Copy link
Owner

@zslayton zslayton left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is lazy_static preferable to something like OnceCell? I don't have much experience with either; if there's no great answer I'm happy to move ahead with lazy_static.

cron has a lot of formatting foibles. When we reach a stopping place in the round of PRs you're working on I'd like to do a pass with rustfmt and add that to the CI build.


impl TimeUnitField for DaysOfWeek {
fn from_ordinal_set(ordinal_set: OrdinalSet) -> Self {
DaysOfWeek(ordinal_set)
fn from_ordinal_set(ordinal_set: Option<OrdinalSet>) -> Self {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Accepting an Option<_> works fine, but to my eyes DaysOfWeek::from_ordinal_set(None) looks like it's using the empty set rather than all values.

Another option here: the TimeUnitField trait could have three methods, a low-level one that accepts an Option<OrdinalSet> (essentially a rename of the existing from_ordinal_set) and two default impls that expose a more self-documenting API for use elsewhere in the crate like parsing.rs:

// Each type implements this:
fn from_optional_ordinal_set(ordinal_set: Option<OrdinalSet>) -> Self;

// === The trait provides these fn impls for free ===

fn all() -> Self {
  Self::from_optional_ordinal_set(None)
}

fn from_ordinal_set(ordinal_set: OrdinalSet) -> Self {
  Self::from_optional_ordinal_set(Some(ordinal_set))
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah no problem! I'll do that.

@@ -236,7 +237,7 @@ where
use self::Specifier::*;
//println!("ordinals_from_specifier for {} => {:?}", Self::name(), specifier);
match *specifier {
All => Ok(Self::supported_ordinals()),
All => Ok(Self::supported_ordinals()), // TODO: change to Self::ALL
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you tell me about this TODO? Off-hand it seems like Self::supported_ordinals() and Self::ALL.clone() would do the same work. (The latter might even do more work because lazy_static has a small amount of overhead to dereference.)

Copy link
Contributor Author

@koenichiwa koenichiwa Feb 17, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Self::supported ordinals creates a new OrdinalSet. If ALL is already initialized, this should run quicker, otherwise ALL initializes the OrdinalSet for the next use (instead of throwing the OrdinalSet away)

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ALL is a static ref though, and would need to be clone()ed to satisfy the function's return type.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That would defeat the purpose then. I can remove the TODO

@koenichiwa
Copy link
Contributor Author

Why is lazy_static preferable to something like OnceCell? I don't have much experience with either; if there's no great answer I'm happy to move ahead with lazy_static.

Never heard of OnceCell, but this is the first sentence I read on the link you send:

This is essentially the lazy_static! macro, but without a macro.

I can try to change it to OnceCell if you prefer.

from_ordinal_set takes an OrdinalSet again, and is implemented in TimeUnitField
@koenichiwa
Copy link
Contributor Author

koenichiwa commented Feb 17, 2021

I'm looking more into OnceCell. It seems that there is a proposition to move it into std. Maybe it actually is better to use that instead of lazy_static.

src/parsing.rs Show resolved Hide resolved
/// assert_eq!(true, schedule.days_of_month().is_specified());
/// assert_eq!(false, schedule.months().is_specified());
/// ```
fn is_specified(&self) -> bool;
Copy link
Owner

@zslayton zslayton Feb 17, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have a minor quibble with the name of this function: the field is specified; the user had to write *. How would you feel about is_all instead, which would return the inverse?

This is slightly different in that (for example) a DayOfWeek field with *, ?, and 1-7 would all return true for is_all() regardless of whether its internal representations were None or the equivalent of Some(ALL).

I like the feel of this better; would it address your use case?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a bit afraid that this implementation would break when you get to years though. If I would type Years.is_all() on 1970-2070 it should actually always return false right? Or maybe the end is actually nigh :p

Yes you can specify 1-7 for weekdays, but then you actually did specify it, as being all weekdays.

My use case was (basically the reason why I came about this project) to change cron strings into launchd files. When you don't specify a year in a launchd file, it means that you want something to happen regardless of the year. I didn't want to print 100 or so years in my launchd file whenever I parsed a string with a * in it.

Copy link
Owner

@zslayton zslayton Feb 17, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a bit afraid that this implementation would break when you get to years though. If I would type Years.is_all() on 1970-2070 it should actually always return false right? Or maybe the end is actually nigh :p

Ha, good point. The impl I laid out would be the default impl, but I think we could easily justify overriding it in the impl for Years to just look at is_some().

Yes you can specify 1-7 for weekdays, but then you actually did specify it, as being all weekdays.

I see the distinction you're making, but I'm not certain I want Schedule to make that distinction. There's not currently a distinction between * and ? (for fields that accept it), and intuitively I would expect 1-7 (or whichever inclusive range) to be treated the same for fields that have a fixed range (all of them but Years).

My use case was (basically the reason why I came about this project) to change cron strings into launchd files. When you don't specify a year in a launchd file, it means that you want something to happen regardless of the year. I didn't want to print 100 or so years in my launchd file whenever I parsed a string with a * in it.

Does the default impl override of is_all for Years address this for you?

To be clear, I could be persuaded to have an is_specified method, it just feels a bit hazy semantically. I wanted to see if we could get by with something that wasn't strictly bound to the input expression syntax first.

}
}
fn is_specified(&self) -> bool {
self.ordinals.is_some()
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If someone wrote 0-23 for the hours field, that would be equivalent to * but this would return false. Is that the intended effect? What if this were something like:

Suggested change
self.ordinals.is_some()
let max_supported_ordinals = (Self::inclusive_max() - Self::inclusive_min()) + 1;
let is_all = self.ordinals
.map(|o| o.len() == max_supported_ordinals)
.unwrap_or(true);
!is_all

This could be a default impl shared across all TimeUnitField types.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(If this were renamed from is_specified to is_all we'd obviously drop the ! in the returned value.)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thios could be an option. I feel like is_all is still a really weird function to call on years though

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I'd make that change, I would rather write it as:

fn is_all(&self) -> bool {
    let max_supported_ordinals = Self::inclusive_max() - Self::inclusive_min() + 1
    self.ordinals.map_or(true, |o| o.len() == max_supported_ordinals))
}

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, that's better. 👍 Didn't know about map_or.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Didn't know about map_or.
Me neither till I tried running rustfix --pedantic. Might not be a bad idea to run that after this PR cycle.

I can do this, but ordinals() in TimeUnitField doesn't return an Option so I just changed it to

    fn is_all(&self) -> bool {
        let max_supported_ordinals = Self::inclusive_max() - Self::inclusive_min() + 1;
        self.ordinals().len() == max_supported_ordinals as usize
    }

Which I'm sure is acceptable as well

@@ -236,7 +237,7 @@ where
use self::Specifier::*;
//println!("ordinals_from_specifier for {} => {:?}", Self::name(), specifier);
match *specifier {
All => Ok(Self::supported_ordinals()),
All => Ok(Self::supported_ordinals()), // TODO: change to Self::ALL
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ALL is a static ref though, and would need to be clone()ed to satisfy the function's return type.

@koenichiwa
Copy link
Contributor Author

Should I use lazy_static or once_cell?
To the first approximation, once_cell is both more flexible and more convenient than lazy_static and should be preferred.
Unlike once_cell, lazy_static supports spinlock-based implementation of blocking which works with #![no_std].
lazy_static has received significantly more real world testing, but once_cell is also a widely used crate

From the once_cell docs. I can change it from lazy_static without any problems. Yea or nay?

@zslayton
Copy link
Owner

Should I use lazy_static or once_cell?
To the first approximation, once_cell is both more flexible and more convenient than lazy_static and should be preferred.
Unlike once_cell, lazy_static supports spinlock-based implementation of blocking which works with #![no_std].
lazy_static has received significantly more real world testing, but once_cell is also a widely used crate

From the once_cell docs. I can change it from lazy_static without any problems. Yea or nay?

Yea. Thanks for looking into this!

@koenichiwa
Copy link
Contributor Author

Ok those were a couple of extra pushes, but I think I'm done now. I changed lazy_static to once_cell. I changed is_specified to is_all. Then realized it was implemented in the wrong position, so I had to change that. And lastly I changed the test string to be "0-59 * 0-23 ?/2 1,2-4 ? *". I think that covers everything.

Copy link
Owner

@zslayton zslayton left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Were you going to add an overriding impl of is_all for Years to check is_some? Otherwise this looks great.

src/time_unit/mod.rs Outdated Show resolved Hide resolved
@koenichiwa
Copy link
Contributor Author

koenichiwa commented Feb 18, 2021

Were you going to add an overriding impl of is_all for Years to check is_some? Otherwise this looks great.

I had a bit of a problem with the name is_all in relation to years. That's why I preferred is_specified. But I do need to check in some way if * is used for years. At the moment it checks if years contains all the years that this library can handle. If it is decided that the bounds of years are changed, this check will still work.
So I'd rather leave it like this

Nit on line #160

Co-authored-by: Zack Slayton <zack.slayton@gmail.com>
@zslayton
Copy link
Owner

zslayton commented Feb 18, 2021

I had a bit of a problem with the name is_all in relation to years. That's why I preferred is_specified. But I do need to check in some way if * is used for years.

My objection to is_specified was/is largely a naming problem. To me, the only way for a value to be unspecified is for that field to be absent, which is currently only legal for Years (though I'd like it to be possible for Seconds too, #13).

Now that we have an is_all method that treats ?, *, (e.g.) 1-7 and <absent> as equivalent, I think I would be ok having a separate is_specified method that is false for <absent> and true for any other expression.

Would that combination of is_specified and is_all satisfy your use case? Or do you need to be able to further distinguish between 1970-2070 and *, which I think is the only "all" equivalence possible in the Years field?

If it is decided that the bounds of years are changed, this check will still work. So I'd rather leave it like this.

Not sure I followed this. Could you give an example?

@zslayton
Copy link
Owner

I'm going to go ahead and merge this PR, we can resolve the outstanding discussion points in a follow-on.

Thanks again!

@zslayton zslayton merged commit 22ea6bc into zslayton:master Feb 18, 2021
@koenichiwa
Copy link
Contributor Author

My objection to is_specified was/is largely a naming problem. To me, the only for a value to be unspecified is for that field to be absent, which is currently only legal for Years (though I'd like it to be possible for Seconds too, #13).

Yeah my problem with is_all was largely a naming problem as well. I but I don't thionk we have to agree on every nitpick. It's your project after all.

Not sure I followed this. Could you give an example?

The current bounds are 1970-2070, if someone comes along and decides that these bounds are insufficient. Then the is_all function on years will still work. of course it's not literaly all years, but yeah... nitpick

@koenichiwa koenichiwa deleted the feature/optional-fields branch February 23, 2021 16:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants