I need to turn content in a spreadsheet column from text to a date.
The cell format is text and the inputters were instructed to input a date as "ddmmyyyy".
Accidents happened and I found some content that would not parse as a date, including entries like "Unknown".
So I used a variable declared as a date and wrote an error handler to deal with content that would not parse.
Now for the bit I cannot work out.
If the date was 3rd March 2000 and someone input that as "03332000" that will not parse because "33" cannot be a month or a day; it is caught by the error handler as I wanted.
But if it was input as "03132000" I can't think of a way of preventing VBA converting that to a valid date as "13/03/2000".
Declaring a format for the date variable will not prevent VBA parsing the date.
I can write something that tests number range of the day and month part of the string but that is extra lines of code and I was hoping to do it just by the error handler.
I'd approach it a little differently and let Excel do the work.
Public Function ValidateDate(ByVal strDate As String) As Boolean
Dim intDay As Integer, intMonth As Integer, intYear As Integer, dtDate As Date
ValidateDate = True
On Error GoTo IsInValid
If Len(strDate) <> 8 Then GoTo IsInValid
If Not IsNumeric(strDate) Then GoTo IsInValid
intDay = Left(strDate, 2)
intMonth = Mid(strDate, 3, 2)
intYear = Right(strDate, 4)
dtDate = DateSerial(intYear, intMonth, intDay)
If DatePart("d", dtDate) <> intDay Then GoTo IsInValid
If DatePart("m", dtDate) <> intMonth Then GoTo IsInValid
If DatePart("yyyy", dtDate) <> intYear Then GoTo IsInValid
Exit Function
IsInValid:
ValidateDate = False
End Function
... this will ensure that anything related to leap years etc. will still work correctly and it will ensure that all entries are validated correctly.
If you place:
03332000
in cell A1 and run:
Sub CheckDate()
Dim s As String, d As Date
s = Range("A1").Text
d = DateSerial(CInt(Right(s, 4)), CInt(Mid(s, 3, 2)), CInt(Left(s, 2)))
MsgBox s & vbCrLf & d
End Sub
You will get:
So even though a valid month can only be in the range [1-12], Excel is trying to "help" you by interpreting the 33 as a projection of future date. For example, if the month was entered as 13, Excel will treat it as December of the following year!
You can't rely on error-handling for this. You need checks like:
Sub CheckDate2()
Dim s As String, d As Date
Dim dd As Integer, mm As Integer, yr As Integer
s = Range("A1").Text
yr = CInt(Right(s, 4))
mm = CInt(Mid(s, 3, 2))
dd = CInt(Left(s, 2))
If yr = 0 Or yr < 1900 Then
MsgBox "year is bad"
Exit Sub
End If
If dd = o Or dd > 31 Then
MsgBox "day is bad"
Exit Sub
End If
If mm = 0 Or mm > 12 Then
MsgBox "month is bad"
Exit Sub
End If
d = DateSerial(yr, mm, dd)
MsgBox s & vbCrLf & d
End Sub
You can also do other checks like looking at the length of the field, etc.
Related
I am trying to convert the date format of my cells as the csv format they are delivered in shows a date but excel doesn't recognize it as a date (it shows "Standard" as format and the dates are aligned on the left, hence not dates in excel).
Dim lr11 As Integer
Dim dates11 As Date
lr11 = WS1.Cells(WS1.Rows.Count, "C").End(xlUp).row
For dates11 = 2 To lr11
WS1.Cells(dates11, 3).Value = CDate(Cells(dates11, 3).Value)
Next dates11
The above code sometimes works in a Test Sub() but when used in my main Sub, I always get "12:00:00 AM" in all cells instead of dates.
What am I doing wrong?
Thanks!
If you have strings that look like dates in the format DD.MM.YYYY you can split them and create a numeric date using DateSerial like below:
Option Explicit
Public Function ConvertStringDDMMYYYYtoDate(ByVal InputString As String) As Date
Dim RetVal As Date
Dim Parts() As String
Parts = Split(InputString, ".")
If UBound(Parts) = 2 Then
RetVal = DateSerial(Parts(2), Parts(1), Parts(0))
If Not Format$(RetVal, "DD.MM.YYYY") = InputString Then
MsgBox "Input String is not a real date", vbCritical
RetVal = 0
End If
End If
ConvertStringDDMMYYYYtoDate = RetVal
End Function
Then use it like
For dates11 = 2 To lr11
WS1.Cells(dates11, 3).Value = ConvertStringDDMMYYYYtoDate(WS1.Cells(dates11, 3).Value)
WS1.Cells(dates11, 3).NumberFormat = "DD.MM.YYYY" ' format it however you like it to look like
Next dates11
I slightly amended some code I found online.
Purpose:
Click on a 'Print with Dates' button, then enter start date, and have Excel automatically generate/print a months worth of timesheets (much better than the previous spreadsheet having 6 weeks of pages to print, and you had to edit every date manually).
Issues:
It prints weekends, which wastes paper. Is there a way it can refer to a list of dates (weekends, public holidays), and not generate those for printing?
You'll see the date format is m/d/yyyy in the code, which strangely prints as dd/mm/yyyy (which is what I wanted). When the code was dd/mm/yyyy it was printing correctly (20/03/2019), but if it goes to the following month it was switching to American format m/d/yyyy (04/20/2019). I know it doesn't seem to make sense, but having it as m/d/yyyy actually prints as dd/mm/yyyy across any start/end dates. I'd like to know why, and also have dd/mm/yyyy in the code correctly printing across any date range.
CODE:
Sub PrintSheet()
Dim s As String
Dim d As Date
s = InputBox(Prompt:="Please enter the start date")
If Not IsDate(s) Then
MsgBox "Incorrect date", vbExclamation
Exit Sub
End If
For d = CDate(s) To DateAdd("m", 1, CDate(s)) - 1
Range("F2").Value = Format(d, "dddd")
Range("I2").Value = "" & Format(d, "m/d/yyyy")
ActiveSheet.PrintOut
Next d
End Sub
Cheers in advance :)
You can use the weekday function. It returns a number from 1 to 7. You can filter for 1-5 only for weekdays.
Sub PrintSheet()
Dim s As String
Dim d As Date
s = InputBox(Prompt:="Please enter the start date")
If Not IsDate(s) Then
MsgBox "Incorrect date", vbExclamation
Exit Sub
End If
For d = CDate(s) To DateAdd("m", 1, CDate(s)) - 1
If Weekday(d, vbMonday) < 6 Then
Range("F2").Value = Format(d, "dddd")
Range("I2").Value = "" & Format(d, "m/d/yyyy")
'MsgBox ("printing ") 'for testing
ActiveSheet.PrintOut
End If
Next d
End Sub
Private Sub TextBox1_BeforeUpdate(ByVal Cancel As MSForms.ReturnBoolean)
If Mid(TextBox1.Value, 4, 2) > 12 Then
MsgBox "Invalid date, please re-enter", vbCritical
TextBox1.Value = vbNullString
TextBox1.SetFocus
Exit Sub
End If
StartDate = DateSerial(Year(Date), Month(Date), Day(Date))
TextBox1.Value = Format(TextBox1.Value, "dd.mm.yyyy")
StartDate = TextBox1.Value
End Sub
This code works well for me and my colleagues to make sure the value entered in the textbox is a date. However, a colleague from a different country (but also with the dd.mm.yyyy date-format) gets weird results.
For example: If he enters 01.10.2017 the TextBox automatically format the date into 20.03.4917.
I suspect that in this case the entered value is not recognized as a date but as a number because 01102017 will transform into 20.03.4917 in Excel if you convert it as a date.
Does anyone have a suggestion or a guess how to work around this problem?
Thanks and best regards
You could split the date by . into an array ArrInput and then use DateSerial to make it a real date, that you can format.
Dim ArrInput As Variant
ArrInput = Split(TextBox1.Value, ".")
'StartDate = DateSerial(Year(Date), Month(Date), Day(Date)) 'I see no use in this at all
TextBox1.Value = Format$(DateSerial(ArrInput(2), ArrInput(1), ArrInput(0)), "dd.mm.yyyy")
StartDate = TextBox1.Value
The issue with Format(TextBox1.Value, "dd.mm.yyyy") is that here you let Excel guess which date format the string in TextBox1.Value is. It automatically casts into a number which is then converted into a string again.
To avoid date misunderstandings I recommend always to use the YYYY-MM-DD format according to ISO 8601. This is the only date format that is human readable and cannot be misunderstood. It also has benefits when sorting by dates that are actually strings.
To make your validation even more solid use something like:
Dim ArrInput As Variant
ArrInput = Split(TextBox1.Value, ".")
Dim ValidDate As Boolean
If UBound(ArrInput) = 2 Then 'make sure there are exactly two dots in the date
If ArrInput(1) > 0 And ArrInput(1) <= 12 And _ 'month <= 12
ArrInput(0) > 0 And ArrInput(0) <= 31 Then 'day <= 31
ValidDate = True
End If
Else
ValidDate = False
End If
If Not ValidDate Then
MsgBox "Invalid date, please re-enter in format dd.mm.yyyy", vbCritical
TextBox1.Value = vbNullString
TextBox1.SetFocus
Exit Sub
End If
TextBox1.Value = Format$(DateSerial(ArrInput(2), ArrInput(1), ArrInput(0)), "dd.mm.yyyy")
StartDate = TextBox1.Value
I do believe that your colleague enters the text string "01102017" without dots.
You might want to convert such kind of entries into a valid date:
' "01102017" => 01.10.2017 - 8 chars variant
' "011017" => 01.10.2017 - 6 chars variant
This needs to be done of cause prior to a date conversion.
Add a log of the input value to a free cell range and
have the workbook sent back to you:
...
Private Sub TextBox1_BeforeUpdate(ByVal Cancel As MSForms.ReturnBoolean)
Range("Z1").value = "'" & TextBox1.Value
If Mid(TextBox1.Value, 4, 2) > 12 Then
...
Check if only numbers are used:
How to check if a string contains only numbers?
and check the length of the string (6 or 8 chars variant) and check if the year part is within a valid range => try to convert to a date and offer it to the user.
The date is supplied as a string in the form: 20180503
The function is supposed to validate that the entry is:
in the form YYYYMMDD
a valid date
The following code does not do the trick:
Function formatDateYYYYMMDD(dateStr As String, dateFormat As String) As String
Dim strToDate As Date
strToDate = CDate(dateStr)
If IsDate(strToDate) Then
formatDateYYYYMMDD= format(dateStr, dateFormat)
Else
formatDateYYYYMMDD= "Not a date"
End If
End Function
Perhaps:
edit: original UDF changed as it would not flag certain invalid format dates.
Option Explicit
Function formatDateYYYYMMDD(dateStr As String, dateformat As String) As String
Dim strToDate As Date
On Error GoTo invalidDate
If Len(dateStr) = 8 And _
Left(dateStr, 4) > 1900 And _
Mid(dateStr, 5, 2) <= 12 And _
Right(dateStr, 2) <= 31 Then
formatDateYYYYMMDD = Format(CDate(Format(dateStr, "0000-00-00")), dateformat)
Exit Function
End If
invalidDate: formatDateYYYYMMDD = "Not a date"
End Function
The On Error will pick up invalid dates that otherwise meet the format criteria: eg Sep 31, Feb 30
Interesting idea for a function. I've rewritten your code below to do exactly what you said. Function returns "Not a date" for 2018101a, 20181033, 201810300, otherwise returns date in formatted string. Note that you need to provide a valid string format and I did not handle that error. I assume there are no spaces at the end?
Function formatDateYYYYMMDD(dateStr As String, dateFormat As String) As String
Dim strToDate As Date
Dim day As Integer
Dim month As Integer
Dim year As Integer
On Error Resume Next
year = Left(dateStr, 4)
month = Mid(dateStr, 5, 2)
day = Right(dateStr, 2)
strToDate = DateSerial(year, month, day)
If Err.Number <> 0 Or Len(dateStr) <> 6 Then
formatDateYYYYMMDD = "Not a date"
Err.Clear
On Error GoTo 0
Else
On Error GoTo 0
formatDateYYYYMMDD = Format(strToDate, dateFormat)
End If
End Function
I fiddled with the code getting some directions from the guy's suggestion and it works now. Thanks, guys for all your input.
This is what I did
sValue = frmForm.txtSearch.Value
If IsDate(sValue) Then
'do nothing
Else
sValue = Format(frmForm.txtSearch.Value, "DD-MM-YYYY")
End If
If the input date is always in this format(YYYYMMDD), you can write a custom code to convert it into a string that can be converted to date using CDATE.
Remember to convert month to name of the month, and year to four digit year. In this way you are explicitly defining the month, year and the remaining one as date, if you keep them as two digit numbers they may be interpreted differently on difference systems (when you convert them using CDATE)
I recommend this format DD-MMM-YYYY
In your code instead of
strToDate = CDate(dateStr)
You have to write a custom function
And in place of
formatDateYYYYMMDD= format(dateStr, dateFormat)
Return just the dateStr and set the format of the cell where it is returned to YYYYMMDD
My code so far is like this:
Sub FindMatchingValue()
Dim i As Integer, TimeValueToFind As Date
TimeValueToFind = "04:00:00"
Sheets("Vessels").Range("F07").ClearContents
For i = 1 To 25 '
If CDate(Sheets("Vessels").Cells(i, 1).Value) = TimeValueToFind Then
MsgBox ("Found value on row " & i)
Sheets("Vessels").Range("F07").Value = Cells(i, 1).Offset(1, 1).Resize(1).Value
Exit Sub
End If
Next i
MsgBox ("Value not found in the range!")
End Sub
This code checks Column A for the time inputted in the format xx:xx:xx Both where the input is, and where the times are written are set as "Time" format.
Initially the CDate edit was not added. And this caused the code to always return false because, as it had been put, I was trying to "compare apples to oranges".
However adding the CDate addition produces a mismatch error. Similarly changing both to be a double also did not work:
Sub FindMatchingValue()
Dim i As Integer, TimeValueToFind As Date
TimeValueToFind = "04:00:00"
Sheets("Vessels").Range("F07").ClearContents
For i = 1 To 25 '
If Sheets("Vessels").Cells(i, 1).Value = CDbl(TimeValueToFind) Then ' < This was the line changed
MsgBox ("Found value on row " & i)
Sheets("Vessels").Range("F07").Value = Cells(i, 1).Offset(1, 1).Resize(1).Value
Exit Sub
End If
Next i
MsgBox ("Value not found in the range!")
End Sub
However this one is a different reason, since Excel stores the values as floating points, each value is still different. "It is well known that the expression a==b is likely to return False when a and b are both doubles, even though you might think they are the same. This is due to the finite precision with which floating point numbers are stored."
The way around this would be to Set a tolerance. If abs(a-b)<tolerance Then
However i'm not particularly sure which tolerance to use nor how to write it to include without messing up the first loop.
I wonder if anyone could shed some light on this and direct me to which additions I need to make and what sort of tolerances would be acceptable? I think the question is essentially twofold. Thank you in advance!
Use TimeValue() or TimeSerial() like so:
Sub SO()
Dim x As Date
Dim y As Date
Dim z As Date
x = TimeValue("04:00:00")
y = TimeSerial(4, 0, 0)
z = CDate(Range("A1").value) '// A1 has "04:00:00" entered
Debug.Print x = y '// True
Debug.Print y = z '// True
Debug.Print x = z '// True
End Sub
Putting this into the context of your code:
Sub FindMatchingValue()
Dim i As Integer, TimeValueToFind As Date
TimeValueToFind = TimeValue("04:00:00")
Sheets("Vessels").Range("F07").ClearContents
For i = 1 To 25 '
If CDate(Sheets("Vessels").Cells(i, 1).value) = TimeValueToFind Then
MsgBox ("Found value on row " & i)
Sheets("Vessels").Range("F07").value = Cells(i, 1).Offset(1, 1).Resize(1).value
Exit Sub
End If
Next i
MsgBox ("Value not found in the range!")
End Sub
You are correct that the imprecision of floating point numbers is the cause of your problem. Remember that the underlying data in a Date data type is still a Double, formatted to look like a date.
The question of "...what sort of tolerances would be acceptable?" is really up to you. Given that your test value is "hh:mm:ss" then equal to the second may suffice.
There are many ways to achieve this. If your data is formatted as "hh:mm:ss" then this will work
If CDate(Sheets("Vessels").Cells(i,1).Text) = TimeValueToFind Then
This relies on the format applied to the sheet being to the same precision as your test value
For those interested, here is the answer:
Sub FindMatchingValue()
Dim i As Integer, TimeValueToFind As Date, Delta As Double, Tolerance As Double
TimeValueToFind = Sheets("Vessels").Range("F06")
Tolerance = 0.001
Sheets("Vessels").Range("F07").ClearContents
For i = 2 To 25 '
Delta = Sheets("Vessels").Cells(i, 1).Value - CDbl(TimeValueToFind)
If Abs(Delta) <= Tolerance Then
MsgBox ("Found value on row " & i)
Sheets("Vessels").Range("F07").Value = Cells(i, 1).Offset(0, 1).Resize(1).Value
Exit Sub
End If
Next i
MsgBox ("Value not found in the range!")
End Sub
So any time in the box F06 typed in, it now finds. A combination of tolerance was used and also converting to a Double. i = 1-25 was changed to 2-25, because I had a text header and that was producing a mismatch error.