From 6172f13193bef013ed76f89fe02e175e3405b2ed Mon Sep 17 00:00:00 2001 From: Jean-Marc MEESSEN Date: Fri, 10 Jul 2020 22:49:28 +0200 Subject: [PATCH] Add the time interpolating feature --- cmd/adif_write.go | 4 +- cmd/display.go | 111 --------------- cmd/displayLog.go | 114 ++++++++++++++++ cmd/inferTime.go | 127 ++++++++++++++++++ cmd/inferTime_test.go | 298 +++++++++++++++++++++++++++++++++++++++++ cmd/load.go | 67 ++++++++- cmd/parse_line.go | 5 + cmd/parse_line_test.go | 32 ++--- cmd/root.go | 2 + go.mod | 3 +- go.sum | 1 + test/commands.md | 3 + todo.md | 6 +- 13 files changed, 638 insertions(+), 135 deletions(-) delete mode 100644 cmd/display.go create mode 100644 cmd/displayLog.go create mode 100644 cmd/inferTime.go create mode 100644 cmd/inferTime_test.go diff --git a/cmd/adif_write.go b/cmd/adif_write.go index e7cd50c..7c1a441 100644 --- a/cmd/adif_write.go +++ b/cmd/adif_write.go @@ -45,8 +45,8 @@ func buildAdif(fullLog []LogLine) (adifList []string) { for _, logLine := range fullLog { adifLine := "" adifLine = adifLine + adifElement("STATION_CALLSIGN", logLine.MyCall) - adifLine = adifLine + adifElement("CALL", logLine.Call) - adifLine = adifLine + adifElement("QSO_DATE", adifDate(logLine.Date)) + adifLine = adifLine + adifElement("CALL", logLine.Call) + adifLine = adifLine + adifElement("QSO_DATE", adifDate(logLine.Date)) adifLine = adifLine + adifElement("TIME_ON", logLine.Time) adifLine = adifLine + adifElement("BAND", logLine.Band) adifLine = adifLine + adifElement("MODE", logLine.Mode) diff --git a/cmd/display.go b/cmd/display.go deleted file mode 100644 index 307ff0c..0000000 --- a/cmd/display.go +++ /dev/null @@ -1,111 +0,0 @@ -package cmd - -/* -Copyright © 2020 Jean-Marc Meessen, ON4KJM - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ -import ( - "fmt" -) - -// SprintLogRecord outputs the content of a logline -func SprintLogRecord(logLine LogLine) (output string) { - output = "" - output = output + "Date " + logLine.Date + "\n" - output = output + "MyCall " + logLine.MyCall + "\n" - output = output + "Operator " + logLine.Operator + "\n" - output = output + "MyWWFF " + logLine.MyWWFF + "\n" - output = output + "MySOTA " + logLine.MySOTA + "\n" - output = output + "QslMsg " + logLine.QslMsg + "\n" - output = output + "Nickname " + logLine.Nickname + "\n" - output = output + "Mode " + logLine.Mode + "\n" - output = output + "ModeType " + logLine.ModeType + "\n" - output = output + "Band " + logLine.Band + "\n" - output = output + " Lower " + fmt.Sprintf("%f", logLine.BandLowerLimit) + "\n" - output = output + " Upper " + fmt.Sprintf("%f", logLine.BandUpperLimit) + "\n" - output = output + "Frequency " + logLine.Frequency + "\n" - output = output + "Time " + logLine.Time + "\n" - output = output + "Call " + logLine.Call + "\n" - output = output + "Comment " + logLine.Comment + "\n" - output = output + "QSLmsg " + logLine.QSLmsg + "\n" - output = output + "OMname " + logLine.OMname + "\n" - output = output + "GridLoc " + logLine.GridLoc + "\n" - output = output + "RSTsent " + logLine.RSTsent + "\n" - output = output + "RSTrcvd " + logLine.RSTrcvd + "\n" - output = output + "SOTA " + logLine.SOTA + "\n" - output = output + "WWFF " + logLine.WWFF + "\n" - - return output -} - -// SprintHeaderValues displays the header values -func SprintHeaderValues(logLine LogLine) (output string) { - output = "" - - output = output + "MyCall " + logLine.MyCall - if logLine.Operator != "" { - output = output + " (" + logLine.Operator + ")" - } - output = output + "\n" - - if logLine.MyWWFF != "" { - output = output + "MyWWFF " + logLine.MyWWFF + "\n" - } - - if logLine.MySOTA != "" { - output = output + "MySOTA " + logLine.MySOTA + "\n" - } - - return output -} - -// Date, Time, band, mode, call, report sent, report rcvd, Notes -var logLineFormat = "%-10s %-4s %-4s %-4s %-10s %-4s %-4s %s \n" - -// SprintColumnTitles displays the column titles for a log line -func SprintColumnTitles(logLine LogLine) (output string) { - output = fmt.Sprintf(logLineFormat, "Date", "Time", "Band", "Mode", "Call", "Sent", "Rcvd", "Notes") - output = output + fmt.Sprintf(logLineFormat, "----", "----", "----", "----", "----", "----", "----", "----") - return output -} - -// SprintLogInColumn displays the logLine in column mode -func SprintLogInColumn(logLine LogLine) (output string) { - notes := "" - if logLine.Frequency != "" { - notes = notes + "QRG: " + logLine.Frequency + " " - } - if logLine.Comment != "" { - notes = notes + "[" + logLine.Comment + "] " - } - if logLine.QSLmsg != "" { - notes = notes + "[" + logLine.QSLmsg + "] " - } - if logLine.OMname != "" { - notes = notes + logLine.OMname + " " - } - if logLine.GridLoc != "" { - notes = notes + logLine.GridLoc + " " - } - if logLine.WWFF != "" { - notes = notes + logLine.WWFF + " " - } - if logLine.SOTA != "" { - notes = notes + logLine.SOTA + " " - } - - output = fmt.Sprintf(logLineFormat, logLine.Date, logLine.Time, logLine.Band, logLine.Mode, logLine.Call, logLine.RSTsent, logLine.RSTrcvd, notes) - - return output -} diff --git a/cmd/displayLog.go b/cmd/displayLog.go new file mode 100644 index 0000000..30c952b --- /dev/null +++ b/cmd/displayLog.go @@ -0,0 +1,114 @@ +package cmd + +/* +Copyright © 2020 Jean-Marc Meessen, ON4KJM + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +import ( + "fmt" + "strings" +) + +// SprintLogRecord outputs the content of a logline +func SprintLogRecord(logLine LogLine) string { + var output strings.Builder + + output.WriteString("Date " + logLine.Date + "\n") + output.WriteString("MyCall " + logLine.MyCall + "\n") + output.WriteString("Operator " + logLine.Operator + "\n") + output.WriteString("MyWWFF " + logLine.MyWWFF + "\n") + output.WriteString("MySOTA " + logLine.MySOTA + "\n") + output.WriteString("QslMsg " + logLine.QslMsg + "\n") + output.WriteString("Nickname " + logLine.Nickname + "\n") + output.WriteString("Mode " + logLine.Mode + "\n") + output.WriteString("ModeType " + logLine.ModeType + "\n") + output.WriteString("Band " + logLine.Band + "\n") + output.WriteString(" Lower " + fmt.Sprintf("%f", logLine.BandLowerLimit) + "\n") + output.WriteString(" Upper " + fmt.Sprintf("%f", logLine.BandUpperLimit) + "\n") + output.WriteString("Frequency " + logLine.Frequency + "\n") + output.WriteString("Time " + logLine.Time + "\n") + output.WriteString("Call " + logLine.Call + "\n") + output.WriteString("Comment " + logLine.Comment + "\n") + output.WriteString("QSLmsg " + logLine.QSLmsg + "\n") + output.WriteString("OMname " + logLine.OMname + "\n") + output.WriteString("GridLoc " + logLine.GridLoc + "\n") + output.WriteString("RSTsent " + logLine.RSTsent + "\n") + output.WriteString("RSTrcvd " + logLine.RSTrcvd + "\n") + output.WriteString("SOTA " + logLine.SOTA + "\n") + output.WriteString("WWFF " + logLine.WWFF + "\n") + + return output.String() +} + +// SprintHeaderValues displays the header values +func SprintHeaderValues(logLine LogLine) string { + var output strings.Builder + + output.WriteString("MyCall " + logLine.MyCall) + if logLine.Operator != "" { + output.WriteString(" (" + logLine.Operator + ")") + } + output.WriteString("\n") + + if logLine.MyWWFF != "" { + output.WriteString("MyWWFF " + logLine.MyWWFF + "\n") + } + + if logLine.MySOTA != "" { + output.WriteString("MySOTA " + logLine.MySOTA + "\n") + } + + return output.String() +} + +// Date, Time, band, mode, call, report sent, report rcvd, Notes +var logLineFormat = "%-10s %-4s %-4s %-4s %-10s %-4s %-4s %s \n" + +// SprintColumnTitles displays the column titles for a log line +func SprintColumnTitles(logLine LogLine) string { + var output strings.Builder + output.WriteString(fmt.Sprintf(logLineFormat, "Date", "Time", "Band", "Mode", "Call", "Sent", "Rcvd", "Notes")) + output.WriteString(fmt.Sprintf(logLineFormat, "----", "----", "----", "----", "----", "----", "----", "----")) + return output.String() +} + +// SprintLogInColumn displays the logLine in column mode +func SprintLogInColumn(logLine LogLine) (output string) { + var notes strings.Builder + if logLine.Frequency != "" { + notes.WriteString("QRG: " + logLine.Frequency + " ") + } + if logLine.Comment != "" { + notes.WriteString("[" + logLine.Comment + "] ") + } + if logLine.QSLmsg != "" { + notes.WriteString("[" + logLine.QSLmsg + "] ") + } + if logLine.OMname != "" { + notes.WriteString(logLine.OMname + " ") + } + if logLine.GridLoc != "" { + notes.WriteString(logLine.GridLoc + " ") + } + if logLine.WWFF != "" { + notes.WriteString(logLine.WWFF + " ") + } + if logLine.SOTA != "" { + notes.WriteString(logLine.SOTA + " ") + } + + output = fmt.Sprintf(logLineFormat, logLine.Date, logLine.Time, logLine.Band, logLine.Mode, logLine.Call, logLine.RSTsent, logLine.RSTrcvd, notes.String()) + + return output +} diff --git a/cmd/inferTime.go b/cmd/inferTime.go new file mode 100644 index 0000000..f7595c4 --- /dev/null +++ b/cmd/inferTime.go @@ -0,0 +1,127 @@ +package cmd + +import ( + "errors" + "fmt" + "log" + "os" + "strings" + "time" +) + +/* +Copyright © 2020 Jean-Marc Meessen, ON4KJM + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +//InferTimeBlock contains the information describing a time gap +type InferTimeBlock struct { + lastRecordedTime time.Time + nextValidTime time.Time + //Number of records without actual time + noTimeCount int + //Position in file of the first log entry with missing date + logFilePosition int + //Computed time interval + deltatime time.Duration +} + +//ADIFdateTimeFormat describes the ADIF date & time parsing and displaying format pattern +const ADIFdateTimeFormat = "2006-01-02 1504" + +//displayTimeGapInfo will print the details stored in an InferTimeBlock +func (tb *InferTimeBlock) String() string { + var buffer strings.Builder + buffer.WriteString(fmt.Sprintf("Last Recorded Time: %s\n", tb.lastRecordedTime.Format(ADIFdateTimeFormat))) + buffer.WriteString(fmt.Sprintf("next Recorded Time: %s\n", tb.nextValidTime.Format(ADIFdateTimeFormat))) + buffer.WriteString(fmt.Sprintf("Log position of last recorded time: %d\n", tb.logFilePosition)) + buffer.WriteString(fmt.Sprintf("Nbr of entries without time: %d\n", tb.noTimeCount)) + buffer.WriteString(fmt.Sprintf("Computed interval: %ds\n", int(tb.deltatime.Seconds()))) + return buffer.String() +} + +//finalizeTimeGap makes the necessary checks and computation +func (tb *InferTimeBlock) finalizeTimeGap() error { + + //Check that lastRecordedTime and nextValidTime are not null + if tb.lastRecordedTime.IsZero() { + return errors.New("Gap start time is empty") + } + if tb.nextValidTime.IsZero() { + return errors.New("Gap end time is empty") + } + + //Are the two times equal? + if tb.nextValidTime == tb.lastRecordedTime { + return errors.New("The start and end gap times are equal") + } + + //Fail if we have a negative time difference + if tb.nextValidTime.Before(tb.lastRecordedTime) { + return errors.New("Gap start time is later than the Gap end time") + } + + //Compute the gap + diff := tb.nextValidTime.Sub(tb.lastRecordedTime) + tb.deltatime = time.Duration(diff / time.Duration(tb.noTimeCount+1)) + + //Do we have a positive noTimeCount + if tb.noTimeCount < 1 { + return fmt.Errorf("Invalid number of records without time (%d)", tb.noTimeCount) + } + + //TODO: What should we expect as logFilePosition? + + return nil +} + +//storeTimeGap updates an InferTimeBLock (last valid time, nbr of records without time). It returns true if we reached the end of the time gap. +func (tb *InferTimeBlock) storeTimeGap(logline LogLine, position int) (bool, error) { + var err error + + //TODO: try to return fast and/or simpllify + + //ActualTime is filled if a time could be found in the FLE input + if logline.ActualTime != "" { + //Are we starting a new block + if tb.noTimeCount == 0 { + if tb.lastRecordedTime, err = time.Parse(ADIFdateTimeFormat, logline.Date+" "+logline.ActualTime); err != nil { + log.Println("Fatal error during internal date concersion: ", err) + os.Exit(1) + } + tb.logFilePosition = position + } else { + // We reached the end of the gap + if tb.lastRecordedTime.IsZero(){ + return false, errors.New("Gap start time is empty") + } + if tb.nextValidTime, err = time.Parse(ADIFdateTimeFormat, logline.Date+" "+logline.ActualTime); err != nil { + log.Println("Fatal error during internal date concersion: ", err) + os.Exit(1) + } + return true, nil + } + } else { + //Check the data is correct. + if tb.lastRecordedTime.IsZero() { + err = errors.New("Gap start time is empty") + //TODO: this smells + } + if !tb.nextValidTime.IsZero() { + err = errors.New("Gap end time is not empty") + } + tb.noTimeCount++ + } + return false, err +} diff --git a/cmd/inferTime_test.go b/cmd/inferTime_test.go new file mode 100644 index 0000000..0249d93 --- /dev/null +++ b/cmd/inferTime_test.go @@ -0,0 +1,298 @@ +package cmd + +import ( + "fmt" + "testing" + "time" +) + +func TestInferTimeBlock_full_happyCase(t *testing.T) { + //Given + recordNumber := 4 + + logLine1 := LogLine{} + logLine1.Date = "2020-05-24" + logLine1.Time = "1401" + logLine1.ActualTime = "1401" + + logLine2 := LogLine{} + logLine2.Date = "2020-05-24" + logLine2.Time = "1401" + + logLine3 := LogLine{} + logLine3.Date = "2020-05-24" + logLine3.Time = "1410" + logLine3.ActualTime = "1410" + + //When + tb := InferTimeBlock{} + isEndGap, err := tb.storeTimeGap(logLine1, recordNumber) + if isEndGap == true || err != nil { + t.Error("Unexpected results processing logline 1") + } + + isEndGap, err = tb.storeTimeGap(logLine2, recordNumber+1) + if isEndGap == true || err != nil { + t.Error("Unexpected results processing logline 2") + } + + isEndGap, err = tb.storeTimeGap(logLine3, recordNumber+2) + if isEndGap == false || err != nil { + t.Error("Unexpected results processing logline 3") + } + + err = tb.finalizeTimeGap() + if err != nil { + t.Errorf("Unexpected error finalizing the timeGap") + } + + fmt.Println(tb.String()) + + //Then + expectedCount := 1 + if tb.noTimeCount != expectedCount { + t.Errorf("Unexpected number of missing records: %d, expected %d", tb.noTimeCount, expectedCount) + } + + expectedInterval := time.Duration(time.Second * 270) + if tb.deltatime != expectedInterval { + t.Errorf("Unexpected interval: %d, expected %d", tb.deltatime, expectedInterval) + } + + expectedLastRecordedTime := time.Date(2020, time.May, 24, 14, 01, 0, 0, time.UTC) + if tb.lastRecordedTime != expectedLastRecordedTime { + t.Errorf("Unexpected last recorded time: %s, expected %s", tb.lastRecordedTime, expectedLastRecordedTime) + } + + expectedNextValidTime := time.Date(2020, time.May, 24, 14, 10, 0, 0, time.UTC) + if tb.nextValidTime != expectedNextValidTime { + t.Errorf("Unexpected last recorded time: %s, expected %s", tb.nextValidTime, expectedNextValidTime) + } +} + +func TestInferTimeBlock_display_happyCase(t *testing.T) { + //Given + tb := InferTimeBlock{} + tb.lastRecordedTime = time.Date(2020, time.May, 24, 14, 01, 0, 0, time.UTC) + tb.nextValidTime = time.Date(2020, time.May, 24, 14, 10, 10, 0, time.UTC) + tb.noTimeCount = 1 + + //When + buffer1 := tb.String() + + tb.finalizeTimeGap() + + buffer2 := tb.String() + + //Then + expectedBuffer1 := "Last Recorded Time: 2020-05-24 1401\nnext Recorded Time: 2020-05-24 1410\nLog position of last recorded time: 0\nNbr of entries without time: 1\nComputed interval: 0s\n" + expectedBuffer2 := "Last Recorded Time: 2020-05-24 1401\nnext Recorded Time: 2020-05-24 1410\nLog position of last recorded time: 0\nNbr of entries without time: 1\nComputed interval: 275s\n" + + if buffer1 != expectedBuffer1 { + t.Errorf("Not the expected display: got: \n%s\n while expecting: \n%s\n", buffer1, expectedBuffer1) + } + if buffer2 != expectedBuffer2 { + t.Errorf("Not the expected finalized display: got: \n%s\n while expecting: \n%s\n", buffer2, expectedBuffer2) + } +} + +func TestInferTimeBlock_computeGaps_invalidData(t *testing.T) { + //Given + tb := InferTimeBlock{} + + //When + err := tb.finalizeTimeGap() + + //Then + if err == nil { + t.Error("Should have failed with an error") + } + if err.Error() != "Gap start time is empty" { + t.Error("Did not not fail with the expected error.") + } +} + +func TestInferTimeBlock_computeGaps_missingEnTime(t *testing.T) { + //Given + tb := InferTimeBlock{} + tb.lastRecordedTime = time.Date(2020, time.May, 24, 14, 01, 0, 0, time.UTC) + + //When + err := tb.finalizeTimeGap() + + //Then + if err == nil { + t.Error("Should have failed with an error") + } + if err.Error() != "Gap end time is empty" { + t.Errorf("Did not not fail with the expected error. Failed with %s", err) + } +} + +func TestInferTimeBlock_computeGaps_negativeDifference(t *testing.T) { + //Given + tb := InferTimeBlock{} + tb.lastRecordedTime = time.Date(2020, time.May, 24, 14, 10, 0, 0, time.UTC) + tb.nextValidTime = time.Date(2020, time.May, 24, 14, 01, 10, 0, time.UTC) + + //When + err := tb.finalizeTimeGap() + + //Then + if err == nil { + t.Error("Should have failed with an error") + } + if err.Error() != "Gap start time is later than the Gap end time" { + t.Errorf("Did not not fail with the expected error. Failed with %s", err) + } +} + +func TestInferTimeBlock_computeGaps_noDifference(t *testing.T) { + //Given + tb := InferTimeBlock{} + tb.lastRecordedTime = time.Date(2020, time.May, 24, 14, 00, 0, 0, time.UTC) + tb.nextValidTime = time.Date(2020, time.May, 24, 14, 00, 00, 0, time.UTC) + + //When + err := tb.finalizeTimeGap() + + //Then + if err == nil { + t.Error("Should have failed with an error") + } + if err.Error() != "The start and end gap times are equal" { + t.Errorf("Did not not fail with the expected error. Failed with %s", err) + } +} + +func TestInferTimeBlock_computeGaps_happyCase(t *testing.T) { + //Given + tb := InferTimeBlock{} + tb.lastRecordedTime = time.Date(2020, time.May, 24, 14, 01, 0, 0, time.UTC) + tb.nextValidTime = time.Date(2020, time.May, 24, 14, 10, 10, 0, time.UTC) + tb.noTimeCount = 1 + + //When + err := tb.finalizeTimeGap() + + //Then + if err != nil { + t.Error("Should not have failed") + } + + //TODO: add some other validation +} + +func TestInferTimeBlock_startsNewBlock(t *testing.T) { + // Given + logLine := LogLine{} + logLine.Date = "2020-05-24" + logLine.Time = "1401" + logLine.ActualTime = "1401" + + recordNbr := 4 + + tb := InferTimeBlock{} + + // When + isEndGap, err := tb.storeTimeGap(logLine, recordNbr) + + // Then + if err != nil { + t.Errorf("Unexpected error: %s", err) + } + if isEndGap == true { + t.Errorf("Result is true while expectig false") + } + if tb.lastRecordedTime != time.Date(2020, time.May, 24, 14, 01, 0, 0, time.UTC) { + t.Errorf("Not the expected lastRecordedTime") + } + if tb.noTimeCount != 0 { + t.Errorf("nTimeCount should be 0, but is %d", tb.noTimeCount) + } + if tb.logFilePosition != recordNbr { + t.Errorf("logFilePosition not set correctly: is %d while expecting %d", tb.logFilePosition, recordNbr) + } +} + +func TestInferTimeBlock_incrementCounter(t *testing.T) { + // Given + logLine := LogLine{} + logLine.Date = "2020-05-24" + logLine.Time = "1401" + + recordNbr := 4 + + tb := InferTimeBlock{} + tb.lastRecordedTime = time.Date(2020, time.May, 24, 14, 01, 0, 0, time.UTC) + tb.logFilePosition = recordNbr + + // When + isEndGap, err := tb.storeTimeGap(logLine, recordNbr) + + // Then + if err != nil { + t.Errorf("Unexpected error: %s", err) + } + if isEndGap == true { + t.Errorf("Result is true while expectig false") + } + if tb.lastRecordedTime != time.Date(2020, time.May, 24, 14, 01, 0, 0, time.UTC) { + t.Errorf("Not the expected lastRecordedTime") + } + if tb.noTimeCount != 1 { + t.Errorf("nTimeCount should be 1, but is %d", tb.noTimeCount) + } + if tb.logFilePosition != recordNbr { + t.Errorf("logFilePosition not set correctly: is %d while expecting %d", tb.logFilePosition, recordNbr) + } +} + +func TestInferTimeBlock_increment_missingLastTime(t *testing.T) { + // Given + logLine := LogLine{} + logLine.Date = "2020-05-24" + logLine.Time = "1401" + + recordNbr := 4 + + tb := InferTimeBlock{} + //tb.lastRecordedTime = time.Date(2020, time.May, 24, 14, 01, 0, 0, time.UTC) + tb.logFilePosition = recordNbr + + // When + isEndGap, err := tb.storeTimeGap(logLine, recordNbr) + + // Then + if err.Error() != "Gap start time is empty" { + t.Errorf("Unexpected error: %s", err) + } + if isEndGap == true { + t.Errorf("Result is true while expectig false") + } +} + +func TestInferTimeBlock_increment_alreadyDefinedNewTime(t *testing.T) { + // Given + logLine := LogLine{} + logLine.Date = "2020-05-24" + logLine.Time = "1401" + + recordNbr := 4 + + tb := InferTimeBlock{} + tb.lastRecordedTime = time.Date(2020, time.May, 24, 14, 01, 0, 0, time.UTC) + tb.nextValidTime = time.Date(2020, time.May, 24, 14, 01, 0, 0, time.UTC) + tb.logFilePosition = recordNbr + + // When + isEndGap, err := tb.storeTimeGap(logLine, recordNbr) + + // Then + if err.Error() != "Gap end time is not empty" { + t.Errorf("Unexpected error: %s", err) + } + if isEndGap == true { + t.Errorf("Result is true while expectig false") + } +} diff --git a/cmd/load.go b/cmd/load.go index f052375..5f1eaa6 100644 --- a/cmd/load.go +++ b/cmd/load.go @@ -19,10 +19,14 @@ limitations under the License. import ( "bufio" "fmt" - "github.com/spf13/cobra" "log" "os" "regexp" + "time" + + //"time" + + "github.com/spf13/cobra" //"strings" ) @@ -101,6 +105,9 @@ func loadFile() (filleFullLog []LogLine, isProcessedOK bool) { headerDate := "" lineCount := 0 + wrkTimeBlock := InferTimeBlock{} + missingTimeBlockList := []InferTimeBlock{} + var isInMultiLine = false var cleanedInput []string var errorLog []string @@ -254,16 +261,70 @@ func loadFile() (filleFullLog []LogLine, isProcessedOK bool) { previousLogLine.Nickname = headerNickname previousLogLine.Date = headerDate - // + //parse a line logline, errorLine := ParseLine(eachline, previousLogLine) + + //we have a valid line (contains a call) if logline.Call != "" { fullLog = append(fullLog, logline) + + //store time inference data + if isInterpolateTime { + var isEndOfGap bool + if isEndOfGap, err = wrkTimeBlock.storeTimeGap(logline, len(fullLog)); err != nil { + log.Println("Fatal error: ", err) + os.Exit(1) + } + //If we reached the end of the time gap, we make the necessary checks and make our gap calculation + if isEndOfGap { + if err := wrkTimeBlock.finalizeTimeGap(); err != nil { + //If an error occured it is a fatal error + log.Println("Fatal error: ", err) + os.Exit(1) + } + + //add it to the gap collection + missingTimeBlockList = append(missingTimeBlockList, wrkTimeBlock) + + //create a new block + wrkTimeBlock = InferTimeBlock{} + + //Store this record in the new block as a new gap might be following + //no error or endOfGap processing as it has already been succesfully processed + wrkTimeBlock.storeTimeGap(logline, len(fullLog)) + } + } } + + //Store append the accumulated soft parsing errors into the global parsing error log file if errorLine != "" { errorLog = append(errorLog, fmt.Sprintf("Parsing error at line %d: %s ", lineCount, errorLine)) } + + //store the current logline so that it can be used as a model when parsing the next line previousLogLine = logline - //Go back to the top (Continue not necessary) + + //We go back to the top to process the next loaded log line (Continue not necessary here) + } + + //*** + //*** We have done processing the log file, so let's post process it + //*** + + //if asked to infer the date, lets update the loaded logfile accordingly + if isInterpolateTime { + for _, timeBlock := range missingTimeBlockList { + for i := 0; i < timeBlock.noTimeCount; i++ { + position := timeBlock.logFilePosition + i + pLogLine := &fullLog[position] + + // durationOffset := time.Second * time.Duration(timeBlock.deltatime*(i+1)) + durationOffset := timeBlock.deltatime * time.Duration(i+1) + newTime := timeBlock.lastRecordedTime.Add(durationOffset) + updatedTimeString := newTime.Format("1504") + pLogLine.Time = updatedTimeString + } + } } displayLogSimple(fullLog) diff --git a/cmd/parse_line.go b/cmd/parse_line.go index f109f6a..6673648 100644 --- a/cmd/parse_line.go +++ b/cmd/parse_line.go @@ -42,6 +42,7 @@ type LogLine struct { BandUpperLimit float64 Frequency string Time string + ActualTime string //time actually recorded in FLE Call string Comment string QSLmsg string @@ -80,6 +81,7 @@ func ParseLine(inputStr string, previousLine LogLine) (logLine LogLine, errorMsg previousLine.OMname = "" previousLine.GridLoc = "" previousLine.Comment = "" + previousLine.ActualTime = "" logLine = previousLine //TODO: what happens when we have <> or when there are multiple comments @@ -155,6 +157,7 @@ func ParseLine(inputStr string, previousLine LogLine) (logLine LogLine, errorMsg if isRightOfCall == false { if regexpIsFullTime.MatchString(element) { logLine.Time = element + logLine.ActualTime = element continue } @@ -162,9 +165,11 @@ func ParseLine(inputStr string, previousLine LogLine) (logLine LogLine, errorMsg if regexpIsTimePart.MatchString(element) { if logLine.Time == "" { logLine.Time = element + logLine.ActualTime = element } else { goodPart := logLine.Time[:len(logLine.Time)-len(element)] logLine.Time = goodPart + element + logLine.ActualTime = goodPart + element } continue } diff --git a/cmd/parse_line_test.go b/cmd/parse_line_test.go index f8ef6b0..1501a46 100644 --- a/cmd/parse_line_test.go +++ b/cmd/parse_line_test.go @@ -24,27 +24,27 @@ func TestParseLine(t *testing.T) { { "Parse for time", args{inputStr: "1314 g3noh", previousLine: LogLine{Mode: "SSB"}}, - LogLine{Time: "1314", Call: "G3NOH", Mode: "SSB", RSTsent: "59", RSTrcvd: "59"}, "", + LogLine{Time: "1314", ActualTime: "1314", Call: "G3NOH", Mode: "SSB", RSTsent: "59", RSTrcvd: "59"}, "", }, { "Parse partial time - 1", args{inputStr: "4 g3noh", previousLine: LogLine{Time: "", Mode: "SSB"}}, - LogLine{Time: "4", Call: "G3NOH", Mode: "SSB", RSTsent: "59", RSTrcvd: "59"}, "", //TODO: should fail + LogLine{Time: "4", ActualTime: "4", Call: "G3NOH", Mode: "SSB", RSTsent: "59", RSTrcvd: "59"}, "", //TODO: should fail }, { "Parse partial time - 2", args{inputStr: "15 g3noh", previousLine: LogLine{Time: "1200", Mode: "SSB"}}, - LogLine{Time: "1215", Call: "G3NOH", Mode: "SSB", RSTsent: "59", RSTrcvd: "59"}, "", + LogLine{Time: "1215", ActualTime: "1215", Call: "G3NOH", Mode: "SSB", RSTsent: "59", RSTrcvd: "59"}, "", }, { "Parse partial time - 3", args{inputStr: "4 g3noh", previousLine: LogLine{Time: "1200", Mode: "SSB"}}, - LogLine{Time: "1204", Call: "G3NOH", Mode: "SSB", RSTsent: "59", RSTrcvd: "59"}, "", + LogLine{Time: "1204", ActualTime: "1204", Call: "G3NOH", Mode: "SSB", RSTsent: "59", RSTrcvd: "59"}, "", }, { "Parse for comment", args{inputStr: "4 g3noh ", previousLine: LogLine{Mode: "SSB"}}, - LogLine{Time: "4", Comment: "PSE QSL Direct", Call: "G3NOH", Mode: "SSB", RSTsent: "59", RSTrcvd: "59"}, "", + LogLine{Time: "4", ActualTime: "4", Comment: "PSE QSL Direct", Call: "G3NOH", Mode: "SSB", RSTsent: "59", RSTrcvd: "59"}, "", }, { "Parse for QSL", @@ -79,32 +79,32 @@ func TestParseLine(t *testing.T) { { "parse partial RST (sent) - CW", args{inputStr: "1230 on4kjm 5", previousLine: LogLine{Mode: "CW", ModeType: "CW"}}, - LogLine{Call: "ON4KJM", Time: "1230", RSTsent: "559", RSTrcvd: "599", Mode: "CW", ModeType: "CW"}, "", + LogLine{Call: "ON4KJM", Time: "1230", ActualTime: "1230", RSTsent: "559", RSTrcvd: "599", Mode: "CW", ModeType: "CW"}, "", }, { "parse partial RST (received) - CW", args{inputStr: "1230 on4kjm 5 44", previousLine: LogLine{Mode: "CW", ModeType: "CW"}}, - LogLine{Call: "ON4KJM", Time: "1230", RSTsent: "559", RSTrcvd: "449", Mode: "CW", ModeType: "CW"}, "", + LogLine{Call: "ON4KJM", Time: "1230", ActualTime: "1230", RSTsent: "559", RSTrcvd: "449", Mode: "CW", ModeType: "CW"}, "", }, { "parse full RST (received) - CW", args{inputStr: "1230 on4kjm 5 448", previousLine: LogLine{Mode: "CW", ModeType: "CW"}}, - LogLine{Call: "ON4KJM", Time: "1230", RSTsent: "559", RSTrcvd: "448", Mode: "CW", ModeType: "CW"}, "", + LogLine{Call: "ON4KJM", Time: "1230", ActualTime: "1230", RSTsent: "559", RSTrcvd: "448", Mode: "CW", ModeType: "CW"}, "", }, { "parse partial report (sent) - FM", args{inputStr: "1230 on4kjm 5", previousLine: LogLine{Mode: "FM", ModeType: "PHONE"}}, - LogLine{Call: "ON4KJM", Time: "1230", RSTsent: "55", RSTrcvd: "59", Mode: "FM", ModeType: "PHONE"}, "", + LogLine{Call: "ON4KJM", Time: "1230", ActualTime: "1230", RSTsent: "55", RSTrcvd: "59", Mode: "FM", ModeType: "PHONE"}, "", }, { "parse partial report (received) - FM", args{inputStr: "1230 on4kjm 5 44", previousLine: LogLine{Mode: "FM", ModeType: "PHONE"}}, - LogLine{Call: "ON4KJM", Time: "1230", RSTsent: "55", RSTrcvd: "44", Mode: "FM", ModeType: "PHONE"}, "", + LogLine{Call: "ON4KJM", Time: "1230", ActualTime: "1230", RSTsent: "55", RSTrcvd: "44", Mode: "FM", ModeType: "PHONE"}, "", }, { "Incompatible report", args{inputStr: "1230 on4kjm 5 599", previousLine: LogLine{Mode: "FM", ModeType: "PHONE"}}, - LogLine{Call: "ON4KJM", Time: "1230", RSTsent: "55", RSTrcvd: "*599", Mode: "FM", ModeType: "PHONE"}, "Invalid report (599) for PHONE mode ", + LogLine{Call: "ON4KJM", Time: "1230", ActualTime: "1230", RSTsent: "55", RSTrcvd: "*599", Mode: "FM", ModeType: "PHONE"}, "Invalid report (599) for PHONE mode ", }, } for _, tt := range tests { @@ -135,31 +135,31 @@ func TestHappyParseLine(t *testing.T) { "test1", args{inputStr: "1202 g4elz", previousLine: LogLine{Mode: "CW", ModeType: "CW", Band: "40m", BandLowerLimit: 7, BandUpperLimit: 7.3}}, - LogLine{Time: "1202", Call: "G4ELZ", Band: "40m", BandLowerLimit: 7, BandUpperLimit: 7.3, Mode: "CW", ModeType: "CW", RSTsent: "599", RSTrcvd: "599"}, "", + LogLine{Time: "1202", ActualTime: "1202", Call: "G4ELZ", Band: "40m", BandLowerLimit: 7, BandUpperLimit: 7.3, Mode: "CW", ModeType: "CW", RSTsent: "599", RSTrcvd: "599"}, "", }, { "test2", args{inputStr: "4 g3noh ", previousLine: LogLine{Time: "1202", Mode: "CW", ModeType: "CW", Band: "40m", BandLowerLimit: 7, BandUpperLimit: 7.3}}, - LogLine{Time: "1204", Call: "G3NOH", Band: "40m", BandLowerLimit: 7, BandUpperLimit: 7.3, Mode: "CW", ModeType: "CW", Comment: "PSE QSL Direct", RSTsent: "599", RSTrcvd: "599"}, "", + LogLine{Time: "1204", ActualTime: "1204", Call: "G3NOH", Band: "40m", BandLowerLimit: 7, BandUpperLimit: 7.3, Mode: "CW", ModeType: "CW", Comment: "PSE QSL Direct", RSTsent: "599", RSTrcvd: "599"}, "", }, { "test3", args{inputStr: "1227 gw4gte ", previousLine: LogLine{Time: "1202", Mode: "FM", ModeType: "PHONE", Band: "2m", BandLowerLimit: 144, BandUpperLimit: 148}}, - LogLine{Time: "1227", Call: "GW4GTE", Band: "2m", BandLowerLimit: 144, BandUpperLimit: 148, Mode: "FM", ModeType: "PHONE", Comment: "Dave", RSTsent: "59", RSTrcvd: "59"}, "", + LogLine{Time: "1227", ActualTime: "1227", Call: "GW4GTE", Band: "2m", BandLowerLimit: 144, BandUpperLimit: 148, Mode: "FM", ModeType: "PHONE", Comment: "Dave", RSTsent: "59", RSTrcvd: "59"}, "", }, { "test4", args{inputStr: "8 gw0tlk/m gwff-0021", previousLine: LogLine{Time: "1227", Mode: "FM", ModeType: "PHONE", Band: "2m", BandLowerLimit: 144, BandUpperLimit: 148}}, - LogLine{Time: "1228", Call: "GW0TLK/M", Band: "2m", BandLowerLimit: 144, BandUpperLimit: 148, Mode: "FM", ModeType: "PHONE", WWFF: "GWFF-0021", RSTsent: "59", RSTrcvd: "59"}, "", + LogLine{Time: "1228", ActualTime: "1228", Call: "GW0TLK/M", Band: "2m", BandLowerLimit: 144, BandUpperLimit: 148, Mode: "FM", ModeType: "PHONE", WWFF: "GWFF-0021", RSTsent: "59", RSTrcvd: "59"}, "", }, { "test5", args{inputStr: "7 dl0dan/p dlff-0002 dl/al-044", previousLine: LogLine{Time: "1220", Mode: "FM", ModeType: "PHONE", Band: "2m", BandLowerLimit: 144, BandUpperLimit: 148}}, - LogLine{Time: "1227", Call: "DL0DAN/P", Band: "2m", BandLowerLimit: 144, BandUpperLimit: 148, Mode: "FM", ModeType: "PHONE", WWFF: "DLFF-0002", SOTA: "DL/AL-044", RSTsent: "59", RSTrcvd: "59"}, "", + LogLine{Time: "1227", ActualTime: "1227", Call: "DL0DAN/P", Band: "2m", BandLowerLimit: 144, BandUpperLimit: 148, Mode: "FM", ModeType: "PHONE", WWFF: "DLFF-0002", SOTA: "DL/AL-044", RSTsent: "59", RSTrcvd: "59"}, "", }, } for _, tt := range tests { diff --git a/cmd/root.go b/cmd/root.go index ee43147..d42cb5d 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -34,6 +34,7 @@ import ( var cfgFile string var inputFilename string +var isInterpolateTime bool // rootCmd represents the base command when called without any subcommands var rootCmd = &cobra.Command{ @@ -64,6 +65,7 @@ func init() { cobra.OnInitialize(initConfig) rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.FLEcli.yaml)") + rootCmd.PersistentFlags().BoolVarP(&isInterpolateTime, "interpolate", "", false, "Interpolates the missing time entries.") rootCmd.PersistentFlags().StringVarP(&inputFilename, "input", "i", "", "FLE formatted input file (mandatory)") rootCmd.MarkPersistentFlagRequired("input") diff --git a/go.mod b/go.mod index e6bf571..a1111ea 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.14 require ( github.com/fsnotify/fsnotify v1.4.9 // indirect + github.com/mitchellh/go-homedir v1.1.0 github.com/mitchellh/mapstructure v1.3.1 // indirect github.com/pelletier/go-toml v1.8.0 // indirect github.com/spf13/afero v1.2.2 // indirect @@ -13,6 +14,6 @@ require ( github.com/spf13/pflag v1.0.5 // indirect github.com/spf13/viper v1.7.0 golang.org/x/sys v0.0.0-20200523222454-059865788121 // indirect + golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc gopkg.in/ini.v1 v1.57.0 // indirect - github.com/mitchellh/go-homedir v1.1.0 ) diff --git a/go.sum b/go.sum index fba41ab..bb64a41 100644 --- a/go.sum +++ b/go.sum @@ -287,6 +287,7 @@ golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgw golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc h1:NCy3Ohtk6Iny5V/reW2Ktypo4zIpWBdRJ1uFMjBxdg8= golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= diff --git a/test/commands.md b/test/commands.md index 0001cc2..420d5c8 100644 --- a/test/commands.md +++ b/test/commands.md @@ -3,3 +3,6 @@ * `./FLEcli -i test/data/fle-1.txt load` * `./FLEcli -i test/data/sample_contest_ru.txt load` * `go test ./...` runs the unit tests + +* `./FLEcli -i test/data/ON4KJM@ONFF-025920200524.txt --interpolate adif --wwff --overwrite` +* `./FLEcli adif -i=test/data/ON4KJM@ONFF-025920200524.txt --interpolate --wwff --overwrite` diff --git a/todo.md b/todo.md index bccb63d..47e6164 100644 --- a/todo.md +++ b/todo.md @@ -15,12 +15,14 @@ * [x] decode and check frequency * [ ] New MYGRID keyword * [ ] Support different date delimiter -* [ ] Support extrapolated date +* [x] Support extrapolated date * [ ] Support date not prefixed by "date" (non header mode) DATE keyword is now optional * [ ] Support date increment +* [ ] Support WWFF keyword +* [ ] Validate that we have the necessary dat for the output ## Output processing -* [ ] WWFF ADIF output +* [x] WWFF ADIF output * [ ] Standard ADIF output * [ ] SOTA ADIF * [ ] SOTA CSV