CodeSOD: Intergral to Dating
Date representations are one of those long-term problems for programmers, which all ties into the problem that "dates are hard". Nowadays, we have all these fancy date-time types that worry about things like time zones and leap years, and all of that stuff for us. Pretty much everything, at some level, relies on the Unix Epoch. But there is a time before that was the standard.
In the mainframe era, not all the numeric representations worked the same way that we're used to, and it was common to simply define the number of digits you wanted to store. So, if you wanted to store a date, you could define an 8-digit field, and store the date as 20201012: October 10th, 2020.
This is a pretty great date format, for that era. Relatively compact (and yes, the whole Y2K thing means that you might have defined that as a six digit field), inherently sortable, and it's not too bad to slice it back up into date parts, when you need it. And like anything else which was a good idea a long time ago, you still see it lurking around today. Which does become a problem when you inherit code written by people who didn't understand why things worked that way.
Virginia N inherited some C# code which meets that criteria. And the awkward date handling isn't even the WTF. There's a lot to unpack in this particular sample, so let's start with... the unpack function.
public static DateTime UnpackDateC(DateTime dateI, long sDate){ if (sDate.ToString().Length != 8) return dateI; try { return new DateTime(Convert.ToInt16(sDate.ToString().Substring(0, 4)), Convert.ToInt16(sDate.ToString().Substring(4, 2)), Convert.ToInt16(sDate.ToString().Substring(6, 2))); } catch { return dateI; }}
sDate is our integer date: 20201012. Instead of converting it to a string once, and then validating and slicing it, we call ToString four times. We also reconvert each date part back into an integer so we can pass them to DateTime, and of course, DateTime.ParseExact is just sitting there in the documentation, shaking its head at all of this.
The really weird choice to me, though, is that we pass in dateI, which appears to be the "fallback" date value. That... worries me.
Well, let's take a peek deep in the body of a method called GetMC, because that's where this unpack function is called.
while (oDataReader.Read()){ //... DataRow oRow = oDataTable.NewRow(); if (oDataReader["DEB"] != DBNull.Value) { DateTime dt = DateTime.Today; dt = UnpackDateC(dt, Convert.ToInt64(oDataReader["DEB"])); } else { oRow["DEB"] = DBNull.Value; } //...}
It's hard to know for absolute certain, based on the code provided, but I don't think UnpackDateC is actually doing anything. We can see that the default dateI value is DateTime.Today. So perhaps the desired behavior is that every invalid/unknown date is today? Seems problematic, but maybe that jives with the requriements.
But note the logic. If the database value is null, we store a null in oRow["DEB"]- our output data. If it isn't null, we unpack the date and store it in... dt. Also, if you trace the type conversions, we convert an integer in the database into an integer in our program (which it already would have been) so that we can convert that integer into a string so that we can split the string and convert each portion into integers so we can convert it into a date.
How do I know that the field is an integer in the database? Well, I don't know for sure, but let's look at the query which drives that loop.
public static void GetMC(string sConnectionString, ref DataTable dtToReturn, string sOrgafi, string valid, string exe, int iOperateur, string sColTri, bool Asc, bool DBLink, string alias) // iOperateur 0, 1, 2{sSql = " select * from (select ENTLCOD as ENTAFI, MARKYEAR as EXE, nvl(to_char(MARKNUM),'')||'-'||nvl(MARKNUMLOT,'') as COD, to_char(MARKNUM) as NUM, MARKOBJ1 as COM, MARKSTARTDATE as DEB, MARKENDDATE as FIN, MARKNUMLOT as NUMLOT, MARKVALIDDATE, TIERNUM as FOUR, MARTBASTIT as TYP " + " from SOMEEXTERNALVIEW" + (DBLink ? alias + " " : " ") + " WHERE 1=1"; if (valid != null && valid.Length > 0) sSql += " and (MARKVALIDDATE >= " + valid + " or MARKVALIDDATE=0 or MARKVALIDDATE is null)"; if (exe != null && exe.Length > 0) sSql += " and TRIM( MARKYEAR ) ='" + exe.Trim() + "' "; sSql += " ) where 1=1"; //...
We can see that MARKSTARTDATE is the database field we call DEB. We can also see some conditional string concatenation to build our query, so hello possible SQL injection attacks. Now, I don't know that MARKSTARTDATE is an integer, but I can see that a similar field, MARKVALIDDATE is. Note the lack of quotes in the query string: "...(MARKVALIDDATE >= " + valid + " or MARKVALIDDATE=0 or MARKVALIDDATE is null)"
So MARKVALIDDATE is numeric in the database, which is great because the variable valid is passed in as a string, so we're just all over the place with types.
The structure of this query also adds on an extra layer of unnecessary complexity, as for some reason, we wrap the actual query up as a subquery, but the outer query is just SELECT * FROM (subquery) WHERE 1=1, so there is literally no reason to do that.
To finish this off, let's look at where GetMC is actually invoked, a method called CallWSM.
private void CallWSM(ref DataTable oDataTable, string sCode, string sNom, string sFourn, int iOperateur) // iOperateur 0, 1, 2{ try { m_bError = false; string sColTri = m_Grid_SortRequest.FieldName; SortOperator oDirection = m_Grid_SortRequest.SortDirection; m_sAnnee = ctlRecherche.GetValueFilterItem(1); string svalid = ""; string sdt = ctlRecherche.GetValueDateItem(0); if (sdt.Length > 0) { DateTime dtvalid = DateTime.Parse(sdt); long ldt = dtvalid.Year * 10000 + dtvalid.Month * 100 + dtvalid.Day; svalid = ldt.ToString(); } m_AppLogic.GetMC(sColTri, oDirection, m_sAdresseWS, ref oDataTable, svalid, m_sAnnee, iOperateur, PageCurrent, m_PageSize); } catch (WebException ex) { if (ex.Status == WebExceptionStatus.Timeout) { frmMessageBox.Show(ML.GetLibelle(4137), CONST.AppTITLE, MessageBoxButtons.OK, MessageBoxIcon.Error); m_bError = true; } else { frmMessageBox.Show(ex.Message); m_bError = true; } } catch (Exception ex) { frmMessageBox.Show(ex.Message); m_bError = true; }}
Now, I'm reading between the lines a bit, and maybe making some assumptions that I shouldn't be, but this method is called CallWSM, and one of the parameters we pass to GetMC is stored in a variable called m_sAdresseWS, and GetMC can apparently throw a WebException.
Are... are we building a query and then passing it off to a web service to execute? And then wrapping the response in a data reader? Because that would be terrible. But if we're not, does that mean that we're also calling a web service in some of the code Virginia didn't supply? Query the DB and call the web service in the same method? Or are we catching an exception that just could never happen, and all the WS stuff has nothing to do with web services?
Any one of those options would be a WTF.
Virginia adds, "I had the job to make a small change in the call. ...I'm used to a good amount of Daily WTF-erry in our code." After reading through the code though, Virginia had some second thoughts about changing the code. "At this point I decided not the change anything, because it hurts my head."
You and me both.
$WORD [Advertisement] BuildMaster allows you to create a self-service release management platform that allows different teams to manage their applications. Explore how!