Reading a Number from an Excel Sheet is changing the value - c#

I have a C# program that reads in an excel workbook, and then builds an XML file that can be ran against an XSLT. The problem that has cropped up is that one of the fields is a number, and when reading it out of the excel sheet the value is being changed. Here is the example:
The excel spreadsheet is read in, and the data is loaded into a data table. One of the ways I do this is by taking the spreadsheet document I create, and pass the cell reference into this method here:
dataRow[columnIndex] = GetCellValue(spreadSheetDocument, cell);
private static string GetCellValue(SpreadsheetDocument document, Cell cell)
{
//This process uses the OpenXML SDK to get individual cells values to populate the DataTable
SharedStringTablePart stringTablePart = document.WorkbookPart.SharedStringTablePart;
string value = "";
//One of the things that needed to be accounted for was empty cells
try
{
value = cell.CellValue.InnerXml;
}
catch (Exception)
{
value = "";
}
//Setting cell data type right now just sets everything to strings
//Later, the better option will be to work on the Date Conversions and Data Types here
if (cell.DataType != null && cell.DataType.Value == CellValues.SharedString)
{
return stringTablePart.SharedStringTable.ChildElements[Int32.Parse(value)].InnerText;
}
else
{
return value;
}
}
So as an example, if the cell that it is reading in is 115, then the output looks like this:
114.99999999999999
Then at other times if the value is 125 then the output looks like this:
125.00000000000001
the inconsistency in the output is a bit perplexing. Was hoping maybe I could get some insight into what is causing this, rather than just fixing it in the XSLT later on.

So I found a workaround, more than an actual solution. Apparently this is a bug in the OpenXML SDK. I found the initial documentation that pointed me in this direction here
What I found as a way to work around was this:
private static string GetCellValue(SpreadsheetDocument document, Cell cell)
{
//This process uses the OpenXML SDK to get individual cells values to populate the DataTable
SharedStringTablePart stringTablePart = document.WorkbookPart.SharedStringTablePart;
string value = "";
//One of the things that needed to be accounted for was empty cells
try
{
value = cell.CellValue.InnerXml;
}
catch (Exception)
{
value = "";
}
//Checking to see if this string contains a decimal with values on either side
if (Regex.IsMatch(value, regexpattern))
{
value = Math.Round(Double.Parse(value), 0, MidpointRounding.AwayFromZero).ToString();
}
//Setting cell data type right now just sets everything to strings
//Later, the better option will be to work on the Date Conversions and Data Types here
if (cell.DataType != null && cell.DataType.Value == CellValues.SharedString)
{
return stringTablePart.SharedStringTable.ChildElements[Int32.Parse(value)].InnerText;
}
else
{
return value;
}
}
I'm using Regex to determine if this bug is being encountered and then compensating using some rounding. It is interesting to note that only appears to happen with integers.
Thanks!

Related

Can't read Excel cell value in Visual C#

I read How to read single Excel cell value and tried it myself. But when I gets to
string s = (myExcelWorkSheet.Cells[3, "E"] as Excel.Range).Value2.ToString();
Everything was terminated and the form was shown.
//Everything worked fine here.
string s = (myExcelWorkSheet.Cells[3, "E"] as Excel.Range).Value2.ToString();
//Everything after this was all skipped!
Why is this, and how can I fix it?
The problem with reading excel cell is that if there is nothing in it, the cell object is Null. Thus, it does not have .Value2 neither .Value.
To find a way how to avoid the check for Null, you may use Convert.ToString() which evaluates the Null to an empty string and thus does not return an error:
for (int i = 1; i < 5; i++)
{
string a = Convert.ToString(wk.Cells[i, 1].Value2);
Console.WriteLine(a);
}
When the cell has a value, you need the ToString().
And when the cell doesn't has a value, then you don't need the ToString()!
Otherwise the whole program will skip out and everything after that was NEVER executed!!!
So I guess it's just a problem of if the system was trying to cast a null value into a string or not!!!

Axosoft Report Builder (Active Reports 3) - Conditional Results Depending on Field Value

I am trying to create a custom report using Axosoft's Report Builder though I have one big issue to tackle. The report will contain calculated totals and counts of the specific items that are pulled in from the database to display. One such field (Custom_163) is a boolean where true will have it be one type of item (Enhancement) while false will have it as another (Maintenance). I am able to get a count of the total amount of items in the report, but I want to break it down as percentages of the total.
Problem: One boolean field will determine two types of items. Differentiating between them to gather a count of each is where I'm struggling.
Initialized Variables:
private bool _oddRow = true;
private string[] _labels = new string[3];
private int _defectsTotal = 0;
private int _enhanceTotal = 0;
private int _maintenTotal = 0;
Detail Section (where it's creating the rows for the items):
public void Detail1_Format()
{
if (rpt.Fields.Contains("Custom_163") && rpt.Fields["Custom_163"].Value != null)
{
if ((bool)rpt.Fields["Custom_163"].Value == true)
{
//needs something here to add a count towards this type
return enhanceTotal;
}
else
{
//needs something here to add a count towards this type
return maintenTotal;
}
_defectsTotal++;
_enhanceTotal += enhanceTotal;
_maintenTotal += maintenTotal;
// To Color Every Other Row
if (_oddRow)
{
rpt.Sections["Detail1"].BackColor = System.Drawing.Color.FromArgb(224, 224, 224);
}
else
{
rpt.Sections["Detail1"].BackColor = System.Drawing.Color.White;
}
_oddRow = !_oddRow;
// Converting to String Values
if (rpt.Fields["ItemId"].Value == null) return;
}
}
The problem is specifically with these lines:
if((bool)rpt.Fields["Custom_163"].Value == true)
I've changed this a ton from similar conditions:
if(rpt.Fields["Custom_163"].Value.ToString() == "True")
if(rpt.Fields["Custom_163"].Value == true)
if(rpt.Fields["Custom_163"].Value = true)
if(rpt.Fields["Custom_163"].Value == 1)
if(rpt.Fields["Custom_163"].Value.ToString() == "1")
if (Convert.ToBoolean(rpt.Fields["Custom_163"].Value) == true)
etc...
But they don't seem to work as intended. I want it to filter out the items where this field is true so the rest will execute.
Another thing is returning the values from the if-else statement (not working and I've tried using a separate method below that doesn't work either):
public class findTotals
{
public findTotals()
{
}
public int resultTotals()
{
bool item = (bool)rpt.Fields["Custom_163"].Value;
if ( item == true)
{
int enhanceTotal = 1;
return;
}
else
{
int maintenTotal = 1;
return;
}
}
}
(This whole method needs a ton of work TBH).
Lastly, these lines has to be outside of the second if-else or at least somehow returned to the original if in order to be printed to the report:
_enhanceTotal += enhanceTotal;
_maintenTotal += maintenTotal;
The _defectsTotal gets a total count of all the items and that works fine. These two lines though are supposed to add a count based on the above conditions for each type so then the total amount of Enhancements and total amount of Maintenance Items get posted at the end of the report. It doesn't work, if not a casting/conversion error or the conditions force the array(s) out of bounds, it's some other issue. I've played around the order for all these too.
Possible Solutions?
How to safely convert (if needed) the bool values to int so that number can be used to add to a total?
How can I return the values in the if-else so the scope matches up with the variables? Will I need a method?
Thank you in advance.
You should be able to do a string compare against "True". i.e.
if(string.Compare(rpt.Fields["Custom_163"].Value.ToString(), "True") == 0)
I figured out what the issue was.
So basically...
if (Convert.ToBoolean(rpt.Fields["Custom_163"].Value) == true)
{
enhanceTotal++;
}
else
{
maintenTotal++;
}
_defectsTotal++;
(This is all within another if-statement and a bunch of code not relevant to issue.) This works fine and actually returns the count, after that another hurdle was figuring out the labels so it can print into it's placeholders correctly. The whole findTotals method wasn't needed. I didn't need any more variables than the three listed above to make it work. A reason I kept on getting errors was because I tried to use one variable as a label to print into different places within the report which isn't allowed apparently. Instead of using one label and sticking it everywhere, I made several that were identical and made sure to give them different names, etc.
So instead of X in three different report sections, X was X, Y, Z (all identical but different identifer) and stuck it where needed. Man, it seems to obvious now, well thanks again.

OpenXML 2.0 Gets wrong cell value

I'm having a bit of a problem reading a value from an Excel 2010 worksheet.
On a standard Excel 2010 worksheet I have a cell with the currency format with two decimal places and the value 1270,14 €.
When I read this value on OpenXML 2.0 (C# code) I get 1270.1400000000001 instead of the original 1270.14.
The same happens with other values on any cell with the same formating.
Get value from cell OpenXML code:
private string GetCellValue(string column, int row)
{
column = column.ToUpper();
var targetCell = cells.Where(p => p.CellReference == (column + row)).SingleOrDefault();
var value = String.Empty;
if (targetCell.DataType != null && targetCell.DataType.Value == CellValues.SharedString)
{
var index = int.Parse(targetCell.CellValue.Text);
value = cellValues[index].InnerText.Trim();
}
else
{
if (targetCell.CellValue != null)
{
value = targetCell.CellValue.Text.Trim();
}
else
{
value = null;
}
}
return value;
}
The specific value passes over the 'if' DataType is not null condition and retrieves the value with the line:
value = targetCell.CellValue.Text.Trim();
How can this be fixed ?
Why is this error even possible ?
As the number is stored as double in Excel, you can first parse the string as double, then convert back to string to get the value for display:
string s = Convert.ToDouble(value).ToString();
Let Microsoft handle its own problem.
Though it may not fit your application, the easiest fix would be simply rounding off the value you get back and assigning it or returning it where applicable. somevarhere=Math.Round(Convert.ToDouble(value), 2)

Winform DatagridView Numeric Column Sorting

I am using just a simple DataGridView to hold a bunch of data (Funny that).
I have decimals in a particular column. But when it comes to ordering by that decimal column, it orders it incorrectly. For example :
Starting order might be :
0.56
3.45
500.89
20078.90
1.56
100.29
2.39
The ending order would be :
0.56
100.29
1.56
20078.90
2.39
3.45
500.89
As you can see, it orders it starting from the first number. And then orders it in this way.
I thought possibly I could set the column to a different "ColumnType" and that may automatically do it. But there is no "Numeric" or "Decimal" column types.
I was on MSDN looking up the issue, and I could find the "sort" method that I can use on the DataGridView. But the explanation was a bit over my head, and the examples didn't use numbers, only text so I couldnt see how I was supposed to switch things up.
Any help would be much appreciated.
You can solve this by adding a handler for the SortCompare event on the DataGridView with the following code:
private void dataGridView1_SortCompare(object sender, DataGridViewSortCompareEventArgs e)
{
if (e.Column.Index == 0)
{
if (double.Parse(e.CellValue1.ToString()) > double.Parse(e.CellValue2.ToString()))
{
e.SortResult = 1;
}
else if (double.Parse(e.CellValue1.ToString()) < double.Parse(e.CellValue2.ToString()))
{
e.SortResult = -1;
}
else
{
e.SortResult = 0;
}
e.Handled = true;
}
}
From MSDN there is this description of the SortResult values:
Less than zero if the first cell will
be sorted before the second cell; zero
if the first cell and second cell have
equivalent values; greater than zero
if the second cell will be sorted
before the first cell.
Note that in my test bed the only numeric column was the first (with index 0) so that is why I have the check on the column index.
Also, depending on your needs and data you may want to refine my code - for example, my code will throw an exception if for some reason you have non numeric data in your column.
You have probably seen it, but here is a link to the MSDN page on customising the DataGridView sorting. As you say, they only deal with text.
I had the same problem. I tried using the event handler as David Hall mentioned. I used the ValueType property when defining the DataGridView. It now sorts as doubles, no custom event handler code needed
dataGridView1.Columns[int index].ValueType = typeof(double);
You can also format the column using
dataGridView2.Columns[int index].DefaultCellStyle.Format = string format;
Numeric types have a built in CompareTo function, which can be used as the SortResult of the SortCompare event.
private void dataGridView_SortCompare(object sender, DataGridViewSortCompareEventArgs e)
{
if (e.Column.Index == 0)
{
e.SortResult = int.Parse(e.CellValue1.ToString()).CompareTo(int.Parse(e.CellValue2.ToString()));
e.Handled = true;
}
}
This is of course assuming you know the type that was put into the DataGridView to begin with.
Your database column type should be int or double or float, not varchar or something....
So you have to change your value-type in the database...
You no need to write any code or something it directly sort when you click on the column Header...
It is sorting it by character. You need to make the column type float so it knows what comparison operator to apply.
(That is you need to make the column type in your dataset float, I believe that will work.)
Your problem is the datagridview is sorting by string. Try casting the string to float when you copy that cell into the datagrid.

C# help to set a Row Css class of a Grid View

I need to alternate row colors in a grid, but not on every other row. I have a variable _AddDate that I can check on the GridRowBound event. If it hasn't changed, I want to apply one css class and if it has I want to apply a different class. The code I have does almost exactly what I want but I am setting the class on the row when the value changes and each concurrent row that should be the same class is having the incorrect class applied. Its definitely something wrong with my method. Can anyone point me in the right direction? Also is there a name for these types of functions. I have to do things like this from time to time and they can be tricky to figure out the correct algorithm. Here is what I have.
private void GridRowBound(object sender, GridViewRowEventArgs e)
{
if(e.Row.RowType == DataControlRowType.DataRow)
{
e.Row.CssClass = SetRowColor();
}
}
private DateTime _dateToSwitch;
private string SetRowColor()
{
var tmpDate = _AddDate;
var doSwitch = (tmpDate == _dateToSwitch);
if (!doSwitch)
{
_dateToSwitch = tmpDate;
return "commentRow";
}
return "altCommentRow";
}
I have another function that correctly sets _AddDate to the appropriate value so it is always current when it is evaluated.
Any help is appreciated. Happy Friday!
Cheers,
~ck in San Diego
I can't think of a more elegant way of doing this (at the moment) aside from this:
private DateTime _previousRowDateTime;
private string[] _commentRowClasses = {"commentRow", "altCommentRow"};
private int _commentRowClassesIndex = 0;
private string SetRowColor()
{
if( _AddDate != _previousRowDateTime )
{
_commentRowClassesIndex = ( _commentRowClassesIndex + 1 ) % 2;
_previousRowDateTime = _AddDate;
}
return _commentRowClasses[_commentRowClassesIndex];
}
What your code is saying:
If the last date stored is NOT equal to the "_AddDate" variable
then SET it to that and return that this is a "commentRow".
If the last date stored IS equal to the "_AddDate" variable
then simply return "altCommentRow".
So, on 2 consecutive rows with the same date where _AddDate has NOT changed, the first one will get the "commentRow" style and the second one, will get "altCommentRow".
If your goal is to rotate coloring so that all consecutive rows with the same date are one color, then, when a new date is reached, switch to the next color, you could try something like this:
private bool _AltFlag;
private string _PreviousDate;
private string SetRowColor()
{
if (_AddDate != _PreviousDate)
{
_AltFlag = !_AltFlag;
}
return _AltFlag ? "altCommentRow" : "commentRow";
}
Essentially, we set up a bool to tell us which of the classes we're currently using. If our current date is not the same as the previous date, then flip the flag and return the new class. If it IS the same, we keep the same flag and return the class.

Categories