Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SylkParser bug fixes #61

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
217 changes: 142 additions & 75 deletions src/War3Net.IO.Slk/SylkParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,138 +6,205 @@
// ------------------------------------------------------------------------------

using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;

namespace War3Net.IO.Slk
{
public sealed class SylkParser
{
private SylkTable _table;
private int? _lastY;

public SylkParser()
public SylkTable Parse(Stream input, bool leaveOpen = false)
{
_lastY = null;
var lines = new List<string>();
using var reader = new StreamReader(input, Encoding.UTF8, true, 1024, leaveOpen);
{
string line;
while ((line = reader.ReadLine()) != null)
{
lines.Add(line);
}
}

return Parse(lines);
}

public SylkTable Parse(Stream input, bool leaveOpen = false)
public SylkTable Parse(List<string> lines)
{
using var reader = new StreamReader(input, Encoding.UTF8, true, 1024, leaveOpen);
int? maxX = null;
int? maxY = null;

var isOnFirstLine = true;
while (true)
var bLine = lines.FirstOrDefault(x => x.StartsWith("B;", StringComparison.InvariantCultureIgnoreCase));
if (!string.IsNullOrWhiteSpace(bLine))
{
var line = reader.ReadLine();
var fields = line.Split(';');
var recordType = fields[0];

string GetField(string fieldName, bool mandatory)
var parts = bLine.Split(';', StringSplitOptions.TrimEntries);
foreach (var part in parts)
{
foreach (var field in fields)
if (part.StartsWith("X", StringComparison.InvariantCultureIgnoreCase))
{
if (field.StartsWith(fieldName))
if (int.TryParse(part.Substring(1), out int x))
{
return field.Substring(fieldName.Length);
maxX = x;
}
}

if (mandatory)
else if (part.StartsWith("Y", StringComparison.InvariantCultureIgnoreCase))
{
throw new InvalidDataException($"Record does not contain mandatory field of type '{fieldName}'.");
if (int.TryParse(part.Substring(1), out int y))
{
maxY = y;
}
}
else if (part.StartsWith("D", StringComparison.InvariantCultureIgnoreCase))
{
var dContent = part.Substring(1).Trim();
var dParts = dContent.Split(' ');
if (dParts.Length == 2)
{
if (int.TryParse(dParts[0], out int y))
{
maxY ??= y;
}
if (int.TryParse(dParts[1], out int x))
{
maxX ??= x;
}
}
else if (dParts.Length == 4)
{
int.TryParse(dParts[0], out int startY);
int.TryParse(dParts[1], out int startX);
if (int.TryParse(dParts[2], out int y))
{
maxY ??= (y - startY);
}
if (int.TryParse(dParts[3], out int x))
{
maxX ??= (x - startX);
}
}
}

return null;
}
}

var _table = new SylkTable(maxX ?? 0, maxY ?? 0);

if (isOnFirstLine)

int nextX = 0;
int nextY = 0;

foreach (var line in lines)
{
var isCell = line.StartsWith("C;", StringComparison.InvariantCultureIgnoreCase);
var isFormatting = line.StartsWith("F;", StringComparison.InvariantCultureIgnoreCase);
if (isCell || isFormatting)
{
isOnFirstLine = false;
if (recordType != "ID")
int? x = null;
int? y = null;
object value = null;

var parts = line.Split(";", StringSplitOptions.TrimEntries);
foreach (var part in parts)
{
throw new InvalidDataException("SYLK file must start with 'ID'.");
if (part.StartsWith("X", StringComparison.InvariantCultureIgnoreCase))
{
if (int.TryParse(part.Substring(1), out int parsedX))
{
x = parsedX - 1;
}
}
else if (part.StartsWith("Y", StringComparison.InvariantCultureIgnoreCase))
{
if (int.TryParse(part.Substring(1), out int parsedY))
{
y = parsedY - 1;
}
}
else if (part.StartsWith("K", StringComparison.InvariantCultureIgnoreCase))
{
value = ParseValueString(part.Substring(1));
}
}

GetField("P", true);
}
else
{
switch (recordType)
if (isFormatting && x == null && y == null)
{
case "ID":
throw new InvalidDataException("Record type 'ID' can only occur on the first line.");
continue;
}

case "B":
if (_table != null)
{
throw new InvalidDataException("Only one record of type 'B' may be present.");
}
x ??= nextX;
y ??= nextY;

_table = new SylkTable(int.Parse(GetField("X", true)), int.Parse(GetField("Y", true)));
break;
nextX = x.Value;
nextY = y.Value;

case "C":
if (_table == null)
{
throw new InvalidDataException("Unable to parse record of type 'C' before encountering a record of type 'B'.");
}
if (isCell)
{
nextX++;
}

SetCellContent(GetField("X", true), GetField("Y", false), GetField("K", false));
break;
if (maxX.HasValue && nextX >= maxX)
{
nextX = 0;
nextY++;
}

case "E":
return _table;
if (value != null)
{
if (x.Value >= _table.Width || y.Value >= _table.Height)
{
_table.Resize(Math.Max(x.Value+1, _table.Width), Math.Max(y.Value+1, _table.Height));
}

default:
throw new NotSupportedException($"Support for record type '{recordType}' is not implemented. Only records of type 'ID', 'B', 'C', and 'E' are supported.");
_table[x.Value, y.Value] = value;
}
}
}

return _table;
}

/// <param name="x">The cell's 1-indexed X position.</param>
/// <param name="y">The cell's 1-indexed Y position.</param>
private void SetCellContent(string x, string? y, string value)
private object ParseValueString(string value)
{
if (y == null && _lastY == null)
if (value.StartsWith('"'))
{
throw new InvalidDataException("Row for cell is not defined.");
}

var xi = int.Parse(x, NumberStyles.Integer, CultureInfo.InvariantCulture) - 1;
var yi = y == null ? _lastY.Value : (int.Parse(y, NumberStyles.Integer, CultureInfo.InvariantCulture) - 1);
if (!value.EndsWith('"'))
{
value = value + "\"";
}

if (value.StartsWith('"') && value.EndsWith('"'))
return value.Substring(1, value.Length - 2);
}
else if (string.Equals(value, bool.TrueString, StringComparison.OrdinalIgnoreCase))
{
_table[xi, yi] = value[1..^1];
return true;
}
else if (int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var @int))
else if (string.Equals(value, bool.FalseString, StringComparison.OrdinalIgnoreCase))
{
_table[xi, yi] = @int;
return false;
}
else if (float.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out var @float))
else if (string.Equals(value, "#VALUE!", StringComparison.Ordinal) || string.Equals(value, "#REF!", StringComparison.Ordinal) || string.IsNullOrEmpty(value))
{
_table[xi, yi] = @float;
return 0;
}
else if (string.Equals(value, bool.TrueString, StringComparison.OrdinalIgnoreCase))
else if (int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intValue))
{
_table[xi, yi] = true;
return intValue;
}
else if (string.Equals(value, bool.FalseString, StringComparison.OrdinalIgnoreCase))
else if (float.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out var floatValue))
{
_table[xi, yi] = false;
return floatValue;
}
else if (string.Equals(value, "#VALUE!", StringComparison.Ordinal) || string.Equals(value, "#REF!", StringComparison.Ordinal))
else if (int.TryParse("0" + value, NumberStyles.Integer, CultureInfo.InvariantCulture, out intValue))
{
_table[xi, yi] = 0;
return intValue;
}
else
else if (float.TryParse("0" + value, NumberStyles.Float, CultureInfo.InvariantCulture, out floatValue))
{
throw new NotSupportedException($"Unable to parse value '{value}'. Can only parse strings, integers, floats, and booleans.");
return floatValue;
}

_lastY = yi;
return null;
}
}
}
28 changes: 25 additions & 3 deletions src/War3Net.IO.Slk/SylkTable.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ namespace War3Net.IO.Slk
{
public sealed class SylkTable : IEnumerable<object[]>
{
private readonly int _width;
private readonly int _height;
private readonly object[,] _values;
private int _width;
private int _height;
private object[,] _values;

private int _rows;
private int _columns;
Expand All @@ -28,6 +28,28 @@ public SylkTable(int width, int height)
_values = new object[_width, _height];
}

public void Resize(int width, int height)
{
var oldWidth = _width;
var oldHeight = _height;
var oldValues = _values;

_width = width;
_height = height;
_values = new object[_width, _height];

for (int column = 0; column < Math.Min(_width, oldWidth); column++)
{
for (int row = 0; row < Math.Min(height, oldHeight); row++)
{
_values[column, row] = oldValues[column, row];
}
}

_rows = Math.Min(_rows, _height-1);
_columns = Math.Min(_columns, _width-1);
}

/// <summary>
/// Gets the maximum amount of columns that can fit in the <see cref="SylkTable"/>.
/// </summary>
Expand Down