adrianmo / go-nmea

A NMEA parser library in pure Go
MIT License
226 stars 77 forks source link

Convert nmea.Date and nmea.Time to time.Time #115

Closed mholt closed 1 month ago

mholt commented 1 month ago

First, thank you for this excellent package. :pray: It's perfectly what I need, and I'm relieved to see it maintained, esp. from developers I recognize!

I'm using it to parse GPS log data from amateur radios.

In particular, I'm finding that it's common to encounter GGA and RMC sentences alternating in a log file. RMC lines have time and date fields; GGA only has time.

I'm wondering if there's a convenient function (or one could be created) that takes both nmea.Date and nmea.Time and returns a time.Time. Once I get my spike code up and running I could contribute this if you think it'd be a good addition.

Edit: Haven't run this yet, but is it just:

time.Date(2000+s.Date.YY, time.Month(s.Date.MM), s.Date.DD,
    s.Time.Hour, s.Time.Minute, s.Time.Second, 0,
    time.UTC)

? Or is there more nuance to it?

PS. How do you know the first two digits of the year? I guess by now we can assume the 2000s, but is it possible that any data was collected in the 20th century?

aldas commented 1 month ago

Seems that first GPS satellites were launched 1978 https://en.wikipedia.org/wiki/Global_Positioning_System Internet says that first commercial GPS receivers arrived 1989.

1985 — The U.S. government opens contracts with private companies to create portable GPS receivers. 1989 — GPS company, Magellan, introduces the first hand-held GPS device, the NAV 1000. The first fully operational satellite is launched by the U.S. Air Force as a part of their Block II program.

I found very old NMEA FAQ example with date from 1994 191194 Date of fix 19 November 1994

Internet says that NMEA 0183 was standardized 1983

NMEA 0183 is a voluntary industry standard, first released in March of 1983

I asked one of our navigation engineer that has access to 0183 spec and he did not find any special rules about dates from it.

aldas commented 1 month ago

side note: I do not know about amateur radios but some of the marine GPS devices can send empty RMC sentences when there is no fix yet (or device is in calibration mode - which for example, for some devices, can be entered into after cycling the power and doing certain maneuver in first 2 minutes). So having checks like rmc.Validity == nmea.ValidRMC makes sense ($GPRMC,,V,,,,,,,,,,N*53)

icholy commented 1 month ago

Here's what I'm thinking for the signature:

// DateTime returns a time.Time computed from the provided Date and Time values.
// The reference time ref is used to determine the century for the two-digit year in Date.
// If either Date or Time is not valid, DateTime returns the zero time.Time.
func DateTime(ref time.Time, d Date, t Time) time.Time

The ref should be the time when the sentence was created (or possibly a fixed time depending on the device).

PS. How do you know the first two digits of the year? I guess by now we can assume the 2000s, but is it possible that any data was collected in the 20th century?

This question is exactly why we've avoided adding a function like this so far :sweat:

mholt commented 1 month ago

Ah, so it is ambiguous. Well, it makes sense. Thanks for clarifying.

What if instead of an entire time.Time for the reference, the user just passes in a number of years offset, like 1900 or 2000? (Or even just 19 or 20. The docs could then even say which two values are the most common inputs.)

icholy commented 1 month ago

How about this:

// DateTime returns a time.Time computed from the provided Date and Time values.
// The century parameter is used to determine the century for the two-digit year in Date.
// If century is 0, the current century is used.
// If either Date or Time is not valid, DateTime returns the zero time.Time.
func DateTime(century int, d Date, t Time) time.Time
mholt commented 1 month ago

I like that! Maybe add something like The century value is typically 19 or 20 to represent the 20th and 21st centuries, respectively.

(I could see myself inputting 20 to represent 19xx because it's the "20th century")

icholy commented 1 month ago

I was imagining usage of the function to look like this:

nmea.DateTime(2000, d, t)

I'd want this code to work:

ref := time.Now()
nmea.DateTime(ref.Year(), d, t)

Perhaps the signature should make that clear:

// DateTime returns a time.Time computed from the provided Date and Time values.
// The centuryYear parameter is used to determine the century for the two-digit year in Date.
// If centuryYear is 0, the current century is used.
// If either Date or Time is not valid, DateTime returns the zero time.Time.
func DateTime(centuryYear int, d Date, t Time) time.Time

Maybe we should omit "century" from the parameter name all together:

// DateTime returns a time.Time computed from the provided Date and Time values.
// The referenceYear parameter is used to determine the century for the two-digit year in Date.
// If referenceYear is 0, the current year is used.
// If either Date or Time is not valid, DateTime returns the zero time.Time.
func DateTime(referenceYear int, d Date, t Time) time.Time
mholt commented 1 month ago

Gotcha gotcha -- the year as input works for me!

-The referenceYear parameter is used to determine the century for the two-digit year in Date.
+The referenceYear parameter is used to determine the offset for two-digit year in Date.

Just a minor suggestion for the godoc. :arrow_up: And then I think it's perfect, personally :100:

icholy commented 1 month ago

Sounds good to me. Are you still interested in implementing this?

mholt commented 1 month ago

Sure. I'll submit a PR for review.

icholy commented 1 month ago

Released in v1.10.0