timetable-extension #1
@ -68,3 +68,36 @@ func generateUpdateDays(days int) []time.Time {
|
||||
|
||||
return updateDays
|
||||
}
|
||||
|
||||
// Parses CIF Schedule Start/End Dates (YYYY-MM-DD) into time.Time types (00:00:00 for start, 23:59:59 for end)
|
||||
func ParseCifDate(input, startOrEnd string) time.Time {
|
||||
layout := "2006-01-02" // Layout of input
|
||||
t, err := time.ParseInLocation(layout, input, londonTimezone)
|
||||
if err != nil {
|
||||
log.Msg.Error("Error parsing date string", zap.String("date string", input), zap.Error(err))
|
||||
return time.Time{}
|
||||
}
|
||||
|
||||
if startOrEnd == "start" {
|
||||
t = time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, londonTimezone)
|
||||
} else if startOrEnd == "end" {
|
||||
t = time.Date(t.Year(), t.Month(), t.Day(), 23, 59, 59, 0, londonTimezone)
|
||||
} else {
|
||||
log.Msg.Error("Error parsing date string", zap.String("date string", input), zap.Error(err))
|
||||
return time.Time{}
|
||||
}
|
||||
|
||||
return t
|
||||
}
|
||||
|
||||
// Parses CIF days_run field and converts to array of day strings
|
||||
func parseDaysRun(daysBinary string) []string {
|
||||
shortDays := []string{"m", "t", "w", "th", "f", "s", "su"}
|
||||
var result []string
|
||||
for i, digit := range daysBinary {
|
||||
if digit == '1' {
|
||||
result = append(result, shortDays[i])
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
package cif
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
@ -86,6 +87,48 @@ func TestGenerateUpdateDays(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseCifDate(t *testing.T) {
|
||||
testCases := []struct {
|
||||
dateString string
|
||||
startOrEnd string
|
||||
expect time.Time
|
||||
}{
|
||||
{"2024-04-05", "start", time.Date(2024, time.April, 5, 0, 0, 0, 0, londonTimezone)},
|
||||
{"2022-01-01", "start", time.Date(2022, time.January, 1, 0, 0, 0, 0, londonTimezone)},
|
||||
{"2015-09-26", "end", time.Date(2015, time.September, 26, 23, 59, 59, 0, londonTimezone)},
|
||||
{"2018-03-13", "end", time.Date(2018, time.March, 13, 23, 59, 59, 0, londonTimezone)},
|
||||
}
|
||||
|
||||
layout := "2006-01-02 15:04:05" // Layout for printing times in error cases.
|
||||
|
||||
for _, tc := range testCases {
|
||||
result := ParseCifDate(tc.dateString, tc.startOrEnd)
|
||||
if result != tc.expect {
|
||||
t.Errorf("For datestring %s, startOrEnd %s, expected %s, but got %s", tc.dateString, tc.startOrEnd, tc.expect.Format(layout), result.Format(layout))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseDaysRun(t *testing.T) {
|
||||
testCases := []struct {
|
||||
input string
|
||||
expect []string
|
||||
}{
|
||||
{"1111111", []string{"m", "t", "w", "th", "f", "s", "su"}},
|
||||
{"0000001", []string{"su"}},
|
||||
{"1000000", []string{"m"}},
|
||||
{"0000100", []string{"f"}},
|
||||
{"0111000", []string{"t", "w", "th"}},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
result := parseDaysRun(tc.input)
|
||||
if !reflect.DeepEqual(result, tc.expect) {
|
||||
t.Errorf("For input %s, expected %v, but got %v", tc.input, tc.expect, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Checks if two time values have the same year, month and day.
|
||||
func isSameDate(t1, t2 time.Time) bool {
|
||||
return t1.Year() == t2.Year() && t1.Month() == t2.Month() && t1.Day() == t2.Day()
|
||||
|
@ -1,105 +0,0 @@
|
||||
package cif
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.fjla.uk/owlboard/go-types/pkg/upstreamApi"
|
||||
)
|
||||
|
||||
func TestParseCifData(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
input []byte
|
||||
expected *parsedData
|
||||
}
|
||||
}
|
||||
|
||||
func returnParsedData() *parsedData {
|
||||
var parsed = {
|
||||
name: "Valid JSON Data 1",
|
||||
input: []byte(`
|
||||
{"JsonTimetableV1":{"classification":"public","timestamp":1711227636,"owner":"Network Rail","Sender":{"organisation":"Rockshore","application":"NTROD","component":"SCHEDULE"},"Metadata":{"type":"update","sequence":4307}}}
|
||||
{"JsonAssociationV1":{"transaction_type":"Create","main_train_uid":"L65283","assoc_train_uid":"L65245","assoc_start_date":"2023-12-11T00:00:00Z","assoc_end_date":"2024-03-28T00:00:00Z","assoc_days":"1111000","category":" ","date_indicator":" ","location":"RDNGSTN","base_location_suffix":null,"assoc_location_suffix":null,"diagram_type":"T","CIF_stp_indicator":"C"}}
|
||||
{"JsonAssociationV1":{"transaction_type":"Delete","main_train_uid":"L65283","assoc_train_uid":"L65245","assoc_start_date":"2024-03-18T00:00:00Z","location":"RDNGSTN","base_location_suffix":null,"diagram_type":"T","CIF_stp_indicator":"C"}}
|
||||
{"JsonScheduleV1":{"CIF_stp_indicator":"O","CIF_train_uid":"C25513","schedule_start_date":"2024-03-23","transaction_type":"Delete"}}
|
||||
{"JsonScheduleV1":{"CIF_bank_holiday_running":null,"CIF_stp_indicator":"O","CIF_train_uid":"Y04345","applicable_timetable":"Y","atoc_code":"SW","new_schedule_segment":{"traction_class":"","uic_code":""},"schedule_days_runs":"0000001","schedule_end_date":"2024-05-19","schedule_segment":{"signalling_id":"5A20","CIF_train_category":"EE","CIF_headcode":"00","CIF_course_indicator":1,"CIF_train_service_code":"24676004","CIF_business_sector":"??","CIF_power_type":"EMU","CIF_timing_load":null,"CIF_speed":"100","CIF_operating_characteristics":"D","CIF_train_class":null,"CIF_sleepers":null,"CIF_reservations":null,"CIF_connection_indicator":null,"CIF_catering_code":null,"CIF_service_branding":"","schedule_location":[{"location_type":"LO","record_identity":"LO","tiploc_code":"FARNHMD","tiploc_instance":null,"departure":"0721","public_departure":null,"platform":null,"line":null,"engineering_allowance":null,"pathing_allowance":null,"performance_allowance":null},{"location_type":"LT","record_identity":"LT","tiploc_code":"FARNHAM","tiploc_instance":null,"platform":"1","arrival":"0729","public_arrival":null,"path":null}]},"schedule_start_date":"2024-05-19","train_status":"T","transaction_type":"Create"}}
|
||||
{"EOF":true}
|
||||
`),
|
||||
expected: &parsedData{
|
||||
header: upstreamApi.JsonTimetableV1{
|
||||
Classification: "public",
|
||||
Timestamp: 1711227636,
|
||||
Owner: "Network Rail",
|
||||
Sender: upstreamApi.TimetableSender{
|
||||
Organisation: "Rockshore",
|
||||
Application: "NTROD",
|
||||
Component: "SCHEDULE",
|
||||
},
|
||||
Metadata: upstreamApi.TimetableMetadata{
|
||||
Type: "update",
|
||||
Sequence: 4307,
|
||||
},
|
||||
},
|
||||
assoc: []upstreamApi.JsonAssociationV1{
|
||||
TransactionType: "Create",
|
||||
MainTrainUid: "L65283",
|
||||
AssocTrainUid: "L65245",
|
||||
AssocStartDate: time.Date(2023, time.December, 11, 0, 0, 0, 0, time.UTC),
|
||||
AssocEndDate: time.Date(2024, time.March, 28, 0, 0, 0, 0, time.UTC),
|
||||
AssocDays: "1111000",
|
||||
Location: "RDNGSTN",
|
||||
DiagramType: "T",
|
||||
CifStpIndicator: "C",
|
||||
}, {
|
||||
TransactionType: "Delete",
|
||||
MainTrainUid: "L65283",
|
||||
AssocTrainUid: "L65245",
|
||||
AssocStartDate: time.Date(2024, time.March, 18, 0, 0, 0, 0, time.UTC),
|
||||
Location: "RDNGSTN",
|
||||
DiagramType: "T",
|
||||
CifStpIndicator: "C",
|
||||
},
|
||||
sched: []upstreamApi.JsonScheduleV1{
|
||||
CifStpIndicator: "O",
|
||||
CifTrainUid: "C25513",
|
||||
ScheduleStartDate: "2024-03-23",
|
||||
TransactionType: "Delete",
|
||||
}, {
|
||||
CifStpIndicator: "O",
|
||||
CifTrainUid: "Y04345",
|
||||
ApplicableTimetable: "Y",
|
||||
AtocCode: "SW",
|
||||
NewScheduleSegment: upstreamApi.CifNewScheduleSegment{},
|
||||
ScheduleDaysRun: "0000001",
|
||||
ScheduleEndDate: "2024-05-19",
|
||||
ScheduleSegment: upstreamApi.CifScheduleSegment{
|
||||
SignallingId: "5A20",
|
||||
CifTrainCategory: "EE",
|
||||
CifHeadcode: "OO",
|
||||
CifCourseIndicator: 1,
|
||||
CifTrainServiceCode: "24676004",
|
||||
CifBusinessSector: "??",
|
||||
CifPowerType: "EMU",
|
||||
CifSpeed: "100",
|
||||
CifOperatingCharacteristics: "D",
|
||||
ScheduleLocation: []upstreamApi.CifScheduleLocation{
|
||||
LocationType: "LO",
|
||||
RecordIdentity: "LO",
|
||||
TiplocCode: "FARNHMD",
|
||||
Departure: "0721",
|
||||
}, {
|
||||
LocationType: "LT",
|
||||
RecordIdentity: "LT",
|
||||
TiplocCode: "FARNHAM",
|
||||
Platform: "1",
|
||||
Arrival: "0729",
|
||||
},
|
||||
},
|
||||
ScheduleStartDate: "2024-05-19",
|
||||
TrainStatus: "T",
|
||||
TransactionType: "Create",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
@ -1,6 +1,68 @@
|
||||
package cif
|
||||
|
||||
import (
|
||||
"git.fjla.uk/owlboard/go-types/pkg/database"
|
||||
"git.fjla.uk/owlboard/go-types/pkg/upstreamApi"
|
||||
"git.fjla.uk/owlboard/timetable-mgr/log"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// Processes parsed CIF data and applies the data to the database
|
||||
func processParsedCif(data *parsedData) error {
|
||||
createTasks := make([]*upstreamApi.JsonScheduleV1, 0)
|
||||
deleteTasks := make([]*upstreamApi.JsonScheduleV1, 0)
|
||||
|
||||
for _, item := range data.sched {
|
||||
switch item.TransactionType {
|
||||
case "Delete":
|
||||
deleteTasks = append(deleteTasks, &item)
|
||||
case "Create":
|
||||
createTasks = append(createTasks, &item)
|
||||
default:
|
||||
log.Msg.Error("Unknown transaction type in CIF Schedule", zap.String("TransactionType", item.TransactionType))
|
||||
}
|
||||
}
|
||||
|
||||
err := doDeletions(deleteTasks)
|
||||
if err != nil {
|
||||
log.Msg.Error("Error deleting CIF Entries", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
err = doCreations(createTasks)
|
||||
if err != nil {
|
||||
log.Msg.Error("Error creating CIF Entries", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create delete query types and pass to the function which batches the deletions
|
||||
func doDeletions(deletions []*upstreamApi.JsonScheduleV1) error {
|
||||
log.Msg.Info("Preparing CIF update Delete tasks", zap.Int("Delete task count", len(deletions)))
|
||||
|
||||
deleteQueries := make([]database.DeleteQuery, 0)
|
||||
for _, item := range deletions {
|
||||
query := database.DeleteQuery{
|
||||
ScheduleStartDate: ParseCifDate(item.ScheduleStartDate, "start"),
|
||||
StpIndicator: item.CifStpIndicator,
|
||||
TrainUid: item.CifTrainUid,
|
||||
}
|
||||
|
||||
deleteQueries = append(deleteQueries, query)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Convert to the correct struct for the database and pass to the function which batches insertions
|
||||
func doCreations(creations []*upstreamApi.JsonScheduleV1) error {
|
||||
log.Msg.Info("Preparing CIF update Create tasks", zap.Int("Create task count", len(creations)))
|
||||
|
||||
createDocuments := make([]database.Service, 0)
|
||||
for _, item := range creations {
|
||||
document := database.Service{}
|
||||
|
||||
createDocuments = append(createDocuments, document)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
@ -60,7 +60,7 @@ func runCifUpdateDownload(cfg *helpers.Configuration, metadata *dbAccess.CifMeta
|
||||
return err
|
||||
}
|
||||
// Parse CIF file
|
||||
parsed, err := parseCifData(data)
|
||||
parsed, err = parseCifData(data)
|
||||
if err != nil {
|
||||
log.Msg.Error("Error parsing CIF data", zap.Error(err))
|
||||
return err
|
||||
|
@ -5,6 +5,7 @@ import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"git.fjla.uk/owlboard/go-types/pkg/database"
|
||||
"git.fjla.uk/owlboard/timetable-mgr/log"
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/mongo"
|
||||
@ -62,3 +63,31 @@ func PutCifMetadata(metadata CifMetadata) bool {
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func DeleteCifEntries(deletions []database.DeleteQuery) error {
|
||||
// Prepare deletion tasks
|
||||
bulkDeletions := make([]mongo.WriteModel, 0, len(deletions))
|
||||
|
||||
for _, deleteQuery := range deletions {
|
||||
filter := bson.M{
|
||||
"trainUid": deleteQuery.TrainUid,
|
||||
"scheduleStartDate": deleteQuery.ScheduleStartDate,
|
||||
"stpIndicator": deleteQuery.StpIndicator,
|
||||
}
|
||||
bulkDeletions = append(bulkDeletions, mongo.NewDeleteManyModel().SetFilter(filter))
|
||||
}
|
||||
|
||||
log.Msg.Info("Running `Delete` tasks from CIF Update", zap.Int("Required deletions", len(deletions)))
|
||||
for i := 0; i < len(bulkDeletions); i += batchsize {
|
||||
end := i + batchsize
|
||||
if end > len(bulkDeletions) {
|
||||
end = len(bulkDeletions)
|
||||
}
|
||||
_, err := MongoClient.Database(databaseName).Collection(TimetableCollection).BulkWrite(context.Background(), bulkDeletions[i:end])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@ -5,3 +5,4 @@ const CorpusCollection string = "corpus"
|
||||
const StationsCollection string = "stations"
|
||||
const metaCollection string = "meta"
|
||||
const TimetableCollection string = "timetable"
|
||||
const batchsize int = 100
|
||||
|
@ -3,7 +3,7 @@ module git.fjla.uk/owlboard/timetable-mgr
|
||||
go 1.21
|
||||
|
||||
require (
|
||||
git.fjla.uk/owlboard/go-types v0.0.0-20240403200521-41796e25b6c3
|
||||
git.fjla.uk/owlboard/go-types v0.0.0-20240405194933-7dafca950460
|
||||
github.com/go-stomp/stomp/v3 v3.0.5
|
||||
go.mongodb.org/mongo-driver v1.12.0
|
||||
go.uber.org/zap v1.24.0
|
||||
|
@ -4,6 +4,8 @@ git.fjla.uk/owlboard/go-types v0.0.0-20240331204922-8f8899eb6072 h1:QjaTVm4bpnXZ
|
||||
git.fjla.uk/owlboard/go-types v0.0.0-20240331204922-8f8899eb6072/go.mod h1:kG+BX9UF+yJaAVnln/QSKlTdrtKRRReezMeSk1ZLMzY=
|
||||
git.fjla.uk/owlboard/go-types v0.0.0-20240403200521-41796e25b6c3 h1:veGmL8GeWsgGCeTgPPSDw5tbUr1pUz8F6DBgFMf6IGc=
|
||||
git.fjla.uk/owlboard/go-types v0.0.0-20240403200521-41796e25b6c3/go.mod h1:kG+BX9UF+yJaAVnln/QSKlTdrtKRRReezMeSk1ZLMzY=
|
||||
git.fjla.uk/owlboard/go-types v0.0.0-20240405194933-7dafca950460 h1:39vcejk0MzZIL2WdriKSfcDoCc/0NWzZMncHqw5ctbY=
|
||||
git.fjla.uk/owlboard/go-types v0.0.0-20240405194933-7dafca950460/go.mod h1:kG+BX9UF+yJaAVnln/QSKlTdrtKRRReezMeSk1ZLMzY=
|
||||
github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
|
||||
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
|
@ -101,7 +101,7 @@ func parseDate(dateString string, end bool) time.Time {
|
||||
var hour, minute, second, nanosecond int
|
||||
location := time.UTC
|
||||
if end {
|
||||
hour, minute, second, nanosecond = 23, 59, 59, 999999999
|
||||
hour, minute, second, nanosecond = 23, 59, 59, 0
|
||||
} else {
|
||||
hour, minute, second, nanosecond = 0, 0, 0, 0
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user