Use Keyboard Maestro to convert time to decimal

When I invoice clients I often need to convert HH:MM to decimal. I searched all over for a way to use KM or use AppleScript to create a service that would allow me to right-click on any document and do the conversion for me and then either copy the result to the clipboard or display it in a window. However, I haven't found anything.

I know a few websites as well as Excel can do this but It's too time consuming to have to open a spreadsheet or visit a URL. I'm hoping for a quicker way.

Does anyone have a macro or Applescript that can do this?

Thanks.

When you say you wish to convert HH:MM to decimal, do you mean convert it to seconds, e.g. since midnight or since a fixed date ?

Give an example of what sort of conversion you're expecting.

For example, I log my working time such as:

Create web page mockup: 01:35 (HH:MM)
Color correct video: 76 min

So the macro/script would help me do the conversion for the two examples above as follows:

01:35 would give me: 1.58 hours

76 mins would give me: 1.27 hours

Answering your question now makes me realize that I might need 2 scripts. One to convert from HH:MM to Hours Decimal, and another to convert from minutes to Hours Decimal. Unless the same script can do both!

And the whole reason I need this is because my invoicing system only takes Hours Decimal.

It would be very time-saving if I could right-click on task durations such as the ones above and have a script convert it on the fly so I can simply paste into the invoice.

I know I might sound lazy but when you have to do several of these, it just takes too much time :slightly_smiling_face:

This forum is literally devoted to discussing ways to shortcut our way through everything. I think your motivation and objective a perfectly reasonable.

2 Likes

Converting from HH:MM to minutes is easily done by breaking the time apart with a Search using Regular Expression action, and then calculating the total:

The reverse is similarly done, breaking the time into hours and minutes and then joining back together.

Keyboard Maestro Actions.kmactions (1.6 KB)

2 Likes

@peternlewis beat me to this by like two minutes :wink: but in the interest of alternate solutions, here's a macro that is designed to be called from a macOS service that should do what you describe for both HH:MM times and minutes:

Time to Decimal.kmmacros (3.5 KB)
image

You may already know this, but to use it as a service, just create a new service in Automator that runs an AppleScript action like this:

08%20PM

on run {input}
	
	tell application "Keyboard Maestro Engine" to do script "Time to Decimal" with parameter input
	
end run
2 Likes

Assuming that your input source data is always on sequential lines, then this macro should produce a nice report for you.

As always, please feel free to post any comments, issues, and/or suggestions you may have concerning this macro.

Example Output

image

EDIT: 2018-08-12 23:57 GMT-5

BTW, the KM Variable "Local__Report" is a tab-delimited report. So you could easily paste it into a spreadsheet like Excel, and the Task and Hours would go into separate cells.


RegEx

(?m)^(.+):\h+(\d+)(?::(\d+))?
For details, see https://regex101.com/r/U91lyI/1/


MACRO:   Convert Time Listing to All Hours

~~~ VER: 1.0    2018-08-12 ~~~

DOWNLOAD:

Convert Time Listing to All Hours.kmmacros (5.4 KB)
Note: This Macro was uploaded in a DISABLED state. You must enable before it can be triggered.


image

2 Likes

For completeness and flexibility, a couple of AppleScript and Javascript functions allowing for formats like M, MM, HH:MM, and HH:MM:SS

minsFromHMS :: String -> Int

and

hoursFromMins :: Int -> Float

for example:

map(minsFromHMS, {"1:35:29", "1:35", "2:05", "76"})
-->  {95, 95, 125, 76}
map(hoursFromMins, {95, 95, 125, 76})
-->  {1.58, 1.58, 2.08, 1.27}
sum(map(hoursFromMins, {95, 95, 125, 76}))
--> 6.51
sum(map(hoursFromMins, map(minsFromHMS, ["1:35:29", "1:35", "2:05", "76"])))
--> 6.51

Applescript and Javascript source below

Applescript source

-- minsFromHMS :: String -> Int
on minsFromHMS(strHMS)
    -- HOURS AND MINUTES AS SUM OF MINUTES
    script sumOfParts
        property digit : my isDigit
        on |λ|(a, x, i, xs)
            if all(digit, characters of x) then
                if 0 ≠ (i - (length of xs)) then
                    set m to 60
                else
                    set m to 1
                end if
                a + (m * (x as integer))
            else
                a
            end if
        end |λ|
    end script
    
    set xs to splitOn(":", strHMS)
    set mins to foldl(sumOfParts, 0, take(2, xs))
    
    -- PLUS ONE MINUTE IN THE CASE OF 30 OR MORE TRAILING SECONDS
    set final to item -1 of xs
    if 2 < length of xs and ¬
        all(my isDigit, characters of final) and ¬
        30 ≤ (final as integer) then
        1 + mins
    else
        mins
    end if
end minsFromHMS

-- hoursFromMins :: Int -> Float
on hoursFromMins(n)
    (round (100 * (n / 60))) / 100
end hoursFromMins

-- TEST ------------------------------------------------------------------
on run
    minsFromHMS("2:05") --> 125
    
    hoursFromMins(125) --> 2.08
    
    map(minsFromHMS, ["1:35:29", "1:35", "2:05", "76"]) --> {95, 95, 125, 76}
    
    map(hoursFromMins, {95, 95, 125, 76}) --> {1.58, 1.58, 2.08, 1.27}
    
    sum(map(hoursFromMins, map(minsFromHMS, ["1:35:29", "1:35", "2:05", "76"]))) --> 6.51
end run


-- GENERIC FUNCTIONS -----------------------------------------------

-- https://github.com/RobTrew/prelude-applescript

-- Applied to a predicate and a list, `all` determines if all elements 
-- of the list satisfy the predicate.
-- all :: (a -> Bool) -> [a] -> Bool
on all(f, xs)
    tell mReturn(f)
        set lng to length of xs
        repeat with i from 1 to lng
            if not |λ|(item i of xs, i, xs) then return false
        end repeat
        true
    end tell
end all

-- foldl :: (a -> b -> a) -> a -> [b] -> a
on foldl(f, startValue, xs)
    tell mReturn(f)
        set v to startValue
        set lng to length of xs
        repeat with i from 1 to lng
            set v to |λ|(v, item i of xs, i, xs)
        end repeat
        return v
    end tell
end foldl

-- isDigit :: Char -> Bool
on isDigit(c)
    set n to (id of c)
    48 ≤ n and 57 ≥ n
end isDigit

-- Lift 2nd class handler function into 1st class script wrapper 
-- mReturn :: First-class m => (a -> b) -> m (a -> b)
on mReturn(f)
    if class of f is script then
        f
    else
        script
            property |λ| : f
        end script
    end if
end mReturn

-- map :: (a -> b) -> [a] -> [b]
on map(f, xs)
    tell mReturn(f)
        set lng to length of xs
        set lst to {}
        repeat with i from 1 to lng
            set end of lst to |λ|(item i of xs, i, xs)
        end repeat
        return lst
    end tell
end map

-- min :: Ord a => a -> a -> a
on min(x, y)
    if y < x then
        y
    else
        x
    end if
end min

-- splitOn :: String -> String -> [String]
on splitOn(needle, haystack)
    set {dlm, my text item delimiters} to ¬
        {my text item delimiters, needle}
    set xs to text items of haystack
    set my text item delimiters to dlm
    return xs
end splitOn

-- sum :: [Num] -> Num
on sum(xs)
    script add
        on |λ|(a, b)
            a + b
        end |λ|
    end script
    
    foldl(add, 0, xs)
end sum

-- take :: Int -> [a] -> [a]
-- take :: Int -> String -> String
on take(n, xs)
    if class of xs is string then
        if 0 < n then
            text 1 thru min(n, length of xs) of xs
        else
            ""
        end if
    else
        if 0 < n then
            items 1 thru min(n, length of xs) of xs
        else
            {}
        end if
    end if
end take

-- Tuple (,) :: a -> b -> (a, b)
on Tuple(a, b)
    {type:"Tuple", |1|:a, |2|:b, length:2}
end Tuple

JavaScript Source

(() => {
    'use strict';

    // minsFromHMS :: String -> Int
    const minsFromHMS = strHHMM => {
        const
            parts = strHHMM.split(':'),
            final = parts[parts.length - 1],
            mins = parts.slice(0, 2)
            .reduce(
                (a, x, i, xs) => a + (
                    parseInt(x) * (
                        Boolean(i - xs.length + 1) ? (
                            60
                        ) : 1
                    )
                ), 0
            );
        return (2 < parts.length) &&
            !isNaN(final) && (30 <= parseInt(final, 10)) ? (
                1 + mins
            ) : mins;
    };

    // hoursFromMins :: Int -> Float
    const hoursFromMins = n =>
        Math.round(100 * (n / 60)) / 100;


    // TEST --------------------------------------------------
    const main = () => {
        minsFromHMS('2:05'); // -> 125

        hoursFromMins(125); // -> 2.08

        map(minsFromHMS, ['1:35:29', '1:35', '2:05', '76']);
        // -> {95, 95, 125, 76}

        map(hoursFromMins, [95, 95, 125, 76]);
        // -> {1.58, 1.58, 2.08, 1.27}

        return sum(map(
            hoursFromMins,
            map(minsFromHMS, ['1:35:29', '1:35', '2:05', '76'])
        ));
        //-> 6.51
    };

    // GENERIC FUNCTIONS --------------------------------------

    // https://github.com/RobTrew/prelude-jxa

    // map :: (a -> b) -> [a] -> [b]
    const map = (f, xs) => xs.map(f);

    // sum :: [Num] -> Num
    const sum = xs => xs.reduce((a, x) => a + x, 0);

    // MAIN ---
    return main();
})();

1 Like