4

I'm working on a simple Excel file with some worksheets where in every one I've report hours and minutes of work. I want to show it like 313:32 that is 313 hours and 32 minutes, to do that I'm using a custom format [h]:mm

To facilitate the workers that use Excel very little, I have thought to create some vba code, so that they could insert also not only the minutes, besides the classical format [h]:mm, so they can also insert value in hours and minutes. I report some example data that I want to have. What I insert -> what I want that are printed inside the cell

  • 1 -> 0:01
  • 2 -> 0:02
  • 3 -> 0:03
  • 65 -> 1:05
  • 23:33 -> 23:33
  • 24:00 -> 24:00
  • 24:01 -> 24:01

Then I formatted every cell that can contain a time value in [h]:mm and I wrote this code

Public Sub Workbook_SheetChange(ByVal Sh As Object, ByVal Target As Range)
On Error GoTo bm_Safe_Exit
    With Sh
        If IsNumeric(Target) = True And Target.NumberFormat = "[h]:mm" Then

            If Int(Target.Value) / Target.Value = 1 Then
                Debug.Print "Integer -> " & Target.Value
                Application.EnableEvents = False
                Target.Value = Target.Value / 1440
                Application.EnableEvents = True
                Exit Sub
            End If

            Debug.Print "Other value -> " & Target.Value
        End If
    End With
bm_Safe_Exit:
    Application.EnableEvents = True
End Sub

The code works well enough, but it errs when I enter 24:00 and its multiples, 48:00, 72:00 ... This because the cell are formatted [h]:mm so 24:00 became 1 before the vba code execution!

I tried to correct the code, and the funny fact is that when I correct the 24:00, so 24:00 remain 24:00 and not 00:24, the problem switch to 1 that became 24:00 instead 00:01

My first idea was to "force" the vba code execution before the cell format, but I don't know if it is possible. I know that seems a stupid question, but I really don't know if it is possible and how to fix it.

Any idea will be much appreciated

Jorman Franzini
  • 329
  • 1
  • 3
  • 17
  • maybe [look this](https://stackoverflow.com/a/14985573/11167163) – TourEiffel Mar 02 '21 at 23:08
  • Thanks Dorian for your reply, I'm not so expert but, usually I insert data like h:m not a fraction of minutes or hours like 3.5 or 3,5 and "pretend" to have 3 hours and 30 minutes. So thanks again for this, can be useful in some way, but I don't think it can be now. But I'm not so expert and, maybe, actually is a good starting point. – Jorman Franzini Mar 03 '21 at 12:42
  • 1
    *so that they could insert also only the minutes* But later on as example you use values like `23:33, 24:00, 24:01` That's not only minutes, those inputs are hours and minutes. Following your own rule, those values should be entered as `1413, 1440, 1441` and your code would convert those inputs into ``23:33, 24:00, 24:01``. So you are breaking your own input rule. Change the aproach or modify the code to check first if the inserted value is just minutes or hours and minutes. Honestly, I think easier way would be forcing workers to enter an **integer** value of just minutes. – Foxfire And Burns And Burns Mar 07 '21 at 02:58
  • Hi @FoxfireAndBurnsAndBurns yes you're right, they could insert only minutes, but they also could insert hours and minutes. I'll correct the question, It can be misinterpreted, although the example of what I want to achieve is correct – Jorman Franzini Mar 08 '21 at 18:57
  • 2
    I'm not sure if allowing 2 different types of units in same cell is the right approach. IT's like allowing in same cell miles and kilometers, or pounds and kilograms. That may cause problems. Consider allowing to work only in format `hh:mm` so 1 minute would be input as `00:01`. Actually, I think it's easier to work in 1 single unit of time than in 2 different ones. – Foxfire And Burns And Burns Mar 09 '21 at 08:34
  • 1
    Always prefer fixing the inputs rather than later sanitizing. Instruct users to use one format and enforce it with data validation checks. – QHarr Mar 09 '21 at 22:58

2 Answers2

5

Requirements: Time is to be reported in Hours and Minutes, minutes is the lowest measure ( i.e.: whatever the amount of time is to be reported in hours and the partial hours in minutes, i.e. 13 days, 1 hour and 32 minutes or 13.0638888888888889 shall be shown as 313:32 ) Users should be allowed to enter time in two different manners:

  1. To enter only minutes: The value entered shall be a whole number (no decimals).
  2. To enter hours and minutes: The value entered shall be composed by two whole numbers representing the hours and the minutes separated a colon :.

Excel Processing Values Entered:

Excel intuitively process the Data type and Number.Format of the values entered in cells. When the Cell NumberFormat is General, Excel converts the values entered to the data type in relation with the data entered (String, Double, Currency, Date, etc. ), it also changes the NumberFormat as per the “format” entered with the value (see table below).

enter image description here

When the Cell NumberFormat is other than General, Excel converts the values entered to the data type corresponding to the format of the cell, with no changes to the NumberFormat (see table below).

enter image description here

Therefore, it's not possible to know the format of the values as entered by the user, unless the values entered can be can intercepted before Excel applies its processing methods.

Although the values entered cannot be intercepted before Excel process them, we can set a validation criteria for the values entered by the users using the Range.Validation property.

Solution: This proposed solution uses:

It's suggested to use a customized style to identify and format the input cells, actually OP is using the NumberFormat to identify the input cells, however it seems that there could also be cells with formulas, or objects (i.e. Summary Tables, PivotTables, etc.) that require the same NumberFormat. By using the customized style only for the input cells, the non-input cells can be easily excluded from the process.

The Style object (Excel) allows to set the NumberFormat, Font, Alignment, Borders, Interior and Protection at once for a single or multiple cells. The procedure below adds a customized Style named TimeInput. The name of the Style is defined as a public constant because it will be used across the workbook.

Add this into an standard module

Public Const pk_StyTmInp As String = "TimeInput"

Private Sub Wbk_Styles_Add_TimeInput()
    
    With ActiveWorkbook.Styles.Add(pk_StyTmInp)
        
        .IncludeNumber = True
        .IncludeFont = True
        .IncludeAlignment = True
        .IncludeBorder = True
        .IncludePatterns = True
        .IncludeProtection = True
    
        .NumberFormat = "[h]:mm"
        .Font.Color = XlRgbColor.rgbBlue
        .HorizontalAlignment = xlGeneral
        .Borders.LineStyle = xlNone
        .Interior.Color = XlRgbColor.rgbPowderBlue
        .Locked = False
        .FormulaHidden = False
    
    End With

End Sub

The new Style will show in the Home tab, just select the input range and apply the Style.

enter image description here

We’ll use the Validation object (Excel) to tell users the criteria for the time values and to force them to enter the values as Text. The following procedure sets the style of the Input range and adds a validation to each cell:

Private Sub InputRange_Set_Properties(Rng As Range)

Const kFml As String = "=ISTEXT(#CLL)"
Const kTtl As String = "Time as ['M] or ['H:M]"
Const kMsg As String = "Enter time preceded by a apostrophe [']" & vbLf & _
                            "enter M minutes as 'M" & vbLf & _
                            "or H hours and M minutes as 'H:M"  'Change as required
Dim sFml As String
    
    Application.EnableEvents = False
    
    With Rng

        .Style = pk_StyTmInp
        sFml = Replace(kFml, "#CLL", .Cells(1).Address(0, 0))

        With .Validation
            .Delete
            .Add Type:=xlValidateCustom, _
                AlertStyle:=xlValidAlertStop, _
                Operator:=xlBetween, Formula1:=sFml
            .IgnoreBlank = True
            .InCellDropdown = False

            .InputTitle = kTtl
            .InputMessage = kMsg
            .ShowInput = True

            .ErrorTitle = kTtl
            .ErrorMessage = kMsg
            .ShowError = True

    End With: End With

    Application.EnableEvents = True

End Sub

The procedure can be called like this

Private Sub InputRange_Set_Properties_TEST()
Dim Rng As Range
    Set Rng = ThisWorkbook.Sheets("TEST").Range("D3:D31")
    Call InputRange_Set_Properties(Rng)
    End Sub

Now that we have set the input range with the appropriated style and validation, let’s write the Workbook Event that will process the Time inputs:

Copy these procedures in ThisWorkbook module:

  • Workbook_SheetChange - Workbook event
  • InputTime_ƒAsDate - Support function
  • InputTime_ƒAsMinutes - Support function

Private Sub Workbook_SheetChange(ByVal Sh As Object, ByVal Target As Range)

Const kMsg As String = "[ #INP ] is not a valid entry."
Dim blValid As Boolean
Dim vInput As Variant, dOutput As Date
Dim iTime As Integer
    
    Application.EnableEvents = False
    
    With Target

        Rem Validate Input Cell
        If .Cells.Count > 1 Then GoTo EXIT_Pcdr         'Target has multiple cells
        If .Style <> pk_StyTmInp Then GoTo EXIT_Pcdr    'Target Style is not TimeInput
        If .Value = vbNullString Then GoTo EXIT_Pcdr    'Target is empty
        
        Rem Validate & Process Input Value
        vInput = .Value                         'Set Input Value
        Select Case True
        Case Application.IsNumber(vInput):      GoTo EXIT_Pcdr      'NO ACTION NEEDED - Cell value is not a text thus is not an user input
        Case InStr(vInput, ":") > 0:            blValid = InputTime_ƒAsDate(dOutput, vInput)        'Validate & Format as Date
        Case Else:                              blValid = InputTime_ƒAsMinutes(dOutput, vInput)     'Validate & Format as Minutes
        End Select

        Rem Enter Output
        If blValid Then
            Rem Validation was OK
            .Value = dOutput
            
        Else
            Rem Validation failed
            MsgBox Replace(kMsg, "#INP", vInput), vbInformation, "Input Time"
            .Value = vbNullString
            GoTo EXIT_Pcdr
        
        End If

    End With

EXIT_Pcdr:
    Application.EnableEvents = True

End Sub

Private Function InputTime_ƒAsDate(dOutput As Date, vInput As Variant) As Boolean

Dim vTime As Variant, dTime As Date
    
    Rem Output Initialize
    dOutput = 0
              
    Rem Validate & Process Input Value as Date
    vTime = Split(vInput, ":")
    Select Case UBound(vTime)
    
    Case 1
        
        On Error Resume Next
        dTime = TimeSerial(CInt(vTime(0)), CInt(vTime(1)), 0)   'Convert Input to Date
        On Error GoTo 0
        If dTime = 0 Then Exit Function                         'Input is Invalid
        dOutput = dTime                                         'Input is Ok
        
    Case Else:      Exit Function                               'Input is Invalid
    End Select

    InputTime_ƒAsDate = True
    
End Function

Private Function InputTime_ƒAsMinutes(dOutput As Date, vInput As Variant) As Boolean

Dim iTime As Integer, dTime As Date
    
    Rem Output Initialize
    dOutput = 0
                
    Rem Validate & Process Input Value as Integer
    On Error Resume Next
    iTime = vInput
    On Error GoTo 0
    Select Case iTime = vInput
    
    Case True
        On Error Resume Next
        dTime = TimeSerial(0, vInput, 0)    'Convert Input to Date
        On Error GoTo 0
        If dTime = 0 Then Exit Function     'Input is Invalid
        dOutput = dTime                     'Input is Ok
        
    Case Else:      Exit Function           'Input is Invalid
    End Select

    InputTime_ƒAsMinutes = True
    
End Function

The table below shows the output for various types of values entered.

enter image description here

EEM
  • 6,601
  • 2
  • 18
  • 33
  • 3
    Awesome job here. As I said in the comments, I think allowing 2 different measures in same cell is a terrible approach and the lenght of your code proves it (there is no easy way to do it) but you did an epic research and a full developing here, and posted a documented answer. Upvoted. – Foxfire And Burns And Burns Mar 11 '21 at 07:44
  • Hi @FoxfireAndBurnsAndBurns, thanks for your kinds words. I consider that there is no problem in allowing different types of measures as long as there is a clear, unique, concise and congruent way to identify them (_rules_), supported by clear _instructions_ and _feedback_ to users, so that they know what is expected from them, why some entries do not show what they expect, and others are simply not valid. If that cannot be achieved, then only the one-measure path remains. However, even so, _rules, instructions and feedback are still essential_. Remember there is no fools proof system. – EEM Mar 11 '21 at 14:00
  • _**Only those who attempt the absurd can achieve the impossible!**_ Thank you EEM! Very good explanation. Now I'll change format and adapt information and format for the custom `TimeInput`. I've a question. I think can be optional, but I can't figure out how to properly set the `InputRange_Set_Properties` I mean, is supposed to start when I open the document, right? What is your main idea? I think the best is to create a named range and set to it. J – Jorman Franzini Mar 12 '21 at 22:09
  • The idea of creating a `Named Range` is good one, however bear in mind that the creation of the customized style, the named ranges and applying the range validation is a onetime process, it does not need to be repeated when the document is opened. It is just like what you are doing now when assigning the `[h]:mm` format to the ranges. Let me know how it goes. – EEM Mar 13 '21 at 02:40
  • Thank you EEM for pointing me that. Do you think it is possible to set by default `InputRange_Set_Properties` to all cells that have `TimeInput` as style? In this way every time I've to set or to change some all is always update – Jorman Franzini Mar 13 '21 at 10:01
  • Absolutely, you can modify the procedure in order to find all the cells with the `TimeInput` Style and apply the Validation property to them. _Do you think it is possible to set by default?_ It’s possible?, absolutely. Practical?, I doubt it, be aware that `InputRange_Set_Properties` is not a “production” procedure but a “maintenance” one. – EEM Mar 13 '21 at 14:44
  • Okay, so how would you use the `InputRange_Set_Properties` procedure? This process will be running on multiple worksheets, and if I add one or more rows or columns, I've to format them with the right `TimeInput` style, however then on those cells no validation will be displayed, or am I getting it wrong? – Jorman Franzini Mar 13 '21 at 16:13
  • Sorry but I'm not able to do that. I moved the `InputRange_Set_Properties` function to the module. Now I've to Call `InputRange_Set_Properties()` but I need to pass a range and I don't know how I can use the actual selected range and use it right after the `TimeInput` style application – Jorman Franzini Mar 13 '21 at 16:42
  • `Call InputRange_Set_Properties(Selection)` – EEM Mar 13 '21 at 16:45
  • I tried to remove custom style `TimeInput` from Home tab, saved, closed and opened back the file, `TimeInput` was not present, so I selected some cells, opened the code and ran manually the module. Now the cells that was selected have the custom style with validation property set, but if I select another range and from home tab I set custom style `TimeInput`, the validation property is not set. What I do wrong? – Jorman Franzini Mar 13 '21 at 17:11
  • How come you remove the style and expect it to still be there?. Have you check the documentation included in the post? It is there for a purpose. Have a look at it and you'll see that `Workbook.Style` and `Range.Validation` are two different properties of two different objects. Therefore you need to apply each to every input cell, once applied it will stay there unless you delete them, but i guess that should not be new to you, if like if you type something in a cell and delete it, it is not there any more. – EEM Mar 13 '21 at 18:13
  • I guess I didn't explain myself well. Follow this test: New empty file with `Wbk_Styles_Add_TimeInput` in a empty module, `InputRange_Set_Properties` in the module? you don't tell where, `Workbook_SheetChange`, `InputTime_ƒAsDate` and `InputTime_ƒAsMinutes` functions in `ThisWorkbook` object. At this point I don't see the new Style in Home tab, only if I run vba manually, normal? I only want to open the file, select a range in any sheet, apply the new style and at the same time the validation. Is possible? Like I said I'm not able to do that – Jorman Franzini Mar 13 '21 at 20:58
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/229893/discussion-between-eem-and-jorman-franzini). – EEM Mar 14 '21 at 15:39
1

The simplest way appears to be to use the cell text (i.e. how the cell is displayed) in preference to the actual cell value. If it looks like a time (e.g. "[h]:mm", "hh:mm", "hh:mm:ss") then use that to add the value of each time part accordingly (to avoid the 24:00 issue). Otherwise, if it's a number, assume that to be minutes.

The below method also works for formats like General, Text and Time (unless the time begins with a days part, but it could be further developed to deal with that too where necessary).

Public Sub Workbook_SheetChange(ByVal Sh As Object, ByVal Target As Range)
    On Error GoTo bm_Safe_Exit
    
    Dim part As String, parts() As String, total As Single
    
    Application.EnableEvents = False
    
    If Not IsEmpty(Target) And Target.NumberFormat = "[h]:mm" Then
        'prefer how the Target looks over its underlying value
        If InStr(Target.Text, ":") Then
            'split by ":" then add the parts to give the decimal value
            parts = Split(Target.Text, ":")
            total = 0
            
            'hours
            If IsNumeric(parts(0)) Then
                total = CInt(parts(0)) / 24
            End If
            
            'minutes
            If 0 < UBound(parts) Then
                If IsNumeric(parts(1)) Then
                    total = total + CInt(parts(1)) / 1440
                End If
            End If
        ElseIf IsNumeric(Target.Value) Then
            'if it doesn't look like a time format but is numeric, count as minutes
            total = Target.Value / 1440
        End If
        
        Target.Value = total
    End If
    
bm_Safe_Exit:
    Application.EnableEvents = True
End Sub
sbgib
  • 5,580
  • 3
  • 19
  • 26
  • Hi @sbgib, thank you for your help, I'll try the code, and I'll update you. Can be a starting point, I don't know if I can set only one range, probably I need to set more ranges. Tomorrow I'll try your code. One question, with this code, is possible to make sum on other cells? I've others cells (external to an eventual range) that sum hours and minutes. – Jorman Franzini Mar 07 '21 at 17:52
  • 1
    Friendly hint: you are using the argument set of the `Worksheet_Change()` event, not of the `Workbook_SheetChange()` event :-) @sbgib – T.M. Mar 07 '21 at 19:07
  • @sbgib I tried to copy paste the code, but I get error during tests, some like "procedure declaration does not match description of event or procedure having the same name" – Jorman Franzini Mar 07 '21 at 21:38
  • 1
    @T.M. thanks for pointing that out, I've corrected it now. – sbgib Mar 08 '21 at 08:08
  • @JormanFranzini sorry about that! If you try again now it should work. It is possible to sum with this code, the resulting values will be numeric in the `"[h]:mm"` format, as you'd expect. – sbgib Mar 08 '21 at 08:11
  • Thank you @sbgib for you help and support. I think you are very close to the final result, seems you're good in Excel. I tried the code, I hope is only a problem in my side, but if I insert 1 or 2 or any integer the code use that value for a multiple of a day, so 1 -> 24:00, 2 -> 48:00 ... Looks like a short blanket. I'm not expert like you but at the end I'm stuck in the same point, so when 1 -> 00:01 the error is 24:00 -> 00:01 and after a fix, the error is 1 -> 24:00 but 24:00 -> 24:00. J – Jorman Franzini Mar 08 '21 at 18:49
  • In that case, I don't think it's going to be possible with the current design. The `Workbook_SheetChange` event triggers after the user has changed the value in the `Target`, by which time the value they entered has already been applied, so there is no way to know what they originally typed (at least, without making it very complicated). It may be much easier to just use a text field instead, then convert it to `"[h]:mm"` using VBA, but then the problem becomes how to identify which cells to change. Another way is to manually run the code, as in my original answer - that seems to work ok. – sbgib Mar 09 '21 at 09:14
  • Oh, that's bad, I thought the expected results were clear. It doesn't have to be `Workbook_SheetChange`, can be on worksheet too, I punt it on `Workbook` because I've a lot of worksheets, but the user have to do nothing. There's no way to set all like text and then make 2 vba? One to "transform" simple text to xx:xx format then the second vba that works only on sum cells? or specific ranges? Another idea, but I don't know if is possible, there's a way to "intercept" entered value before the cell format came and "transform" data? Seems that format cell came before the vba, can be forced? J – Jorman Franzini Mar 09 '21 at 18:56
  • The intercept idea is one I had too, but it'd be difficult. In VBA there is no "beforeCellUpdated" method, so [keeping track of which cell is selected](https://docs.microsoft.com/en-us/office/vba/api/excel.workbook.sheetselectionchange) and [running a procedure each time any key is pressed](https://docs.microsoft.com/en-us/office/vba/api/Excel.Application.OnKey) would be the alternative (which wouldn't work with copy & paste, for example). If they were already text then it'd be ok, but it'd only work for a specified range and would not work if the user tries to change the field again. – sbgib Mar 10 '21 at 08:27
  • Another idea was as you say, convert the `Target` to text format in `Workbook_SheetChange` so the value can be checked, but by then the `1` value has already been converted to `24:00` by the existing `"[h]:mm"` formatting, so that wouldn't work either. Another way would be to make an input form which the users can fill in so that the entries can be checked and converted before being applied to the worksheet. If I think of any other ideas I'll give them a try! – sbgib Mar 10 '21 at 08:27
  • What if you filter every keypress then, if `CTRL+C` or `CTRL+V` exit. If non digits, except `:`, are pressed, exit. So with only progressive digits stay in sub then if `:` key is pressed you change the format to text then you can strip out all data, calculate the right value, put back on cell and change format to [h]:mm. Finish if only numbers are typed, after enter you can do the calculations. I don't know if you also have to filter `enter` key. What do you think? – Jorman Franzini Mar 10 '21 at 18:30
  • I think even if that all worked as expected, it would still likely cause other problems. For example, if the user types a non-digit by mistake and wants to remove it, or if they click the mouse instead of pressing enter to end the text, there is no way to reliably capture all of these events. Performance would suffer from running code on every keypress and there would need to be controls to make sure this only runs on the intended cells (e.g. there could be time cells elsewhere in the workbook that should not be affected). You would be designing your entire workbook around this one detail. – sbgib Mar 11 '21 at 13:13