Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
0d91568
move class WVP to new namespace Parsers
jamaa Mar 14, 2026
47447d0
introduce a Parser base class which is inherited by WVP
jamaa Mar 14, 2026
254033a
Make FileReference a proper class and add docstrings
jamaa Mar 14, 2026
b70a36c
Add TalsimClipboard Parser class
jamaa Mar 14, 2026
739862a
add a Parser.verifyFormat method
jamaa Mar 14, 2026
974172c
remove call to WEL.extractFromWLZIP from TimeSeriesFile, as it is now…
jamaa Mar 14, 2026
b74dc4f
overload Parser.Process in TalsimClipboard so that we can delete any …
jamaa Mar 14, 2026
a4ad681
move Talsim-specific tests to a separate class and add a new test Tes…
jamaa Mar 14, 2026
602727c
move extractFromWLZIP method to TalsimClipboard
jamaa Mar 14, 2026
c130e17
update changelog
jamaa Mar 14, 2026
1811845
simplify by reusing TimeSeriesDisplayOptions class
jamaa Mar 20, 2026
a77110d
centralize parsing of interpretation from string and integer
jamaa Mar 20, 2026
430913e
check for optional "Einheit" entry
jamaa Mar 20, 2026
a1d4562
add more tests for Talsim clipboard
jamaa Mar 20, 2026
f4d855a
add docstring and warning log entry on failure to delete extracted fi…
jamaa Mar 20, 2026
a80c94c
update changelog
jamaa Mar 20, 2026
dbaf5c6
Merge branch 'master' into parsers
jamaa Mar 20, 2026
95fc2bc
rename test method
jamaa Mar 20, 2026
db4bff0
upload test results even if tests fail
jamaa Mar 20, 2026
e17dd37
more robust splitting of lines in clipboard text
jamaa Mar 20, 2026
81c8ad2
make LoadFromClipboard_TALSIM private again
jamaa Mar 20, 2026
31a1cc7
do not instantiate a Wave instance when testing as that can cause a m…
jamaa Mar 20, 2026
b5d3cf9
change workdir in Talsim tests to allow for use of relative paths in …
jamaa Mar 20, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/run-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ jobs:
dotnet test BlueM.Wave\tests\bin\x64\Debug\Wave.Tests.dll --settings BlueM.Wave\tests\tests.runsettings

- name: Upload test results
if: always()
uses: actions/upload-artifact@v6
with:
name: test-results
Expand Down
8 changes: 8 additions & 0 deletions source/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,15 @@ BlueM.Wave Release Notes

Version X.X.X
-------------
FIXED:
* WEL files extracted from WLZIP archives when processing Talsim clipboard data are now deleted after use #136

CHANGED:
* Automatic extraction of WEL files from WLZIP archives now only occurs when processing Talsim clipboard data

API-CHANGES:
* New namespace `Parsers` containing classes for parsing file import instructions such as WVP files and Talsim clipboard content
* New helper method `Helpers.ParseInterpretation()` for parsing interpretation from strings and integers
* Renamed `TimeSeriesFile.Write_File()` functions to `writeFile()` for consistency

Version 2.16.0
Expand Down
2 changes: 1 addition & 1 deletion source/CLI.vb
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ Friend Class CLI
Throw New NotImplementedException("TEN files are currently not supported in the CLI!")

Case TimeSeriesFile.FileExtensions.WVP
Dim wvp As New Fileformats.WVP(file_in)
Dim wvp As New Parsers.WVP(file_in)
Dim wvpSeries As List(Of TimeSeries) = wvp.Process()
Log.AddLogEntry(Log.levels.info, $"Imported {wvpSeries.Count} time series")
tsList.AddRange(wvpSeries)
Expand Down
9 changes: 1 addition & 8 deletions source/Classes/TimeSeriesFile.vb
Original file line number Diff line number Diff line change
Expand Up @@ -664,14 +664,7 @@ Public MustInherit Class TimeSeriesFile

'Check whether the file exists
If Not IO.File.Exists(file) Then
'A WEL/WBL file may be zipped within a WLZIP file, so try extracting it from there
If fileExt = FileExtensions.WEL Or fileExt = FileExtensions.WBL Then
If Not Fileformats.WEL.extractFromWLZIP(file) Then
Throw New IO.FileNotFoundException($"File '{file}' not found!")
End If
Else
Throw New IO.FileNotFoundException($"File '{file}' not found!")
End If
Throw New IO.FileNotFoundException($"File '{file}' not found!")
End If

'set default
Expand Down
18 changes: 9 additions & 9 deletions source/Controllers/WaveController.vb
Original file line number Diff line number Diff line change
Expand Up @@ -561,15 +561,15 @@ Friend Class WaveController
End If
tsList.Add(ts)
Next
Call Fileformats.WVP.writeFile(tsList, dlg.FileName,
saveRelativePaths:=dlg.SaveRelativePaths,
saveTitle:=dlg.SaveTitle,
saveUnit:=dlg.SaveUnit,
saveInterpretation:=dlg.SaveInterpretation,
saveColor:=dlg.SaveColor,
saveLineStyle:=dlg.SaveLineStyle,
saveLineWidth:=dlg.SaveLineWidth,
savePointsVisibility:=dlg.SavePointsVisibility
Call Parsers.WVP.writeFile(tsList, dlg.FileName,
saveRelativePaths:=dlg.SaveRelativePaths,
saveTitle:=dlg.SaveTitle,
saveUnit:=dlg.SaveUnit,
saveInterpretation:=dlg.SaveInterpretation,
saveColor:=dlg.SaveColor,
saveLineStyle:=dlg.SaveLineStyle,
saveLineWidth:=dlg.SaveLineWidth,
savePointsVisibility:=dlg.SavePointsVisibility
)
MsgBox($"Wave project file {dlg.FileName} saved.", MsgBoxStyle.Information)
End If
Expand Down
50 changes: 0 additions & 50 deletions source/FileFormats/WEL.vb
Original file line number Diff line number Diff line change
Expand Up @@ -310,56 +310,6 @@ Namespace Fileformats

End Function

''' <summary>
''' Attempts to extract a specified WEL file from a WLZIP file of the same name
''' </summary>
''' <param name="file">path to WEL file</param>
''' <returns>True if successful</returns>
''' <remarks>TALSIM specific</remarks>
Public Shared Function extractFromWLZIP(file As String) As Boolean

Dim file_wlzip As String
Dim filename As String
Dim zipEntryFound As Boolean = False
Dim success As Boolean = False

'determine WLZIP filename for files ending with .WEL (may also be e.g. .KTR.WEL, .CHLO.WEL, etc.)
Dim m As Match = Regex.Match(file, "^(.+?)(\.[a-z]+)?\.WEL$", RegexOptions.IgnoreCase)
If m.Success Then
file_wlzip = $"{m.Groups(1)}.WLZIP"

If IO.File.Exists(file_wlzip) Then

Log.AddLogEntry(Log.levels.info, $"Looking for file in {file_wlzip} ...")
filename = IO.Path.GetFileName(file)

Dim zip As IO.Compression.ZipArchive = IO.Compression.ZipFile.OpenRead(file_wlzip)

For Each entry As IO.Compression.ZipArchiveEntry In zip.Entries
If entry.Name.ToLower() = filename.ToLower() Then
zipEntryFound = True
'extract file from zip archive
Log.AddLogEntry(Log.levels.info, $"Extracting file from {file_wlzip} ...")
Dim fs As New IO.FileStream(file, FileMode.CreateNew)
entry.Open().CopyTo(fs)
fs.Flush()
fs.Close()
success = True
Exit For
End If
Next

If Not zipEntryFound Then
Log.AddLogEntry(Log.levels.error, $"File {filename} not found in {file_wlzip}!")
End If

End If
End If

Return success

End Function

#End Region 'Methoden

End Class
Expand Down
30 changes: 30 additions & 0 deletions source/Helpers.vb
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,36 @@ Public Module Helpers

End Function

''' <summary>
''' Parses a string to a TimeSeries.InterpretationEnum value.
''' If the string does not match any of the enum names, TimeSeries.InterpretationEnum.Undefined is returned and a warning is logged.
''' </summary>
''' <param name="interpretationString">The string to be parsed</param>
''' <returns>The corresponding TimeSeries.InterpretationEnum value</returns>
Public Function ParseInterpretation(interpretationString As String) As TimeSeries.InterpretationEnum
If Not [Enum].IsDefined(GetType(TimeSeries.InterpretationEnum), interpretationString) Then
Log.AddLogEntry(levels.warning, $"Interpretation {interpretationString} is not recognized!")
Return TimeSeries.InterpretationEnum.Undefined
Else
Return [Enum].Parse(GetType(TimeSeries.InterpretationEnum), interpretationString)
End If
End Function

''' <summary>
''' Parses an integer to a TimeSeries.InterpretationEnum value.
''' If the integer does not match any of the enum values, TimeSeries.InterpretationEnum.Undefined is returned and a warning is logged.
''' </summary>
''' <param name="interpretationValue">The integer value to be parsed</param>
''' <returns>The corresponding TimeSeries.InterpretationEnum value</returns>
Public Function ParseInterpretation(interpretationValue As Integer) As TimeSeries.InterpretationEnum
If Not [Enum].IsDefined(GetType(TimeSeries.InterpretationEnum), interpretationValue) Then
Log.AddLogEntry(levels.warning, $"Interpretation {interpretationValue} is not recognized!")
Return TimeSeries.InterpretationEnum.Undefined
Else
Return [Enum].Parse(GetType(TimeSeries.InterpretationEnum), interpretationValue)
End If
End Function

''' <summary>
''' Returns a specified color palette
''' </summary>
Expand Down
224 changes: 224 additions & 0 deletions source/Parsers/Parser.vb
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
'BlueM.Wave
'Copyright (C) BlueM Dev Group
'<https://www.bluemodel.org>
'
'This program is free software: you can redistribute it and/or modify
'it under the terms of the GNU Lesser General Public License as published by
'the Free Software Foundation, either version 3 of the License, or
'(at your option) any later version.
'
'This program is distributed in the hope that it will be useful,
'but WITHOUT ANY WARRANTY; without even the implied warranty of
'MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
'GNU Lesser General Public License for more details.
'
'You should have received a copy of the GNU Lesser General Public License
'along with this program. If not, see <https://www.gnu.org/licenses/>.
'
Namespace Parsers

''' <summary>
''' Base class for parsing file import instructions
''' </summary>
Public MustInherit Class Parser

''' <summary>
''' A reference to a file to be imported with all information needed for importing series from that file
''' </summary>
Protected Class FileReference

''' <summary>
''' The path to the file to be imported
''' </summary>
Public path As String

''' <summary>
''' The series to be imported from the file with their options, where the key is the name of the series as it appears in the file and the value is a SeriesOptions object containing options for importing that series
''' An empty dictionary means that all series contained in the file should be imported with default options
''' </summary>
Public series As Dictionary(Of String, SeriesOptions)

''' <summary>
''' File import settings as key value pairs, e.g. separator, date format, etc.
''' An empty dictionary means that default settings should be used for importing the file.
''' </summary>
Public settings As Dictionary(Of String, String)

Public Sub New()
Me.series = New Dictionary(Of String, SeriesOptions)
Me.settings = New Dictionary(Of String, String)
End Sub

End Class

''' <summary>
''' Holds options for importing a series, e.g. display options, custom title and unit, etc.
''' </summary>
Protected Class SeriesOptions
Public title As String
Public unit As String
Public interpretation As TimeSeries.InterpretationEnum?
Public displayOptions As TimeSeriesDisplayOptions
Public Sub New()
Me.displayOptions = New TimeSeriesDisplayOptions()
End Sub
End Class

''' <summary>
''' The path to the input file
''' </summary>
Protected InputFile As String

''' <summary>
''' The input text to be parsed
''' </summary>
Protected InputText As String

Protected FileReferences As New List(Of FileReference)

''' <summary>
''' Instantiates a new Parser instance with either a file or text input and calls the Parse method to parse the input
''' </summary>
''' <param name="inputfile">The path to the input file</param>
''' <param name="inputtext">The input text</param>
Public Sub New(Optional inputfile As String = Nothing, Optional inputtext As String = Nothing)
If IsNothing(inputfile) And IsNothing(inputtext) Then
Throw New ArgumentException("Either a file or text input must be provided!")
ElseIf Not IsNothing(inputfile) And Not IsNothing(inputtext) Then
Throw New ArgumentException("Only one of file or text input can be provided!")
End If
Me.InputFile = inputfile
If Not IsNothing(inputfile) Then
Me.InputText = IO.File.ReadAllText(inputfile, Text.Encoding.UTF8)
Else
Me.InputText = inputtext
End If
Call Me.Parse()
End Sub

Public Shared Function verifyFormat() As Boolean
'default implementation always returns true, can be overridden in subclasses to implement specific format verification
Return True
End Function

''' <summary>
''' Parses the input and stores the results in the FileReferences list
''' </summary>
Protected MustOverride Sub Parse()

''' <summary>
''' Processes the FileReferences list by reading the specified series from the files and returning them as a list
''' </summary>
''' <returns>a list of time series</returns>
Public Function Process() As List(Of TimeSeries)

Dim tsList As New List(Of TimeSeries)

'loop over file list
For Each fileRef As FileReference In FileReferences

Log.AddLogEntry(Log.levels.info, $"Reading file {fileRef.path} ...")

'get an instance of the file
Dim fileInstance As TimeSeriesFile = TimeSeriesFile.getInstance(fileRef.path)

'apply custom file import settings
If fileRef.settings.Count > 0 Then
For Each key As String In fileRef.settings.Keys
Dim value As String = fileRef.settings(key)
Try
Select Case key.ToLower()
Case "iscolumnseparated"
fileInstance.IsColumnSeparated = If(value.ToLower() = "true", True, False)
Case "separator"
fileInstance.Separator = New Character(value)
Case "dateformat"
fileInstance.Dateformat = value
Case "decimalseparator"
fileInstance.DecimalSeparator = New Character(value)
Case "ilineheadings"
fileInstance.iLineHeadings = Convert.ToInt32(value)
Case "ilineunits"
fileInstance.iLineUnits = Convert.ToInt32(value)
Case "ilinedata"
fileInstance.iLineData = Convert.ToInt32(value)
Case "useunits"
fileInstance.UseUnits = If(value.ToLower() = "true", True, False)
Case "columnwidth"
fileInstance.ColumnWidth = Convert.ToInt32(value)
Case "datetimecolumnindex"
fileInstance.DateTimeColumnIndex = Convert.ToInt32(value)
Case Else
Log.AddLogEntry(Log.levels.warning, $"Setting '{key}' was not recognized and was ignored!")
End Select
Catch ex As Exception
Log.AddLogEntry(Log.levels.warning, $"Setting '{key}' with value '{value}' could not be parsed and was ignored!")
End Try
Next
'reread columns with new settings
fileInstance.readSeriesInfo()
End If

'select series for importing
If fileRef.series.Count = 0 Then
'read all series contained in the file
Call fileInstance.selectAllSeries()
Else
'loop over series names
Dim seriesNames As List(Of String) = fileRef.series.Keys.ToList()
Dim seriesNotFound As New List(Of String)
For Each seriesName As String In seriesNames
Dim found As Boolean = fileInstance.selectSeries(seriesName)
If Not found Then
seriesNotFound.Add(seriesName)
Log.AddLogEntry(Log.levels.error, $"Series {seriesName} not found in file, skipping series!")
End If
Next
'remove series that were not found from the list
For Each seriesName As String In seriesNotFound
seriesNames.Remove(seriesName)
Next
'if no series remain to be imported, abort reading the file altogether
If seriesNames.Count = 0 Then
Log.AddLogEntry(Log.levels.error, "No series left to import, skipping file!")
Continue For
End If

End If

'read the file
fileInstance.readFile()

'Log
Call Log.AddLogEntry(Log.levels.info, $"File '{fileRef.path}' imported successfully!")

'store the series
For Each ts As TimeSeries In fileInstance.TimeSeries.Values
'set series options
If fileRef.series.ContainsKey(ts.Title) Then
Dim options As SeriesOptions = fileRef.series(ts.Title)
'options affecting the time series itself
If Not IsNothing(options.title) Then
ts.Title = options.title
End If
If Not IsNothing(options.unit) Then
ts.Unit = options.unit
End If
If options.interpretation.HasValue Then
ts.Interpretation = options.interpretation
End If
'display options
ts.DisplayOptions = options.displayOptions
End If
'store the time series in the list
tsList.Add(ts)
Next
Next

Return tsList

End Function

End Class

End Namespace
Loading
Loading