From c157eda5808957e0076cc5e1bba1772fe5838fe8 Mon Sep 17 00:00:00 2001 From: "J. Ritchie Carroll" Date: Fri, 24 Sep 2021 03:00:36 -0400 Subject: [PATCH] Added initial DataSet implementation --- .vscode/settings.json | 4 +- .../AdvancedSubscribe/AdvancedSubscribe.go | 154 +++++++++++++ sttp/[TypeName]HashSet.tt | 2 +- sttp/guid/HashSet.go | 2 +- sttp/metadata/DataColumn.go | 78 +++++++ sttp/metadata/DataRow.go | 203 ++++++++++++++++++ sttp/metadata/DataSet.go | 132 ++++++++++++ sttp/metadata/DataTable.go | 150 +++++++++++++ sttp/metadata/DataType.go | 117 ++++++++++ sttp/transport/SignalKind.go | 96 ++++++--- 10 files changed, 901 insertions(+), 37 deletions(-) create mode 100644 examples/AdvancedSubscribe/AdvancedSubscribe.go create mode 100644 sttp/metadata/DataColumn.go create mode 100644 sttp/metadata/DataRow.go create mode 100644 sttp/metadata/DataSet.go create mode 100644 sttp/metadata/DataTable.go create mode 100644 sttp/metadata/DataType.go diff --git a/.vscode/settings.json b/.vscode/settings.json index cdab9be..c5221b7 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,13 +2,15 @@ "cSpell.words": [ "ALOG", "ALRM", + "ANTLR", "bufio", + "DFDT", "Gbtc", "goapi", "GPLAINS", - "IDXOR", "IPHA", "IPHM", + "msdata", "phasor", "Ritchie", "strconv", diff --git a/examples/AdvancedSubscribe/AdvancedSubscribe.go b/examples/AdvancedSubscribe/AdvancedSubscribe.go new file mode 100644 index 0000000..d8c8f92 --- /dev/null +++ b/examples/AdvancedSubscribe/AdvancedSubscribe.go @@ -0,0 +1,154 @@ +//****************************************************************************************************** +// AdvancedSubscribe.go - Gbtc +// +// Copyright © 2021, Grid Protection Alliance. All Rights Reserved. +// +// Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See +// the NOTICE file distributed with this work for additional information regarding copyright ownership. +// The GPA licenses this file to you under the MIT License (MIT), the "License"; you may not use this +// file except in compliance with the License. You may obtain a copy of the License at: +// +// http://opensource.org/licenses/MIT +// +// Unless agreed to in writing, the subject software distributed under the License is distributed on an +// "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the +// License for the specific language governing permissions and limitations. +// +// Code Modification History: +// ---------------------------------------------------------------------------------------------------- +// 09/23/2021 - J. Ritchie Carroll +// Generated original version of source code. +// +//****************************************************************************************************** + +package main + +import ( + "bufio" + "fmt" + "math" + "os" + "strconv" + "strings" + "time" + + "github.com/sttp/goapi/sttp" + "github.com/sttp/goapi/sttp/transport" +) + +// AdvancedSubscriber is a simple STTP data subscriber implementation. +type AdvancedSubscriber struct { + sttp.SubscriberBase // Provides default implementation +} + +// NewAdvancedSubscriber creates a new AdvancedSubscriber. +func NewAdvancedSubscriber() *AdvancedSubscriber { + subscriber := &AdvancedSubscriber{} + subscriber.SubscriberBase = sttp.NewSubscriberBase(subscriber) + return subscriber +} + +func main() { + hostname, port := parseCmdLineArgs() + subscriber := NewAdvancedSubscriber() + subscription := subscriber.Subscription() + + subscriber.Hostname = hostname + subscriber.Port = port + + subscription.FilterExpression = "FILTER TOP 20 ActiveMeasurements WHERE True" + subscription.UdpDataChannel = true + subscription.DataChannelLocalPort = 9600 + subscription.UseMillisecondResolution = true + + subscriber.Connect() + defer subscriber.Dispose() + + reader := bufio.NewReader(os.Stdin) + reader.ReadRune() +} + +// ReceivedMetadata handles reception of the metadata response. +func (ss *AdvancedSubscriber) ReceivedMetadata(metadata []byte) { + ss.StatusMessage(fmt.Sprintf("Received %d bytes of metadata", len(metadata))) +} + +// SubscriptionUpdated handles notifications that a new SignalIndexCache has been received. +func (ss *AdvancedSubscriber) SubscriptionUpdated(signalIndexCache *transport.SignalIndexCache) { + ss.StatusMessage(fmt.Sprintf("Received signal index cache with %d mappings", signalIndexCache.Count())) +} + +var lastMessageDisplay time.Time + +// ReceivedNewMeasurements handles reception of new measurements. +func (ss *AdvancedSubscriber) ReceivedNewMeasurements(measurements []transport.Measurement) { + + if time.Since(lastMessageDisplay).Seconds() < 5.0 { + return + } + + defer func() { lastMessageDisplay = time.Now() }() + + if lastMessageDisplay.IsZero() { + ss.StatusMessage("Receiving measurements...") + return + } + + var message strings.Builder + + message.WriteString(strconv.FormatUint(ss.TotalMeasurementsReceived(), 10)) + message.WriteString(" measurements received so far...\n") + message.WriteString("Timestamp: ") + message.WriteString(measurements[0].DateTime().Format("2006-01-02 15:04:05.999999999")) + message.WriteRune('\n') + message.WriteString("\tID\tSignal ID\t\t\t\tValue\n") + + for i := 0; i < len(measurements); i++ { + measurement := measurements[i] + metadata := ss.Metadata(&measurement) + + message.WriteRune('\t') + message.WriteString(strconv.FormatUint(metadata.ID, 10)) + message.WriteRune('\t') + message.WriteString(measurement.SignalID.String()) + message.WriteRune('\t') + message.WriteString(strconv.FormatFloat(measurement.Value, 'f', 6, 64)) + message.WriteRune('\n') + } + + ss.StatusMessage(message.String()) +} + +// ConnectionTerminated handles notification that a connection has been terminated. +func (ss *AdvancedSubscriber) ConnectionTerminated() { + // Call base implementation which will display a connection terminated message to stderr + ss.SubscriberBase.ConnectionTerminated() + + // Reset last message display time on disconnect + lastMessageDisplay = time.Time{} +} + +func parseCmdLineArgs() (string, uint16) { + args := os.Args + + if len(args) < 3 { + fmt.Println("Usage:") + fmt.Println(" AdvancedSubscribe HOSTNAME PORT") + os.Exit(1) + } + + hostname := args[1] + port, err := strconv.Atoi(args[2]) + + if err != nil { + fmt.Printf("Invalid port number \"%s\": %s\n", args[1], err.Error()) + os.Exit(2) + } + + if port < 1 || port > math.MaxUint16 { + fmt.Printf("Port number \"%s\" is out of range: must be 1 to %d\n", args[1], math.MaxUint16) + os.Exit(2) + } + + return hostname, uint16(port) +} diff --git a/sttp/[TypeName]HashSet.tt b/sttp/[TypeName]HashSet.tt index 331abd2..3ea9e78 100644 --- a/sttp/[TypeName]HashSet.tt +++ b/sttp/[TypeName]HashSet.tt @@ -55,7 +55,7 @@ var member void // TODO: Code can be changed from template to type "T" when generics are available: //type HashSet[T any] map[T]void -// <#=TypeName#>HashSet represents a distinct collection of values, i.e., a set. +// <#=TypeName#>HashSet represents a distinct collection of <#=TypeName#> values, i.e., a set. // A <#=TypeName#>HashSet is not sorted and will not contain duplicate elements. // The methods of the <#=TypeName#>HashSet are not intrinsically thread-safe procedures, // to guarantee thread safety, you should initiate a lock before calling a method. diff --git a/sttp/guid/HashSet.go b/sttp/guid/HashSet.go index 7cd3024..9f7586d 100644 --- a/sttp/guid/HashSet.go +++ b/sttp/guid/HashSet.go @@ -42,7 +42,7 @@ var member void // TODO: Code can be changed from template to type "T" when generics are available: //type HashSet[T any] map[T]void -// HashSet represents a distinct collection of values, i.e., a set. +// HashSet represents a distinct collection of Guid values, i.e., a set. // A HashSet is not sorted and will not contain duplicate elements. // The methods of the HashSet are not intrinsically thread-safe procedures, // to guarantee thread safety, you should initiate a lock before calling a method. diff --git a/sttp/metadata/DataColumn.go b/sttp/metadata/DataColumn.go new file mode 100644 index 0000000..d552201 --- /dev/null +++ b/sttp/metadata/DataColumn.go @@ -0,0 +1,78 @@ +//****************************************************************************************************** +// DataColumn.go - Gbtc +// +// Copyright © 2021, Grid Protection Alliance. All Rights Reserved. +// +// Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See +// the NOTICE file distributed with this work for additional information regarding copyright ownership. +// The GPA licenses this file to you under the MIT License (MIT), the "License"; you may not use this +// file except in compliance with the License. You may obtain a copy of the License at: +// +// http://opensource.org/licenses/MIT +// +// Unless agreed to in writing, the subject software distributed under the License is distributed on an +// "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the +// License for the specific language governing permissions and limitations. +// +// Code Modification History: +// ---------------------------------------------------------------------------------------------------- +// 09/23/2021 - J. Ritchie Carroll +// Generated original version of source code. +// +//****************************************************************************************************** + +package metadata + +// DataColumn represents a column, i.e., a field, in a DataTable defining a name and a data type. +// Data columns can also be computed where its value would be derived from other columns and +// functions (https://sttp.github.io/documentation/filter-expressions/) defined in an expression. +type DataColumn struct { + parent *DataTable + name string + dataType DataTypeEnum + expression string + computed bool + index int +} + +func newDataColumn(parent *DataTable, name string, dataType DataTypeEnum, expression string) *DataColumn { + return &DataColumn{ + parent: parent, + name: name, + dataType: dataType, + expression: expression, + computed: len(expression) > 0, + index: -1, + } +} + +// Parent gets the parent DataTable of the DataColumn. +func (dc *DataColumn) Parent() *DataTable { + return dc.parent +} + +// Name gets the column name of the DataColumn. +func (dc *DataColumn) Name() string { + return dc.name +} + +// Type gets the column DataType enumeration value of the DataColumn. +func (dc *DataColumn) Type() DataTypeEnum { + return dc.dataType +} + +// Expression gets the column expression value of the DataColumn, if any. +func (dc *DataColumn) Expression() string { + return dc.expression +} + +// Computed gets a flag that determines if the DataColumn is a computed value, +// i.e., has a defined expression. +func (dc *DataColumn) Computed() bool { + return dc.computed +} + +// Index gets the index of the DataColumn within its parent DataTable columns collection. +func (dc *DataColumn) Index() int { + return dc.index +} diff --git a/sttp/metadata/DataRow.go b/sttp/metadata/DataRow.go new file mode 100644 index 0000000..9546bd8 --- /dev/null +++ b/sttp/metadata/DataRow.go @@ -0,0 +1,203 @@ +//****************************************************************************************************** +// DataRow.go - Gbtc +// +// Copyright © 2021, Grid Protection Alliance. All Rights Reserved. +// +// Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See +// the NOTICE file distributed with this work for additional information regarding copyright ownership. +// The GPA licenses this file to you under the MIT License (MIT), the "License"; you may not use this +// file except in compliance with the License. You may obtain a copy of the License at: +// +// http://opensource.org/licenses/MIT +// +// Unless agreed to in writing, the subject software distributed under the License is distributed on an +// "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the +// License for the specific language governing permissions and limitations. +// +// Code Modification History: +// ---------------------------------------------------------------------------------------------------- +// 09/23/2021 - J. Ritchie Carroll +// Generated original version of source code. +// +//****************************************************************************************************** + +package metadata + +import ( + "fmt" + "strconv" + "time" +) + +// DataRow represents a row, i.e., a record, in a DataTable defining a set of values for each +// defined DataColumn field in the DataTable columns collection. +type DataRow struct { + parent *DataTable + values []interface{} +} + +func newDataRow(parent *DataTable) *DataRow { + return &DataRow{ + parent: parent, + values: make([]interface{}, parent.ColumnCount()), + } +} + +// Parent gets the parent DataTable of the DataRow. +func (dr *DataRow) Parent() *DataTable { + return dr.parent +} + +func (dr *DataRow) getColumnIndex(columnName string) int { + column := dr.parent.ColumnByName(columnName) + + if column == nil { + panic("Column name \"" + columnName + "\" was not found in table \"" + dr.parent.Name() + "\"") + } + + return column.Index() +} + +func (dr *DataRow) validateColumnType(columnIndex int, targetType int, read bool) *DataColumn { + column := dr.parent.Column(columnIndex) + + if column == nil { + panic("Column index " + strconv.Itoa(columnIndex) + " is out of range for table \"" + dr.parent.Name() + "\"") + } + + if targetType > -1 && column.Type() != DataTypeEnum(targetType) { + var action string + var preposition string + + if read { + action = "read" + preposition = "from" + } else { + action = "assign" + preposition = "to" + } + + panic(fmt.Sprintf("Cannot %s \"%s\" value %s DataColumn \"%s\" for table \"%s\", column data type is \"%s\"", action, DataTypeEnum(targetType).Name(), preposition, column.Name(), dr.parent.Name(), column.Type().Name())) + } + + if !read && column.Computed() { + panic("Cannot assign value to DataColumn \"" + column.Name() + "\" for table \"" + dr.parent.Name() + "\", column is computed with an expression") + } + + return column +} + +// func (dr *DataRow) getExpressionTree(column *DataColumn) (*ExpressionTree, error) { +// columnIndex := column.Index() + +// if dr.values[columnIndex] == nil { +// dataTable := column.Parent() +// parser := NewFilterExpressionParser(column.Expression()) + +// parser.SetDataSet(dataTable.Parent()) +// parser.SetPrimaryTableName(dataTable.Name()) +// parser.SetTrackFilteredSignalIDs(false) +// parser.SetTrackFilteredRows(false) + +// expressionTrees := parser.GetExpressionTrees() + +// if len(expressionTrees) == 0 { +// return nil, errors.New("Expression defined for computed DataColumn \"" + column.Name() + "\" for table \"" + dr.parent.Name() + "\" cannot produce a value") +// } + +// dr.values[columnIndex] = parser +// return expressionTrees[0] +// } + +// return dr.values[columnIndex].(*FilterExpressionParser).GetExpressionTrees()[0] +// } + +func (dr *DataRow) getComputedValue(column *DataColumn) interface{} { + // TODO: Evaluate expression using ANTLR grammar: + // https://github.com/sttp/cppapi/blob/master/src/lib/filterexpressions/FilterExpressionSyntax.g4 + // expressionTree, err := dr.getExpressionTree(column) + // sourceValue = expressionTree.Evaluate() + + // switch sourceValue.ValueType { + // case ExpressionValueType.Boolean: + // } + + return nil +} + +// Value reads the record value at the specified columnIndex. +func (dr *DataRow) Value(columnIndex int) interface{} { + column := dr.validateColumnType(columnIndex, -1, true) + + if column.Computed() { + return dr.getComputedValue(column) + } + + return dr.values[columnIndex] +} + +// ValueByName reads the record value for the specified columnName. +func (dr *DataRow) ValueByName(columnName string) interface{} { + return dr.values[dr.getColumnIndex(columnName)] +} + +// SetValue assigns the record value at the specified columnIndex. +func (dr *DataRow) SetValue(columnIndex int, value interface{}) { + dr.validateColumnType(columnIndex, -1, false) + dr.values[columnIndex] = value +} + +// SetValueByName assins the record value for the specified columnName. +func (dr *DataRow) SetValueByName(columnName string, value interface{}) { + dr.SetValue(dr.getColumnIndex(columnName), value) +} + +// ValueAsString gets the record value at the specified columnIndex as a string. +func (dr *DataRow) ValueAsString(columnIndex int) string { + column := dr.validateColumnType(columnIndex, int(DataType.String), true) + + if column.Computed() { + return dr.getComputedValue(column).(string) + } + + return dr.values[columnIndex].(string) +} + +// ValueAsStringByName gets the record value for the specified columnName as a string. +func (dr *DataRow) ValueAsStringByName(columnName string) string { + return dr.ValueAsString(dr.getColumnIndex(columnName)) +} + +// ValueAsBool gets the record value at the specified columnIndex as a bool. +func (dr *DataRow) ValueAsBool(columnIndex int) bool { + column := dr.validateColumnType(columnIndex, int(DataType.Boolean), true) + + if column.Computed() { + return dr.getComputedValue(column).(bool) + } + + return dr.values[columnIndex].(bool) +} + +// ValueAsBoolByName gets the record value for the specified columnName as a bool. +func (dr *DataRow) ValueAsBoolByName(columnName string) bool { + return dr.ValueAsBool(dr.getColumnIndex(columnName)) +} + +// ValueAsDateTime gets the record value at the specified columnIndex as a time.Time. +func (dr *DataRow) ValueAsDateTime(columnIndex int) time.Time { + column := dr.validateColumnType(columnIndex, int(DataType.DateTime), true) + + if column.Computed() { + return dr.getComputedValue(column).(time.Time) + } + + return dr.values[columnIndex].(time.Time) +} + +// ValueAsDateTimeByName gets the record value for the specified columnName as a time.Time. +func (dr *DataRow) ValueAsDateTimeByName(columnName string) time.Time { + return dr.ValueAsDateTime(dr.getColumnIndex(columnName)) +} + +// TODO: Add remaining ValueAs methods diff --git a/sttp/metadata/DataSet.go b/sttp/metadata/DataSet.go new file mode 100644 index 0000000..fb0b2b9 --- /dev/null +++ b/sttp/metadata/DataSet.go @@ -0,0 +1,132 @@ +//****************************************************************************************************** +// DataSet.go - Gbtc +// +// Copyright © 2021, Grid Protection Alliance. All Rights Reserved. +// +// Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See +// the NOTICE file distributed with this work for additional information regarding copyright ownership. +// The GPA licenses this file to you under the MIT License (MIT), the "License"; you may not use this +// file except in compliance with the License. You may obtain a copy of the License at: +// +// http://opensource.org/licenses/MIT +// +// Unless agreed to in writing, the subject software distributed under the License is distributed on an +// "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the +// License for the specific language governing permissions and limitations. +// +// Code Modification History: +// ---------------------------------------------------------------------------------------------------- +// 09/23/2021 - J. Ritchie Carroll +// Generated original version of source code. +// +//****************************************************************************************************** + +package metadata + +import "strings" + +const ( + // XmlSchemaNamespace defines the schema namespace for the W3C XML Schema Definition Language (XSD) + // used by STTP metadata tables. + XmlSchemaNamespace = "http://www.w3.org/2001/XMLSchema" + + // ExtXmlSchemaDataNamespace is used to define an WSD extended type element as a Guid value. + ExtXmlSchemaDataNamespace = "urn:schemas-microsoft-com:xml-msdata" +) + +// DataSet represents an in-memory cache of records that is structured similarly to information +// defined in a database. The data set object consists of a collection of data table objects. +// See https://sttp.github.io/documentation/data-sets/ for more information. +// Note that this implementation uses a case-insensitive map for DataTable name lookups. Internally +// this is accomplished using ToUpper to keep things simple and efficient, however, this implies that +// case-insensitivity will be effectively restricted to ASCII-based table names. +type DataSet struct { + tables map[string]*DataTable + + // Name defines the name of the DataSet. + Name string +} + +// NewDataSet creates a new DataSet. +func NewDataSet() *DataSet { + return &DataSet{ + tables: make(map[string]*DataTable), + Name: "DataSet", + } +} + +// AddTable adds the specified table to the DataSet. +func (ds *DataSet) AddTable(table *DataTable) { + ds.tables[strings.ToUpper(table.Name())] = table +} + +// Table gets the DataTable for the specified tableName if the name exists; +// otherwise, nil is returned. Lookup is case-insensitive. +func (ds *DataSet) Table(tableName string) *DataTable { + if table, ok := ds.tables[strings.ToUpper(tableName)]; ok { + return table + } + + return nil +} + +// TableNames gets the table names defined in the DataSet. +func (ds *DataSet) TableNames() []string { + tableNames := make([]string, len(ds.tables)) + + for _, table := range ds.tables { + tableNames = append(tableNames, table.Name()) + } + + return tableNames +} + +// Tables gets the DataTable instances defined in the DataSet. +func (ds *DataSet) Tables() []*DataTable { + tables := make([]*DataTable, len(ds.tables)) + + for _, table := range ds.tables { + tables = append(tables, table) + } + + return tables +} + +// CreateTable creates a new DataTable associated with the DataSet. +// Use AddTable to add the new table to the DataSet. +func (ds *DataSet) CreateTable(name string) *DataTable { + return newDataTable(ds, name) +} + +// TableCount gets the total number of tables defined in the DataSet. +func (ds *DataSet) TableCount() int { + return len(ds.tables) +} + +// RemoveTable removes the specified tableName from the DataSet. Returns +// true if table was removed; otherwise, false if it did not exist. +func (ds *DataSet) RemoveTable(tableName string) bool { + if _, ok := ds.tables[tableName]; ok { + delete(ds.tables, tableName) + return true + } + + return false +} + +// ReadXML loads the DataSet from the XML in the specified buffer. +func (ds *DataSet) ReadXML(buffer []byte) { + +} + +// WriteXML saves the DataSet information as XML into the specified buffer. +func (ds *DataSet) WriteXml(buffer *[]byte, dataSetName string) { + // TODO: Will be needed by DataPublisher +} + +// FromXml creates a new DataSet as read from the XML in the specified buffer. +func FromXml(buffer []byte) *DataSet { + dataSet := NewDataSet() + dataSet.ReadXML(buffer) + return dataSet +} diff --git a/sttp/metadata/DataTable.go b/sttp/metadata/DataTable.go new file mode 100644 index 0000000..4983378 --- /dev/null +++ b/sttp/metadata/DataTable.go @@ -0,0 +1,150 @@ +//****************************************************************************************************** +// DataTable.go - Gbtc +// +// Copyright © 2021, Grid Protection Alliance. All Rights Reserved. +// +// Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See +// the NOTICE file distributed with this work for additional information regarding copyright ownership. +// The GPA licenses this file to you under the MIT License (MIT), the "License"; you may not use this +// file except in compliance with the License. You may obtain a copy of the License at: +// +// http://opensource.org/licenses/MIT +// +// Unless agreed to in writing, the subject software distributed under the License is distributed on an +// "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the +// License for the specific language governing permissions and limitations. +// +// Code Modification History: +// ---------------------------------------------------------------------------------------------------- +// 09/23/2021 - J. Ritchie Carroll +// Generated original version of source code. +// +//****************************************************************************************************** + +package metadata + +import "strings" + +// DataTable represents a collection of DataColumn objects where each data column defines a name and +// a data type. Data columns can also be computed where its value would be derived from other columns +// and functions (https://sttp.github.io/documentation/filter-expressions/) defined in an expression. +// Note that this implementation uses a case-insensitive map for DataColumn name lookups. Internally +// this is accomplished using ToUpper to keep things simple and efficient, however, this implies that +// case-insensitivity will be effectively restricted to ASCII-based column names. +type DataTable struct { + parent *DataSet + name string + columnIndexes map[string]int + columns []*DataColumn + rows []*DataRow +} + +func newDataTable(parent *DataSet, name string) *DataTable { + return &DataTable{ + parent: parent, + name: name, + } +} + +// Parent gets the parent DataSet of the DataTable. +func (dt *DataTable) Parent() *DataSet { + return dt.parent +} + +// Name gets the name of the DataTable. +func (dt *DataTable) Name() string { + return dt.name +} + +// InitColumns initializes the internal column collection to the specified length. +// Any existing columns will be deleted. +func (dt *DataTable) InitColumns(length int) { + dt.columns = make([]*DataColumn, length) + dt.columnIndexes = make(map[string]int, length) +} + +// AddColumn adds the specified column to the DataTable. +func (dt *DataTable) AddColumn(column *DataColumn) { + column.index = len(dt.columns) + dt.columnIndexes[strings.ToUpper(column.Name())] = column.index + dt.columns = append(dt.columns, column) +} + +// Column gets the DataColumn at the specified columnIndex if the index is in range; +// otherwise, nil is returned. +func (dt *DataTable) Column(columnIndex int) *DataColumn { + if columnIndex < 0 || columnIndex >= len(dt.columns) { + return nil + } + + return dt.columns[columnIndex] +} + +// ColumnByName gets the DataColumn for the specified columnName if the name exists; +// otherwise, nil is returned. Lookup is case-insensitive. +func (dt *DataTable) ColumnByName(columnName string) *DataColumn { + if columnIndex, ok := dt.columnIndexes[strings.ToUpper(columnName)]; ok { + return dt.Column(columnIndex) + } + + return nil +} + +// CreateColumn creates a new DataColumn associated with the DataTable. +// Use AddColumn to add the new column to the DataTable. +func (dt *DataTable) CreateColumn(name string, dataType DataTypeEnum, expression string) *DataColumn { + return newDataColumn(dt, name, dataType, expression) +} + +// CloneColumn creates a copy of the specified source DataColumn associated with the DataTable. +func (dt *DataTable) CloneColumn(source *DataColumn) *DataColumn { + return dt.CreateColumn(source.Name(), source.Type(), source.Expression()) +} + +// ColumnCount gets the total number columns defined in the DataTable. +func (dt *DataTable) ColumnCount() int { + return len(dt.columns) +} + +// InitRows initializes the internal row collection to the specified length. +// Any existing rows will be deleted. +func (dt *DataTable) InitRows(length int) { + dt.rows = make([]*DataRow, length) +} + +// AddRow adds the specified row to the DataTable. +func (dt *DataTable) AddRow(row *DataRow) { + dt.rows = append(dt.rows, row) +} + +// Row gets the DataRow at the specified rowIndex if the index is in range; +// otherwise, nil is returned. +func (dt *DataTable) Row(rowIndex int) *DataRow { + if rowIndex < 0 || rowIndex >= len(dt.rows) { + return nil + } + + return dt.rows[rowIndex] +} + +// CreateRow creates a new DataRow associated with the DataTable. +// Use AddRow to add the new row to the DataTable. +func (dt *DataTable) CreateRow() *DataRow { + return newDataRow(dt) +} + +// CloneRow creates a copy of the specified source DataRow associated with the DataTable. +func (dt *DataTable) CloneRow(source *DataRow) *DataRow { + row := dt.CreateRow() + + for i := 0; i < len(dt.columns); i++ { + row.SetValue(i, source.Value(i)) + } + + return row +} + +// RowCount gets the total number of rows defined in the DataTable. +func (dt *DataTable) RowCount() int { + return len(dt.rows) +} diff --git a/sttp/metadata/DataType.go b/sttp/metadata/DataType.go new file mode 100644 index 0000000..0cdabdf --- /dev/null +++ b/sttp/metadata/DataType.go @@ -0,0 +1,117 @@ +//****************************************************************************************************** +// DataType.go - Gbtc +// +// Copyright © 2021, Grid Protection Alliance. All Rights Reserved. +// +// Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See +// the NOTICE file distributed with this work for additional information regarding copyright ownership. +// The GPA licenses this file to you under the MIT License (MIT), the "License"; you may not use this +// file except in compliance with the License. You may obtain a copy of the License at: +// +// http://opensource.org/licenses/MIT +// +// Unless agreed to in writing, the subject software distributed under the License is distributed on an +// "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the +// License for the specific language governing permissions and limitations. +// +// Code Modification History: +// ---------------------------------------------------------------------------------------------------- +// 09/23/2021 - J. Ritchie Carroll +// Generated original version of source code. +// +//****************************************************************************************************** + +package metadata + +// DataTypeEnum defines the type for the DataType enumeration. +type DataTypeEnum int + +// DataType is an enumeration of the possible data types for a DataColumn. +var DataType = struct { + // String represents a Go string data type. + String DataTypeEnum + // Boolean represents a Go bool data type. + Boolean DataTypeEnum + // DateTime represents a Go time.Time data type. + DateTime DataTypeEnum + // Single represents a Go float32 data type. + Single DataTypeEnum + // Double represents a Go float64 data type. + Double DataTypeEnum + // Decimal represents a Go decimal.Decimal data type. + // Type defined in github.com/shopspring/decimal. + Decimal DataTypeEnum + // Guid represents a Go guid.Guid data type. + // Type defined in github.com/sttp/goapi/sttp/guid. + Guid DataTypeEnum + // Int8 represents a Go int8 data type. + Int8 DataTypeEnum + // Int16 represents a Go int16 data type. + Int16 DataTypeEnum + // Int32 represents a Go int32 data type. + Int32 DataTypeEnum + // Int64 represents a Go int64 data type. + Int64 DataTypeEnum + // UInt8 represents a Go uint8 data type. + UInt8 DataTypeEnum + // UInt16 represents a Go uint16 data type. + UInt16 DataTypeEnum + // UInt32 represents a Go uint32 data type. + UInt32 DataTypeEnum + // UInt64 represents a Go uint64 data type. + UInt64 DataTypeEnum +}{ + String: 0, + Boolean: 1, + DateTime: 2, + Single: 3, + Double: 4, + Decimal: 5, + Guid: 6, + Int8: 7, + Int16: 8, + Int32: 9, + Int64: 10, + UInt8: 11, + UInt16: 12, + UInt32: 13, + UInt64: 14, +} + +// Name gets the DataType enumeration name as a string. +func (dte DataTypeEnum) Name() string { + switch dte { + case DataType.String: + return "String" + case DataType.Boolean: + return "Boolean" + case DataType.DateTime: + return "DateTime" + case DataType.Single: + return "Single" + case DataType.Double: + return "Double" + case DataType.Decimal: + return "Decimal" + case DataType.Guid: + return "Guid" + case DataType.Int8: + return "Int8" + case DataType.Int16: + return "Int16" + case DataType.Int32: + return "Int32" + case DataType.Int64: + return "Int64" + case DataType.UInt8: + return "UInt8" + case DataType.UInt16: + return "UInt16" + case DataType.UInt32: + return "UInt32" + case DataType.UInt64: + return "UInt64" + default: + return "Undefined" + } +} diff --git a/sttp/transport/SignalKind.go b/sttp/transport/SignalKind.go index 971a486..0b6ed4f 100644 --- a/sttp/transport/SignalKind.go +++ b/sttp/transport/SignalKind.go @@ -72,44 +72,72 @@ var SignalKind = struct { Unknown: 11, } -var ( - // SignalKindDescription defines the string representations of the SignalKind enumeration values. - SignalKindDescription = [...]string{ - "Angle", - "Magnitude", - "Frequency", - "DfDt", - "Status", - "Digital", - "Analog", - "Calculation", - "Statistic", - "Alarm", - "Quality", - "Unknown"} - - // SignalKindAcronym defines the abbreviated string representations of the SignalKind enumeration values. - SignalKindAcronym = [...]string{ - "PA", - "PM", - "FQ", - "DF", - "SF", - "DV", - "AV", - "CV", - "ST", - "AL", - "QF", - "??"} -) +// Name gets the SignalKind enumeration name as a string. +func (ske SignalKindEnum) Name() string { + switch ske { + case SignalKind.Angle: + return "Angle" + case SignalKind.Magnitude: + return "Magnitude" + case SignalKind.Frequency: + return "Frequency" + case SignalKind.DfDt: + return "DfDt" + case SignalKind.Status: + return "Status" + case SignalKind.Digital: + return "Digital" + case SignalKind.Analog: + return "Analog" + case SignalKind.Calculation: + return "Calculation" + case SignalKind.Statistic: + return "Statistic" + case SignalKind.Alarm: + return "Alarm" + case SignalKind.Quality: + return "Quality" + default: + return "Unknown" + } +} + +// Acronym gets the SignalKind enumeration value as its two-character acronym string. +func (ske SignalKindEnum) Acronym() string { + switch ske { + case SignalKind.Angle: + return "PA" + case SignalKind.Magnitude: + return "PM" + case SignalKind.Frequency: + return "FQ" + case SignalKind.DfDt: + return "DF" + case SignalKind.Status: + return "SF" + case SignalKind.Digital: + return "DV" + case SignalKind.Analog: + return "AV" + case SignalKind.Calculation: + return "CV" + case SignalKind.Statistic: + return "ST" + case SignalKind.Alarm: + return "AL" + case SignalKind.Quality: + return "QF" + default: + return "??" + } +} // SignalTypeAcronym gets the specific four-character signal type acronym for a SignalKind // enumeration value and phasor type, i.e., "V" voltage or "I" current. -func SignalTypeAcronym(kind SignalKindEnum, phasorType rune) string { +func (ske SignalKindEnum) SignalTypeAcronym(phasorType rune) string { // A SignalType represents a more specific measurement type than SignalKind, i.e., // a phasor type (voltage or current) can also be determined by the type. - switch kind { + switch ske { case SignalKind.Angle: if unicode.ToUpper(phasorType) == 'V' { return "VPHA" @@ -143,7 +171,7 @@ func SignalTypeAcronym(kind SignalKindEnum, phasorType rune) string { return "NULL" } -// ParseSignalKindAcronym gets the SignalKind enumeration value for the specified acronym. +// ParseSignalKindAcronym gets the SignalKind enumeration value for the specified two-character acronym. func ParseSignalKindAcronym(acronym string) SignalKindEnum { acronym = strings.TrimSpace(strings.ToUpper(acronym))