My function always reads date wrong by exactly 1 year - excel

I have dates stored in strings, sometimes in the form of "Nov-2018" and sometimes in the form of "11/1/2018". I want to change them universally to "Nov 2018". Just month and date.
I wrote the following function.
Function FnDateExtract(fnFile, fnTab, fnRow, fnColumn)
Dim RawExtract As String
With Workbooks(fnFile).Worksheets(fnTab)
RawExtract = .Cells(fnRow, fnColumn).Text
FnDateExtract = Format(RawExtract, "mmm yyyy")
End With
End Function
I keep getting into this problem where the function would read strings '11/1/2018' and 'Nov-2018' and return the result 'Nov 2019'. I have no clue why 2019 (but the problem only appeared from January 1, 2019 onward. How strange.)
Question:
As a temporary solution, is there a way to return the year minus 1? Something akin to FnDateExtract = Format(RawExtract, "mmm yyyy - 1")?
If you could help me solve the problem why the function always misreads strings by exactly 1 year, that'd also help a lot. Thanks!
Edit
RonRosenfeld below suggested that the function would only extract the current year. I did a few tests and it seems like it would also extract 2017 as 2019. Not sure why that is. Still looking for a solution.

Use the IsDate check combined with the CDate conversion to achieve the correct string representing a date.
Option Explicit
Function FnDateExtract(fnFile As String, fnTab As String, fnRow As Long, fnColumn As Variant)
With Workbooks(fnFile).Worksheets(fnTab)
If IsDate(.Cells(fnRow, fnColumn).Text) Then
FnDateExtract = Format(CDate(.Cells(fnRow, fnColumn).Text), "mmm yyyy")
End If
End With
End Function

Related

How do I convert Excel General Text to Date

So my problem is I have a Date that is defined as General in Excel as this "10 JUL 2021 10:30" I want to make it Excel Date.
kindly have a look at my picture for detailed understanding.
Need any kind of solution to automate this VBA or any formula,
Thanks in advance
If you need VBA to extract Date, please use the next function:
Function TextToDateX(txt As String) As Date
Dim arrD, arrM
arrM = Split("JAN,FEB,MAR,APR,MAY,JUN,JUL,AUG,SEP,OCT,NOV,DEC", ",")
arrD = Split(txt, " ")
TextToDateX = DateSerial(CLng(arrD(2)), Application.match(arrD(1), arrM, 0), CLng(arrD(0))) + CVDate(arrD(3))
End Function
It can be tested as:
Sub testTextToDateX()
Dim txt As String, d As Date
txt = "10 JUL 2021 10:30"
d = TextToDateX(txt)
Debug.Print Format(d, "dd/mm/yyyy hh:mm")
End Sub
DateValue is localization related. Microsoft states: "DateValue recognizes the order for month, day, and year according to the Short Date format that you specified for your system". So, it may work or not.
In my case it doesn't...
I just tried using the DateValue() worksheet function, and every worked fine (normal Excel formula, no VBA needed):
=DATEVALUE("07 JUL 2021 10:30")
One thing that might be interesting for you: there exists a cell formatting (dd mmm yyyy hh:mm), which you seem to be using. (When you apply this on any cell, the value inside gets automatically formatted into that kind of date format.)
Edit, based on comment:
In case there's a comma in your date string, remove it first and then apply the DateValue() function:
=DATEVALUE(SUBSTITUTE("07 JUL 2021, 10:30",",",""))

Calculations in Excel spreadsheet using pre-1900 date

Microsoft Excel does not recognise pre-1900 dates and there is plenty of information online which documents this, including the question here.
The best work around (which many other posts link to) seems to be at ExcelUser
However, although the work around gets Excel to recognise a pre-1900 date as a date, it still does not allow it to be used in calculations e.g. when wanting to calculate the number of years since a pre-1900 date.
My question is whether the work around described at ExcelUser can be modified to allow the result to be used in a calculation.
To put things simply, for example, I want to calculate in Excel the number of years since 1/4/1756 - is this possible?
Or does another solution have to be adopted? Perhaps there are plug-ins which address this problem?
First of all I highly recommend to use the ISO 8601 format yyyy-mm-dd for dates because even if you only have strings and no real numeric dates this is properly sortable and the only date format that is defined clearly and cannot be misinterpret like 02/03/2021 where no one can ever say if it is mm/dd/yyyy or dd/mm/yyyy because both actually exist.
Since old dates cannot be real numeric dates but only entered as strings (looking like a date) that means misinterpretation needs to be avioded or you get wrong results. Therefore a date format that cannot be misinterpret is a clear advantage.
Second there is more than one way to calculate "how many years since the birth of Mr. X": For example lets take the birthday of Maryam Mirzakhani 1977-05-12 compared to the date today 2021-04-15. Today she would not have had birthday yet this year and therefore she would be 43 years old. But this year she would have turned 44 years (2021 - 1977 = 44). So the question needs to be asked more precisely. Either "how old would Mr. X be today?" or "how old would Mr. X be this year". The calculation for that would be different.
So let's start and assume the following data. We already know the fact that Excel cannot calculate with dates before 1900. You can see that if we enter pre-1900 dates that they are formatted as string (red dates) and post-1900 dates get formatted as numeric dates (green dates).
Image 1: #WERT! means #VALUE! (sorry for the German screenshot).
Also in column D where the formula =DATEDIF($B2,TODAY(),"y") was used the string dates cannot be calculated with. But since VBA can actually handle pre-1900 dates we can write our own UDF (user defined function) for that. Since as I explained above there is 2 different methods to calculate there is 2 different functions:
OldDateDiff(Date1, Date2, Interval) called like =OldDateDiff($B2,TODAY(),"yyyy")
OldDateAge(Date1, Date2) called like =OldDateAge($B2,TODAY())
Option Explicit
Public Function OldDateDiff(ByVal Date1 As Variant, ByVal Date2 As Variant, ByVal Interval As String) As Long
Dim RetVal As Long 'variable for the value we want to return
Dim localDate1 As Date
If VarType(Date1) = vbDate Or VarType(Date1) = vbDouble Then 'check if Date1 is numeric
localDate1 = CDate(Date1) 'if numeric take it
ElseIf VarType(Date1) = vbString Then 'check if Date1 is a string
localDate1 = ISO8601StringToDate(Date1) 'if it is a string convert it to numeric
Else 'neither string nor numeric throw an error
RetVal = CVErr(xlErrValue)
Exit Function
End If
Dim localDate2 As Date 'same as for Date1 but with Date2
If VarType(Date2) = vbDate Or VarType(Date2) = vbDouble Then
localDate2 = CDate(Date2)
ElseIf VarType(Date2) = vbString Then
localDate2 = ISO8601StringToDate(Date2)
Else
RetVal = CVErr(xlErrValue)
Exit Function
End If
If localDate1 <> 0 And localDate2 <> 0 Then 'make sure both dates were filled with values
RetVal = DateDiff(Interval, localDate1, localDate2) 'calculate the difference between dates with the desired interaval eg yyyy for years
End If
OldDateDiff = RetVal 'return the difference as result of the function
End Function
Public Function OldDateAge(ByVal Date1 As Variant, ByVal Date2 As Variant) As Long
Dim RetVal As Long 'variable for the value we want to return
Dim localDate1 As Date
If VarType(Date1) = vbDate Or VarType(Date1) = vbDouble Then 'check if Date1 is numeric
localDate1 = CDate(Date1) 'if numeric take it
ElseIf VarType(Date1) = vbString Then 'check if Date1 is a string
localDate1 = ISO8601StringToDate(Date1) 'if it is a string convert it to numeric
Else 'neither string nor numeric throw an error
RetVal = CVErr(xlErrValue)
Exit Function
End If
Dim localDate2 As Date 'same as for Date1 but with Date2
If VarType(Date2) = vbDate Or VarType(Date2) = vbDouble Then
localDate2 = CDate(Date2)
ElseIf VarType(Date2) = vbString Then
localDate2 = ISO8601StringToDate(Date2)
Else
RetVal = CVErr(xlErrValue)
Exit Function
End If
If localDate1 <> 0 And localDate2 <> 0 Then 'make sure both dates were filled with values
RetVal = WorksheetFunction.RoundDown((localDate2 - localDate1) / 365, 0)
'subtract date1 from date2 and divide by 365 to get years, then round down to full years to respect the birthday date.
End If
OldDateAge = RetVal 'return the age as result of the function
End Function
' convert yyyy-mm-dd string into numeric date
Private Function ISO8601StringToDate(ByVal ISO8601String As String) As Date
Dim ISO8601Split() As String
ISO8601Split = Split(ISO8601String, "-") 'split input yyyy-mm-dd by dashes into an array with 3 parts
ISO8601StringToDate = DateSerial(ISO8601Split(0), ISO8601Split(1), ISO8601Split(2)) 'DateSerial returns a real numeric date
' ≙yyyy ≙mm ≙dd
End Function
Note that here column B contains 2 different kind of data. Strings (that look like a date) and real numeric dates. If you sort them, all the numeric dates will sort before the string dates (which is probably not what you want). So if you want this to be sortable by birthday column make sure you turn all dates into strings. This can be done by adding an apostrophe ' infront of every date. This will not display but ensure the entered date is considered to be a string.
If your date is in an unambiguous format (eg ISO or a format corresponding to your Windows Regional Settings, or a real date if after 1900), you can use VBA which will recognize early dates.
Function Age(dt As Date)
Age = DateDiff("yyyy", dt, Date)
End Function
You should be aware that, because of how the function calculates years differences, depending on what you want exactly for a result, you may need to adjust the answer if the birthdate is before/after today's date.
In other words, if the day of the year of the birthdate is after the day of the year of Today, you may need to subtract 1 from the result.
But this should get you started.
There's a much easier way than the accepted answer. Simply convert your dates to Unix time:
Function nUnixTime(dTimestamp As Date) As LongLong
' Return given VB date converted to a Unix timestamp.
Const nSecondsPerDay As Long = 86400 ' 24 * 60 * 60
nUnixTime = Int(CDbl(CDate(dTimestamp) - CDate("1/1/1970"))) * nSecondsPerDay
End Function
Unix time is the number of seconds since Jan. 1, 1970, with times before that date being negative. So if you convert your dates to Unix time, you can just subtract them and divide the result by 86,400 to have the difference in days, or by 31,557,600 for years (31,557,600 = 60 * 60 * 24 * 365.25).
Example results of the above VB function called from Excel:
Column A
Column B formula
Column B value
2/2/2022
=nUnixTime(A1)
1,643,760,000
1/1/1970
=nUnixTime(A2)
0
12/14/1901
=nUnixTime(A3)
-2,147,472,000
12/13/1901
=nUnixTime(A4)
-2,147,558,400
1/1/1900
=nUnixTime(A5)
-2,208,988,800
1/1/1800
=nUnixTime(A6)
-5,364,662,400
1/1/100
=nUnixTime(A7)
-59,011,459,200
The reason I included the two dates in 1901 is because their magnitudes in Unix time are just smaller than and just larger than the largest magnitude of a signed 32-bit integer, i.e., a Long in VBA. If the output of the above function were a Long, then values for dates before Dec. 14, 1901 would be the error #Value!. That is the reason the output of the function is defined as LongLong, which is VBA's signed 64-bit integer.

Unexpected CDate() behavior

While going through some old code without error handling I stumbled upon unexpected CDate() function behavior.
Sub Test()
Dim userinput as Variant
userinput = Application.Inputbox("Enter a date")
'userinput will be a Variant/String with the inputs below
Debug.Print CDate(userinput)
End Sub
Input: "27/8", "27/08", "27-8", "27-08", "27.8", "27.08"
Output: 27.08.2019 ' for all of the above
Input: "27.8.", "27.08."
Output: 04.10.1900, 31.05.1907
I was either expecting Error 13: Type-mismatch, or 27.08.1900 or 27.08.2019 as output.
What is happening with the latter two inputs? I can't wrap my head around it.
Additional input: "26.8." -> output: 24.09.1900
Input: "26.08." -> output: 20.02.1907
Regional setting is German (Germany) (Deutsch (Deutschland))
Date format is DD.MM.YYYY
Edit:
The complete userinput code looks like this:
Sub Test()
Dim userinput As Variant
Dim cancelBool As Boolean
Do While Not ((IsDate(userinput) And Not userinput = vbNullString) Or cancelBool)
userinput = Application.InputBox("Enter a date")
If userinput = False Then cancelBool = True
'the following line was inspired by Plutian
If Not IsDate(userinput) And IsNumeric(userinput) Then userinput = userinput & Year(Now())
Loop
If Not cancelBool Then Debug.Print CDate(userinput)
End Sub
This doesn't appear to be a CDate behaviour problem, just a text-to-number conversion in general problem.
I have no citation, but from observation: When attempting to convert text to a numeric value, Excel will check to see if the text is an obvious date. If it's not, it will then strip out any thousand's separator - also local currency symbols, and other things, no doubt - to reduce the text to a number where possible.
So, on my English locale set up:
"27.8" ("27,8" on yours) is a recognisable decimal value
= 27 days and 8/10ths past 31/12/1899 = 26/01/1900 19:12:00
"27,8" ("27.8" on yours) is a not recognisable decimal value, nor is it a recognisable date
so it becomes "278" as it strips out the 000 separators (commas on my set up, periods on yours)
278 days past 31/12/1899 = 27/09/1900
As pointed out by #Nacorid however, CDATE treats this a little differently (to standard conversion) and attempts to resolve this to a date - being 27 Aug (current year).
"27.8." ("27,8," on yours) throws an error, as is a not a recognisable date and due to the two decimal pointers an Error is produced.
"27,8," ("27.8." on yours) is not a recognisable date, and Excel assumes the 000 separators need removing so converts this to 278
=278 days past 31/12/1899 = 04/10/1900
So, the TL;DR is that "27.8." - while acceptable in German as a date - is not acceptable to Excel and you'll need to trap these and add an assumed year or similar to get around it.
Alternatively, consider adding a calendar pop-up form that forces the user to provide day, month and year.
Your issue here is not with the cdate function as it is giving the expected behaviour when the input is formatted as a string caused by the German custom of writing a date without the year as dd.mm..
The issue is how to handle this input and still get the expected result, this can be achieved with the following code:
If IsNumeric(userinput) Then
Else
userinput = CDate(userinput & Year(Now()))
End If
Which forcefully inserts the current year in the user input when the variant is not recognised as numeric, which is caused by ending on a .. This works since dates in excel are always stored as a numeric value. Adding the year to the output converts it back to a numeric value which cdate can handle, since excel will now recognize the preferred separator as indeed a separator and handles it as a date as expected.
To me this would be a preferred alternative to forcing the user to amend their input. However wouldn't work if the required date is not in the current year, and might cause issues around new years. Alternatively you could replace the year snippet with a plain "0" or any year of your choice.

VBA dates collected correctly but output wrongly

I grab dates from one spreadsheet and output them onto another spreadsheet. After grabbing the date, when I debug.print it is in the correct format. When I output the date, debug.print also displays the correct format. However, the format on the spreadsheet the value has just been sent to, doesnt show the correct format.
I am using:
Sheets(SheetName).Cells(RowNum, ColNum).Value = Data
Sheets(SheetName).Cells(RowNum, ColNum).NumberFormat = "dd/mm/yyyy"
after I have pasted the value, but the months and days are still switched the wrong way.
Is there something I am doing wrong?? If I right click the cell it thinks it's date is dd/mm/yyyy but instead of 4th Sept it is showing 9th April.
This might be some trouble with localization:
Try using NumberFormatLocal, if DanielCooks tip didn't help ;)
edit: erlier it was statet by mister Cook, to check if the given data is correct.
edit:
With my german version I have quite some trouble to use / as the seperator, that is why i tryied with this code .NumberFormat ="dd-mm-yyyy;#" - works fine; I can switch days and month as I like.
edit:
With .NumberFormatLocal = "TT/MM/JJJJ" I have to use the german shorts for day, month and year, but now I can use / as the seperator.
You should experiment a litte bit with some formats strings ;)
Sorry to resurrect an old post, however I had a problem with VBA outputting a valid date as US style with the actual date changed for example 1st May changed to 5th Jan. I came upon this post but I didn't find the answer I needed here and now that I have worked it out I thought I would share it:
The key is not to define the variable storing the date as a "date" but as a "double", e.g.
Dim ReportDate As Double
ReportDate = Date
Range("E6").Value = ReportDate
This works as it outputs the numeric "date value" which excel then formats locally e.g. 41644 gets formatted as "05/01/14" using UK format or "01/05/14" using US format.
Hope this proves useful to other people (probably me when I forget how I solved it in six months time).
In the end I had to format the cell as "TEXT" to keep the correct format
(1) You need to define the variable to "Date" type to read the input, then set the date format before assigning it to the date variable.
(2) You also need to format the date output to make it work !
'**Read the date input****
Dim date1 as Date
Range("B2").NumberFormatLocal = "dd/mm/yyyy"
date1 = Range("B2").Value
'**Output the date****
Range("E2").Value = date1
Range("E2").NumberFormatLocal = "dd/mm/yyyy"

How to convert and compare a date string to a date in Excel

= "7/29/2011 12:58:00 PM" > NOW()
I'd like this expression to return FALSE and yet it returns TRUE.
I know I can break apart my datetime into a date and a time and add them together as follows:
= DateValue("7/29/2011") + TimeValue("12:58:00 PM") > NOW()
But, this seems inelegant to me. I want a simple function or approach that looks nice and I feel certain that it's out there but I just can't find it.
I also know there is a VBA function called CDate which can typecast the string into a datetime and that would be perfect. But, I don't see how to call a VBA function in an excel cell.
Multiply the string by one and the comparison function will work:
= 1*"7/29/2011 12:58:00 PM" > NOW()
The answer to your question is tightly related to #Jean-François's comment: Why is the date being interpreted by Excel as a Text and not by a date?
Once you find it out, you'll be able to do the comparison.
If that's because the string is being retrieved as a text, you can simply multiply it by one and the comparison function will work, then. But it applies only in case the string format is a valid date/time format in your regional settings.
You could wrap the VBA call in a custom function:
Function ReturnDate(ByVal datestr As String) As Date
ReturnDate = CDate(datestr)
End Function
which you can use just like a formula in your sheet.
I'm upgrading the following from a comment to an answer:
Unless you have a very specific reason to do so (and right now I can't think of any), dates (and other values) really shouldn't be "hard-coded" in cells as strings like you show. Hard-coding the string like that makes it invisible and inflexible. The user will just see TRUE or FALSE with no indication of what this means.
I would just put your date 7/29/2011 12:58:00 PM in a cell on its own e.g. A1, and set the cell's format to some date format. Then you can say = A1 > NOW().
Contrary to #jonsca's and #Tiago Cardoso's answers, this answer doesn't address your specific question, but then again, what you are asking seems like really bad practice to me!
The simplest way to do this is to make a VBA function that uses CDATE and return your comparison. Then, call the function from an excel cell.
The VBA Function
Public Function compareDate(ByVal inputDate As String) As Boolean
compareDate = CDate(inputDate) > Now()
End Function
Then in your spreadsheet, just do
=compareDate("YOUR DATE")
The function will return "FALSE" if it is older and "TRUE" if it is newer than Now()

Resources