Add the time interpolating feature

pull/2/head
Jean-Marc MEESSEN 4 years ago committed by GitHub
parent 1077b7254f
commit 6172f13193
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -1,111 +0,0 @@
package cmd
/*
Copyright © 2020 Jean-Marc Meessen, ON4KJM <on4kjm@gmail.com>
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
}

@ -0,0 +1,114 @@
package cmd
/*
Copyright © 2020 Jean-Marc Meessen, ON4KJM <on4kjm@gmail.com>
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
}

@ -0,0 +1,127 @@
package cmd
import (
"errors"
"fmt"
"log"
"os"
"strings"
"time"
)
/*
Copyright © 2020 Jean-Marc Meessen, ON4KJM <on4kjm@gmail.com>
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
}

@ -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")
}
}

@ -19,10 +19,14 @@ limitations under the License.
import ( import (
"bufio" "bufio"
"fmt" "fmt"
"github.com/spf13/cobra"
"log" "log"
"os" "os"
"regexp" "regexp"
"time"
//"time"
"github.com/spf13/cobra"
//"strings" //"strings"
) )
@ -101,6 +105,9 @@ func loadFile() (filleFullLog []LogLine, isProcessedOK bool) {
headerDate := "" headerDate := ""
lineCount := 0 lineCount := 0
wrkTimeBlock := InferTimeBlock{}
missingTimeBlockList := []InferTimeBlock{}
var isInMultiLine = false var isInMultiLine = false
var cleanedInput []string var cleanedInput []string
var errorLog []string var errorLog []string
@ -254,16 +261,70 @@ func loadFile() (filleFullLog []LogLine, isProcessedOK bool) {
previousLogLine.Nickname = headerNickname previousLogLine.Nickname = headerNickname
previousLogLine.Date = headerDate previousLogLine.Date = headerDate
// //parse a line
logline, errorLine := ParseLine(eachline, previousLogLine) logline, errorLine := ParseLine(eachline, previousLogLine)
//we have a valid line (contains a call)
if logline.Call != "" { if logline.Call != "" {
fullLog = append(fullLog, logline) 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 != "" { if errorLine != "" {
errorLog = append(errorLog, fmt.Sprintf("Parsing error at line %d: %s ", lineCount, 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 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) displayLogSimple(fullLog)

@ -42,6 +42,7 @@ type LogLine struct {
BandUpperLimit float64 BandUpperLimit float64
Frequency string Frequency string
Time string Time string
ActualTime string //time actually recorded in FLE
Call string Call string
Comment string Comment string
QSLmsg string QSLmsg string
@ -80,6 +81,7 @@ func ParseLine(inputStr string, previousLine LogLine) (logLine LogLine, errorMsg
previousLine.OMname = "" previousLine.OMname = ""
previousLine.GridLoc = "" previousLine.GridLoc = ""
previousLine.Comment = "" previousLine.Comment = ""
previousLine.ActualTime = ""
logLine = previousLine logLine = previousLine
//TODO: what happens when we have <> or when there are multiple comments //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 isRightOfCall == false {
if regexpIsFullTime.MatchString(element) { if regexpIsFullTime.MatchString(element) {
logLine.Time = element logLine.Time = element
logLine.ActualTime = element
continue continue
} }
@ -162,9 +165,11 @@ func ParseLine(inputStr string, previousLine LogLine) (logLine LogLine, errorMsg
if regexpIsTimePart.MatchString(element) { if regexpIsTimePart.MatchString(element) {
if logLine.Time == "" { if logLine.Time == "" {
logLine.Time = element logLine.Time = element
logLine.ActualTime = element
} else { } else {
goodPart := logLine.Time[:len(logLine.Time)-len(element)] goodPart := logLine.Time[:len(logLine.Time)-len(element)]
logLine.Time = goodPart + element logLine.Time = goodPart + element
logLine.ActualTime = goodPart + element
} }
continue continue
} }

@ -24,27 +24,27 @@ func TestParseLine(t *testing.T) {
{ {
"Parse for time", "Parse for time",
args{inputStr: "1314 g3noh", previousLine: LogLine{Mode: "SSB"}}, 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", "Parse partial time - 1",
args{inputStr: "4 g3noh", previousLine: LogLine{Time: "", Mode: "SSB"}}, 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", "Parse partial time - 2",
args{inputStr: "15 g3noh", previousLine: LogLine{Time: "1200", Mode: "SSB"}}, 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", "Parse partial time - 3",
args{inputStr: "4 g3noh", previousLine: LogLine{Time: "1200", Mode: "SSB"}}, 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", "Parse for comment",
args{inputStr: "4 g3noh <PSE QSL Direct>", previousLine: LogLine{Mode: "SSB"}}, args{inputStr: "4 g3noh <PSE QSL Direct>", 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", "Parse for QSL",
@ -79,32 +79,32 @@ func TestParseLine(t *testing.T) {
{ {
"parse partial RST (sent) - CW", "parse partial RST (sent) - CW",
args{inputStr: "1230 on4kjm 5", previousLine: LogLine{Mode: "CW", ModeType: "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", "parse partial RST (received) - CW",
args{inputStr: "1230 on4kjm 5 44", previousLine: LogLine{Mode: "CW", ModeType: "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", "parse full RST (received) - CW",
args{inputStr: "1230 on4kjm 5 448", previousLine: LogLine{Mode: "CW", ModeType: "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", "parse partial report (sent) - FM",
args{inputStr: "1230 on4kjm 5", previousLine: LogLine{Mode: "FM", ModeType: "PHONE"}}, 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", "parse partial report (received) - FM",
args{inputStr: "1230 on4kjm 5 44", previousLine: LogLine{Mode: "FM", ModeType: "PHONE"}}, 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", "Incompatible report",
args{inputStr: "1230 on4kjm 5 599", previousLine: LogLine{Mode: "FM", ModeType: "PHONE"}}, 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 { for _, tt := range tests {
@ -135,31 +135,31 @@ func TestHappyParseLine(t *testing.T) {
"test1", "test1",
args{inputStr: "1202 g4elz", args{inputStr: "1202 g4elz",
previousLine: LogLine{Mode: "CW", ModeType: "CW", Band: "40m", BandLowerLimit: 7, BandUpperLimit: 7.3}}, 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", "test2",
args{inputStr: "4 g3noh <PSE QSL Direct>", args{inputStr: "4 g3noh <PSE QSL Direct>",
previousLine: LogLine{Time: "1202", Mode: "CW", ModeType: "CW", Band: "40m", BandLowerLimit: 7, BandUpperLimit: 7.3}}, 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", "test3",
args{inputStr: "1227 gw4gte <Dave>", args{inputStr: "1227 gw4gte <Dave>",
previousLine: LogLine{Time: "1202", Mode: "FM", ModeType: "PHONE", Band: "2m", BandLowerLimit: 144, BandUpperLimit: 148}}, 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", "test4",
args{inputStr: "8 gw0tlk/m gwff-0021", args{inputStr: "8 gw0tlk/m gwff-0021",
previousLine: LogLine{Time: "1227", Mode: "FM", ModeType: "PHONE", Band: "2m", BandLowerLimit: 144, BandUpperLimit: 148}}, 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", "test5",
args{inputStr: "7 dl0dan/p dlff-0002 dl/al-044", args{inputStr: "7 dl0dan/p dlff-0002 dl/al-044",
previousLine: LogLine{Time: "1220", Mode: "FM", ModeType: "PHONE", Band: "2m", BandLowerLimit: 144, BandUpperLimit: 148}}, 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 { for _, tt := range tests {

@ -34,6 +34,7 @@ import (
var cfgFile string var cfgFile string
var inputFilename string var inputFilename string
var isInterpolateTime bool
// rootCmd represents the base command when called without any subcommands // rootCmd represents the base command when called without any subcommands
var rootCmd = &cobra.Command{ var rootCmd = &cobra.Command{
@ -64,6 +65,7 @@ func init() {
cobra.OnInitialize(initConfig) cobra.OnInitialize(initConfig)
rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.FLEcli.yaml)") 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.PersistentFlags().StringVarP(&inputFilename, "input", "i", "", "FLE formatted input file (mandatory)")
rootCmd.MarkPersistentFlagRequired("input") rootCmd.MarkPersistentFlagRequired("input")

@ -4,6 +4,7 @@ go 1.14
require ( require (
github.com/fsnotify/fsnotify v1.4.9 // indirect 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/mitchellh/mapstructure v1.3.1 // indirect
github.com/pelletier/go-toml v1.8.0 // indirect github.com/pelletier/go-toml v1.8.0 // indirect
github.com/spf13/afero v1.2.2 // 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/pflag v1.0.5 // indirect
github.com/spf13/viper v1.7.0 github.com/spf13/viper v1.7.0
golang.org/x/sys v0.0.0-20200523222454-059865788121 // indirect 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 gopkg.in/ini.v1 v1.57.0 // indirect
github.com/mitchellh/go-homedir v1.1.0
) )

@ -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-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-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-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/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= 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= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=

@ -3,3 +3,6 @@
* `./FLEcli -i test/data/fle-1.txt load` * `./FLEcli -i test/data/fle-1.txt load`
* `./FLEcli -i test/data/sample_contest_ru.txt load` * `./FLEcli -i test/data/sample_contest_ru.txt load`
* `go test ./...` runs the unit tests * `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`

@ -15,12 +15,14 @@
* [x] decode and check frequency * [x] decode and check frequency
* [ ] New MYGRID keyword * [ ] New MYGRID keyword
* [ ] Support different date delimiter * [ ] 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 not prefixed by "date" (non header mode) DATE keyword is now optional
* [ ] Support date increment * [ ] Support date increment
* [ ] Support WWFF keyword
* [ ] Validate that we have the necessary dat for the output
## Output processing ## Output processing
* [ ] WWFF ADIF output * [x] WWFF ADIF output
* [ ] Standard ADIF output * [ ] Standard ADIF output
* [ ] SOTA ADIF * [ ] SOTA ADIF
* [ ] SOTA CSV * [ ] SOTA CSV

Loading…
Cancel
Save