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

Add Time() and Nanos() methods #31

Merged
merged 2 commits into from Aug 22, 2018
Merged

Add Time() and Nanos() methods #31

merged 2 commits into from Aug 22, 2018

Conversation

rkuris
Copy link
Contributor

@rkuris rkuris commented Jul 18, 2018

These methods can be used to extract time values
from V1 UUIDs

Originally added as satori/go.uuid#50

@coveralls
Copy link

Pull Request Test Coverage Report for Build 92

  • 15 of 15 (100.0%) changed or added relevant lines in 1 file are covered.
  • No unchanged relevant lines lost coverage.
  • Overall coverage increased (+0.04%) to 99.104%

Totals Coverage Status
Change from base Build 90: 0.04%
Covered Lines: 332
Relevant Lines: 335

💛 - Coveralls

@coveralls
Copy link

coveralls commented Jul 18, 2018

Pull Request Test Coverage Report for Build 134

  • 14 of 14 (100.0%) changed or added relevant lines in 1 file are covered.
  • No unchanged relevant lines lost coverage.
  • Overall coverage increased (+0.04%) to 99.143%

Totals Coverage Status
Change from base Build 128: 0.04%
Covered Lines: 347
Relevant Lines: 350

💛 - Coveralls

@rkuris
Copy link
Contributor Author

rkuris commented Jul 18, 2018

I'd gladly stop using my fork if this can be maintained by gofrs; suggest bumping to revision 2.1.0 when adding this functionality.

@rkuris
Copy link
Contributor Author

rkuris commented Jul 18, 2018

@jbsturgeon @michaelrios your upvote here would be appreciated.

I strongly support moving away from satori/go.uuid, and this looks like a good set of developers to steward such an important library forward.

uuid.go Outdated
@@ -153,6 +156,28 @@ func (u *UUID) SetVariant(v byte) {
}
}

// Nanos returns the number of nanoseconds in a V1 UUID
func (u *UUID) Nanos() (int64, error) {
Copy link
Contributor

Choose a reason for hiding this comment

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

my recommendation would be to name this UnixNanoOf() to be consistent with the naming convention from the time library (time.Now().UnixNano())

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good suggestion. Completed.

uuid.go Outdated

// Time returns the time embedded in a V1 UUID
// An error is returned if this is not a V1 UUID
func (u *UUID) Time() (time.Time, error) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Recommend naming TimeOf(), per @theckman's idea

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

@rkuris rkuris force-pushed the master branch 2 times, most recently from c8e54eb to f772d06 Compare July 19, 2018 00:27
Copy link
Member

@theckman theckman left a comment

Choose a reason for hiding this comment

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

@rkuris thank you for being the first outside contributor to this project. 👍

I like the direction we took with changing the method names from the original PR. I do have some small things we can tweak to make this PR even more polished, in my opinion.

uuid.go Outdated
@@ -153,6 +156,28 @@ func (u *UUID) SetVariant(v byte) {
}
}

// UnixNanoOf returns the number of nanoseconds in a V1 UUID
Copy link
Member

Choose a reason for hiding this comment

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

Just to be very clear to consumers, would you mind adding a quip about it being the number of nanoseconds since the UNIX epoch? I definitely don't want to nitpick copy, but I'm thinking something like:

// UnixNanoOf returns the number of nanoseconds elapsed since January 1, 1970 UTC in a V1 UUID

It might also be good to indicate that an error is returned for a non-V1 UUID here too (like we did with TimeOf).

Finally, please add punctuation to the sentence just so that the documentation reads better.

uuid.go Outdated
low := binary.BigEndian.Uint32(u[0:4])
mid := binary.BigEndian.Uint16(u[4:6])
hi := binary.BigEndian.Uint16(u[6:8]) & 0xfff
return 100 * (int64(low) + (int64(mid) << 32) + (int64(hi) << 48) - int64(epochStart)), nil
Copy link
Member

Choose a reason for hiding this comment

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

When you have a moment would you please add some spacing to this block of code and put the return calculation on a separate line (for readability purposes)? For spacing I'm just thinking of an empty line before the low line and after the hi line.

For the return, I think it'd be okay to put the calculation on its own line so that we can then have return var, nil. For me at least, it made it easier to grok on a quick look.

uuid.go Outdated

// TimeOf returns the time embedded in a V1 UUID
// An error is returned if this is not a V1 UUID
func (u *UUID) TimeOf() (time.Time, error) {
Copy link
Member

Choose a reason for hiding this comment

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

Would you mind converting this to a function on the package that takes the UUID as input? Because this method only takes a UUID, I think it's more appropriate to be a function. If the UUID wasn't the input to the method, but was the state needed for the method to take action on the input, then I'd say 👍 on it being a method.

uuid.go Outdated
@@ -153,6 +156,28 @@ func (u *UUID) SetVariant(v byte) {
}
}

// UnixNanoOf returns the number of nanoseconds in a V1 UUID
func (u *UUID) UnixNanoOf() (int64, error) {
Copy link
Member

Choose a reason for hiding this comment

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

Would you be able to make this a package function too?

uuid.go Outdated
return 100 * (int64(low) + (int64(mid) << 32) + (int64(hi) << 48) - int64(epochStart)), nil
}

// TimeOf returns the time embedded in a V1 UUID
Copy link
Member

Choose a reason for hiding this comment

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

This may seem pedantic, but would you be able to add punctuation after UUID on this line? I'll make the GoDoc read better.

t.Run("TestTimeOfErrors", testTimeOfErrors)
}

func testTimeOfErrors(t *testing.T) {
Copy link
Member

Choose a reason for hiding this comment

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

Can we also add a test that would use a constant UUID, and test to make sure that UnixNanosOf() and TimeOf() parse the time out of it properly?

Copy link
Member

@acln0 acln0 left a comment

Choose a reason for hiding this comment

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

Thanks! A few minor nitpicks from me too.

@@ -45,6 +45,7 @@ func testNewV1(t *testing.T) {
t.Run("FaultyRand", testNewV1FaultyRand)
t.Run("MissingNetwork", testNewV1MissingNetwork)
t.Run("MissingNetworkFaultyRand", testNewV1MissingNetworkFaultyRand)
t.Run("V1Ordering", testNewV1Ordering)
Copy link
Member

Choose a reason for hiding this comment

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

Please change "V1Ordering" to "Ordering", since this is a subtest of testNewV1.

t.Fatal(err)
}
if n1 > n2 {
t.Errorf("generated older UUID: %s > %s", u1, u2)
Copy link
Member

Choose a reason for hiding this comment

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

Perhaps this message should print the nanoseconds, like the one below prints time.

t.Run("TestTimeOfErrors", testTimeOfErrors)
}

func testTimeOfErrors(t *testing.T) {
Copy link
Member

Choose a reason for hiding this comment

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

This test does not test a property of NewV2, so I don't think it should be a subtest of testNewV2.

@zerkms
Copy link
Member

zerkms commented Jul 19, 2018

After giving it some more thoughts and reading the spec I came with the following new types to introduce

// 100s of nanos since 00:00:00.00, 15 October 1582
type V1Timestamp int64

func TimestampFromV1(UUID) (V1Timestamp, error)

func TimeFromV1Timestamp(V1Timestamp) (time.Time, error)

Rationale behind it:

  • Those are functions not methods of UUID since they make sense only for V1 and not the other types of UUID

  • The V1Timestamp type specifically chosen according the RFC terminology (https://tools.ietf.org/html/rfc4122#section-4.1.4)

  • V1Timestamp values are comparable by themselves, if one needs exact time - there is a converting function for convenience

@acln0
Copy link
Member

acln0 commented Jul 19, 2018

@zerkms I like this API very much. It's a little verbose, but it's very explicit, and it encodes the notion that timestamps are 100s of nanoseconds since the Gregorian calendar in a type. Big +1 from me.

@niaow
Copy link
Member

niaow commented Jul 19, 2018

Would it be a better idea to do V1Timestamp.Time() or TimeFromV1Timestamp(V1Timestamp)?

@acln0
Copy link
Member

acln0 commented Jul 19, 2018

I have no opinion on the method vs. top-level function matter, but I do like the proposed data type, and I think we should use it.

@rkuris
Copy link
Contributor Author

rkuris commented Jul 19, 2018 via email

@niaow
Copy link
Member

niaow commented Jul 19, 2018

@rkuris it doesn't really make sense to do both. I would be fine with either.

@theckman
Copy link
Member

I would prefer we only offer a function, and not both.

@acln0
Copy link
Member

acln0 commented Jul 21, 2018

I agree with not offering both. Let's have one, obvious way to do things.

That being said, an argument can be made for the method variant, based on prior cases. Note, for example, that the time package offers https://golang.org/pkg/time/#Time.Minute and https://golang.org/pkg/time/#Time.Month rather than MinuteFromTime and MonthFromTime.

I wouldn't be opposed to an API like

func (u UUID) V1Timestamp() (V1Timestamp, error)

and

func (t V1Timestamp) Time() (time.Time, error)

Both this and the function-based API read well to me. I suppose it's a question of taste and style. We need a resolution nevertheless, so we can move forward with the proposal.

@theckman
Copy link
Member

theckman commented Jul 21, 2018

I think I'm hesitant to have it on the type because not every UUID is a V1 UUID. I'm looking at it from the perspective of what the implicit meaning of the implementation is. I think having it on the type implies the wrong thing.

@acln0
Copy link
Member

acln0 commented Jul 21, 2018

@theckman Thanks. That's a very good point, and I am now leaning strongly towards the function variant as well.

@theckman
Copy link
Member

theckman commented Jul 28, 2018

We've had quite a few discussions in this PR, so I think it'd be good to summarize what we've talked about.

I think the agreement is that we want to create a new unsigned integer timestamp type [1], to represent the number of 100-nanosecond intervals since 00:00:00.00, 15 October 1582 [2]. This is to be closer to the RFC and how it thinks about time. A user will obtain one of these timestamps by calling a function on the package, not a method on a type within the package, while giving it a V1 UUID which it can parse the time out of.

To then get a time.Time value, I'm of the opinion that it should be a method on this timestamp type (taking inspiration from the time package).

I like the idea of the proposal so far, but I've been thinking and I want to propose a slight tweak and see what people think. Based on reading the RFC again, the "timestamp" of V2 ~ V5 UUIDs isn't actually a timestamp at all, confirming that only applies to V1. So with this I think specifying V1 is a bit redundant. We can just mention it in the docs for those who are familiar with the RFC.

Here's what I was thinking of, to expand on the example above:

// Timestamp is the count of 100-nanosecond intervals since 00:00:00.00,
// 15 October 1582 within a V1 UUID. This type has no meaning for
// V2 ~ V5 UUIDs as they don't have an embedded timestamp .
type Timestamp uint64

// Time returns the UTC time.Time representation of the Timestamp.
func (t Timestamp) Time() (time.Time, error) {}

// TimestampFromV1 returns the Timestamp embedded within a V1 UUID.
// Returns an error if the UUID is any version other than 1.
func TimestampFromV1(u UUID) (Timestamp, error) {}

What does everyone think? Would this be acceptable for everyone as the API we want to support?

[1] https://tools.ietf.org/html/rfc4122#section-4.2.2
[2] https://tools.ietf.org/html/rfc4122#section-4.1.4

@theckman theckman modified the milestones: 2.2.0, 2.3.0 Jul 28, 2018
@niaow niaow added the enhancement New feature or request label Jul 28, 2018
@theckman
Copy link
Member

@rkuris let us know if there's anything we can do to help with this PR.

@rkuris
Copy link
Contributor Author

rkuris commented Jul 30, 2018 via email

@theckman
Copy link
Member

@rkuris I wanted to follow back up and see if there's anything we can do to help you with this PR. I think we'd be happy to adapt your work and raise a separate PR, if you're okay with that. If we do go that route, I'd like to do it in a way that tries to make sure we are able to still attribute the contribution to you in some capacity.

@rkuris
Copy link
Contributor Author

rkuris commented Aug 13, 2018 via email

@rkuris
Copy link
Contributor Author

rkuris commented Aug 18, 2018

Testing the edge cases turned up a bug in how times were constructed in the previous PR. I'll make a note in the satori.uuid repo to that effect.

@rkuris
Copy link
Contributor Author

rkuris commented Aug 21, 2018

Greetings,

I think all the comments have been addressed by this rewrite. Is this ready to merge now or does this need more work?

const _100nsPerSecond = 10000000

// Time returns the UTC time.Time representation of a Timestamp
func (t Timestamp) Time() (time.Time, error) {
Copy link
Member

Choose a reason for hiding this comment

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

Is error as a second return value here for potential unknown cases in future to avoid BCs, or is there any other reason?

I'm not against, or pro- having it, just curious.

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 think that's the idea. You can use Must(thing.Time()) if you don't care about errors.

uuid.go Outdated
low := binary.BigEndian.Uint32(u[0:4])
mid := binary.BigEndian.Uint16(u[4:6])
hi := binary.BigEndian.Uint16(u[6:8]) & 0xfff
return Timestamp(low) + (Timestamp(mid) << 32) + (Timestamp(hi) << 48), nil
Copy link
Member

Choose a reason for hiding this comment

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

I think using Timestamp type here is confusing.

How about doing math + bit shift operations with uint64 first?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

A Timestamp is a uint64, and we need to return one. Why is it confusing?

Copy link
Member

Choose a reason for hiding this comment

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

I'm a proponent of coding against types: I strongly believe that types carry a lot of extra context.

In this case, keeping in mind that Timestamp is a type that represents number of 100s of nanoseconds since 00:00:00.00 15 October 1582, the Timestamp(mid) << 32 operation makes very little sense: what does "shift 100s of nanoseconds 32 bits left" mean?

It's a mathematical operation and should be applied to numbers, which then (in the very end) might be interpreted as something domain-specific.

PS: it's my personal opinion, if other people are okay - I won't insist on this change too hard.

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 changed it. I don't have a strong opinion either way, but to me it seems harder to read after making the change (a ton of casting is now happening).

I suppose I could break it up into multiple lines but it still seems "wordier". I don't have a strong opinion though.

Copy link
Member

Choose a reason for hiding this comment

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

It looks ideal to me now :-)

@@ -133,3 +134,51 @@ func TestMust(t *testing.T) {
}
Must(fn())
}

func TestTimeFromTimestamp(t *testing.T) {
tests := []struct {
Copy link
Member

Choose a reason for hiding this comment

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

gofmt please.

Following the code review suggestions, added a new Timestamp type,
and broke the code into two functions, one to get the timestamp from
a V1 UUID and one to convert the timestamp to a time.Time value.
Also changed from Timestamp shifts to uint64 shifts
@acln0
Copy link
Member

acln0 commented Aug 21, 2018

LGTM now. Ping @theckman.

@theckman
Copy link
Member

I'll review / merge tomorrow morning (in ~8 hours).

Copy link
Member

@theckman theckman left a comment

Choose a reason for hiding this comment

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

Thank you so much for this contribution!

@theckman theckman merged commit bfaa575 into gofrs:master Aug 22, 2018
theckman added a commit that referenced this pull request Aug 22, 2018
Add Time() and Nanos() methods

Signed-off-by: Tim Heckman <t@heckman.io>
@theckman
Copy link
Member

New release cut that includes this change: https://github.com/gofrs/uuid/releases/tag/v3.1.0

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

7 participants