Formatting negative time on a countdown timer

Hi again,
I have the following code running a countdown timer. It works just fine, until the clock hits zero when it starts counting back up as a negative number. This in itself is fine (and actually quite useful for our needs), but I have two minor issues. One is that it displays as "-1:-1, -1:12 etc", so I need to somehow add leading zeros (I guess) to it when it's in negative. The other is a slightly bigger problem which is that it seems to jump straight from "0" to "-1:-1", so it basically skips a minute (the first minus minute should be "-00:01").

This function node is setting the remaining time (there are other buttons involved to start, stop, add and remove time on the fly):

var currentTime = msg.payload;
var lastTickTime = flow.get("lastTickTime") || currentTime;
var timeRemaining = flow.get("timeRemaining") || 0;
var gameState = flow.get("gameState");

if(gameState == "playing") {

    timeRemaining -= (currentTime - lastTickTime);
    
    flow.set("timeRemaining", timeRemaining);
    
}

msg.timeRemaining = timeRemaining;
flow.set("lastTickTime", currentTime);

node.status({fill:"green", shape:"dot", text:timeRemaining});


return msg;

And then this function formats the time to a simple mm:ss display:

var t = msg.timeRemaining / 1000;
var m = Math.floor(t / 60);
var s = Math.floor(t%60 % 60);

msg.formattedTimeRemaining = ("0" + m).slice(-2) + ":" + ("0" + s).slice(-2);

node.status({fill:"green", shape:"dot", msg:flow.formattedTimeRemaining});


return msg;

The node status on the first function counts down as 2000, 1000, 0, -1000, -2000 etc, so I guess I just need to figure out what I can add to the second function to format the negative time properly.

Any ideas?

I can't see anywhere that you are actually generating a timestamp?

You should probably be using a millisecond based timestamp (e.g. JavaScript Date) and then you could use JavaScript date/time functions to work everything out.

I appreciate you are trying to do it in a function node - and I don't necessarily have the bigger picture, but rather than re-inventing the wheel, are you open to using a separate node as the countdown timer. Something like stoptimer-varidelay (https://flows.nodered.org/node/node-red-contrib-stoptimer-varidelay)?

The timestamp comes into the first function as the msg.payload.

I'm not trying to reinvent anything here, just using what others have supplied (I'd have never of got this far on my own! :sweat_smile:), and trying to tweak it a little. The timer works just fine as it is, and I've managed to add some buttons for adding and removing time successfully. It just seems to be something in the formatting that isn't quite right, or non existing, for when it goes into minus time. It's for an escape room timer/gamemaster software, so there's other buttons and arduinos going to be linked into it all, but I here (I think) are all the parts that relate to the timer section:

[
    {
        "id": "906339c5.ce4fa",
        "type": "inject",
        "z": "29662af6.96a146",
        "name": "Setup",
        "props": [
            {
                "p": "payload"
            }
        ],
        "repeat": "",
        "crontab": "",
        "once": true,
        "onceDelay": 0.1,
        "topic": "",
        "payload": "",
        "payloadType": "date",
        "x": 190,
        "y": 100,
        "wires": [
            [
                "becf850a.cbcee"
            ]
        ]
    },
    {
        "id": "becf850a.cbcee",
        "type": "change",
        "z": "29662af6.96a146",
        "name": "",
        "rules": [
            {
                "t": "set",
                "p": "gameState",
                "pt": "flow",
                "to": "stopped",
                "tot": "str"
            },
            {
                "t": "set",
                "p": "timeRemaining",
                "pt": "flow",
                "to": "3600000",
                "tot": "str"
            },
            {
                "t": "set",
                "p": "currentTime",
                "pt": "flow",
                "to": "payload",
                "tot": "msg"
            },
            {
                "t": "set",
                "p": "logging",
                "pt": "flow",
                "to": "true",
                "tot": "bool"
            }
        ],
        "action": "",
        "property": "",
        "from": "",
        "to": "",
        "reg": false,
        "x": 360,
        "y": 100,
        "wires": [
            [
                "cc5aea0b.1742e"
            ]
        ]
    },
    {
        "id": "ca343107.aec8b",
        "type": "inject",
        "z": "29662af6.96a146",
        "name": "Start",
        "props": [
            {
                "p": "payload"
            },
            {
                "p": "topic",
                "vt": "str"
            }
        ],
        "repeat": "",
        "crontab": "",
        "once": false,
        "onceDelay": 0.1,
        "topic": "",
        "payload": "",
        "payloadType": "date",
        "x": 190,
        "y": 240,
        "wires": [
            [
                "49b44f5a.132568"
            ]
        ]
    },
    {
        "id": "9266125d.112b38",
        "type": "inject",
        "z": "29662af6.96a146",
        "name": "Stop",
        "props": [
            {
                "p": "payload"
            },
            {
                "p": "topic",
                "vt": "str"
            }
        ],
        "repeat": "",
        "crontab": "",
        "once": false,
        "onceDelay": 0.1,
        "topic": "",
        "payload": "",
        "payloadType": "date",
        "x": 610,
        "y": 240,
        "wires": [
            [
                "8157f92c.9324e"
            ]
        ]
    },
    {
        "id": "49b44f5a.132568",
        "type": "change",
        "z": "29662af6.96a146",
        "name": "",
        "rules": [
            {
                "t": "set",
                "p": "gameState",
                "pt": "flow",
                "to": "playing",
                "tot": "str"
            }
        ],
        "action": "",
        "property": "",
        "from": "",
        "to": "",
        "reg": false,
        "x": 380,
        "y": 240,
        "wires": [
            []
        ]
    },
    {
        "id": "8157f92c.9324e",
        "type": "change",
        "z": "29662af6.96a146",
        "name": "",
        "rules": [
            {
                "t": "set",
                "p": "gameState",
                "pt": "flow",
                "to": "stopped",
                "tot": "str"
            }
        ],
        "action": "",
        "property": "",
        "from": "",
        "to": "",
        "reg": false,
        "x": 810,
        "y": 240,
        "wires": [
            []
        ]
    },
    {
        "id": "91ccf9a6.2af778",
        "type": "inject",
        "z": "29662af6.96a146",
        "name": "Reset",
        "props": [
            {
                "p": "payload"
            },
            {
                "p": "topic",
                "vt": "str"
            }
        ],
        "repeat": "",
        "crontab": "",
        "once": false,
        "onceDelay": 0.1,
        "topic": "",
        "payload": "",
        "payloadType": "date",
        "x": 1030,
        "y": 240,
        "wires": [
            [
                "b8b19971.5380a8"
            ]
        ]
    },
    {
        "id": "b8b19971.5380a8",
        "type": "change",
        "z": "29662af6.96a146",
        "name": "",
        "rules": [
            {
                "t": "set",
                "p": "timeRemaining",
                "pt": "flow",
                "to": "3600000",
                "tot": "str"
            }
        ],
        "action": "",
        "property": "",
        "from": "",
        "to": "",
        "reg": false,
        "x": 1240,
        "y": 240,
        "wires": [
            []
        ]
    },
    {
        "id": "bf54624f.1f05b8",
        "type": "ui_button",
        "z": "29662af6.96a146",
        "name": "",
        "group": "e28e9683.8bc2e8",
        "order": 4,
        "width": 2,
        "height": 2,
        "passthru": true,
        "label": "Start",
        "tooltip": "",
        "color": "",
        "bgcolor": "green",
        "icon": "",
        "payload": "start",
        "payloadType": "str",
        "topic": "TIMER",
        "x": 170,
        "y": 180,
        "wires": [
            [
                "49b44f5a.132568",
                "e070cc33.ebeea"
            ]
        ]
    },
    {
        "id": "cf24c6ee.702378",
        "type": "ui_button",
        "z": "29662af6.96a146",
        "name": "",
        "group": "e28e9683.8bc2e8",
        "order": 5,
        "width": 2,
        "height": 2,
        "passthru": true,
        "label": "Stop",
        "tooltip": "",
        "color": "",
        "bgcolor": "red",
        "icon": "",
        "payload": "stop",
        "payloadType": "str",
        "topic": "TIMER",
        "x": 610,
        "y": 180,
        "wires": [
            [
                "8157f92c.9324e",
                "b7ffebb9.b20958"
            ]
        ]
    },
    {
        "id": "47ca99f6.2d9bf",
        "type": "ui_button",
        "z": "29662af6.96a146",
        "name": "",
        "group": "e28e9683.8bc2e8",
        "order": 6,
        "width": "2",
        "height": 2,
        "passthru": false,
        "label": "<b>!!! Reset !!!</b>",
        "tooltip": "",
        "color": "",
        "bgcolor": "red",
        "icon": "",
        "payload": "reset",
        "payloadType": "str",
        "topic": "TIMER",
        "x": 980,
        "y": 180,
        "wires": [
            [
                "b8b19971.5380a8",
                "f03e9c77.072b48"
            ]
        ]
    },
    {
        "id": "f03e9c77.072b48",
        "type": "link out",
        "z": "29662af6.96a146",
        "name": "reset",
        "links": [
            "7105f2a1.21812c",
            "ad7b2357.d2e098",
            "c1443e15.6728c8",
            "ab7c57db.9ca698",
            "a6cd94c7.4d6bd",
            "b91d3509.17212",
            "66ce8743.b007a8",
            "f6e1656c.d7af6",
            "a7abf569.ae721",
            "2d230649.2da632"
        ],
        "x": 1155,
        "y": 180,
        "wires": []
    },
    {
        "id": "ad7b2357.d2e098",
        "type": "link in",
        "z": "29662af6.96a146",
        "name": "reset-stop",
        "links": [
            "f03e9c77.072b48",
            "6a781a22.d020c4"
        ],
        "x": 515,
        "y": 180,
        "wires": [
            [
                "cf24c6ee.702378"
            ]
        ]
    },
    {
        "id": "9b7fbc0.3a3a0c8",
        "type": "change",
        "z": "29662af6.96a146",
        "name": "",
        "rules": [
            {
                "t": "set",
                "p": "timeRemaining",
                "pt": "flow",
                "to": "timeRemaining",
                "tot": "msg"
            }
        ],
        "action": "",
        "property": "",
        "from": "",
        "to": "",
        "reg": false,
        "x": 620,
        "y": 300,
        "wires": [
            []
        ]
    },
    {
        "id": "a3531003.a731d",
        "type": "ui_button",
        "z": "29662af6.96a146",
        "name": "",
        "group": "e28e9683.8bc2e8",
        "order": 7,
        "width": 2,
        "height": "2",
        "passthru": false,
        "label": "+1 min",
        "tooltip": "",
        "color": "",
        "bgcolor": "",
        "icon": "",
        "payload": "+1 min",
        "payloadType": "str",
        "topic": "TIMER",
        "x": 170,
        "y": 300,
        "wires": [
            [
                "77f6da55.553bcc",
                "ec0c7f2f.eb6ab8"
            ]
        ]
    },
    {
        "id": "77f6da55.553bcc",
        "type": "function",
        "z": "29662af6.96a146",
        "name": "",
        "func": "var timeRemaining = flow.get(\"timeRemaining\") || 0;\nvar addOne = 60000\n\ntimeRemaining += addOne;\n    \nflow.set(\"timeRemaining\", timeRemaining);\n\nmsg.timeRemaining = timeRemaining;\n\nreturn msg;",
        "outputs": 1,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "x": 420,
        "y": 300,
        "wires": [
            [
                "9b7fbc0.3a3a0c8"
            ]
        ]
    },
    {
        "id": "d74ba333.870fc8",
        "type": "change",
        "z": "29662af6.96a146",
        "name": "",
        "rules": [
            {
                "t": "set",
                "p": "timeRemaining",
                "pt": "flow",
                "to": "timeRemaining",
                "tot": "msg"
            }
        ],
        "action": "",
        "property": "",
        "from": "",
        "to": "",
        "reg": false,
        "x": 1240,
        "y": 300,
        "wires": [
            []
        ]
    },
    {
        "id": "6873a943.66294",
        "type": "ui_button",
        "z": "29662af6.96a146",
        "name": "",
        "group": "e28e9683.8bc2e8",
        "order": 8,
        "width": 2,
        "height": "2",
        "passthru": false,
        "label": "+5 min",
        "tooltip": "",
        "color": "",
        "bgcolor": "",
        "icon": "",
        "payload": "+5 min",
        "payloadType": "str",
        "topic": "TIMER",
        "x": 810,
        "y": 300,
        "wires": [
            [
                "e69b3482.115b38",
                "16cc4509.c631bb"
            ]
        ]
    },
    {
        "id": "e69b3482.115b38",
        "type": "function",
        "z": "29662af6.96a146",
        "name": "",
        "func": "var timeRemaining = flow.get(\"timeRemaining\") || 0;\nvar addOne = 60000*5\n\ntimeRemaining += addOne;\n    \nflow.set(\"timeRemaining\", timeRemaining);\n\nmsg.timeRemaining = timeRemaining;\n\nreturn msg;",
        "outputs": 1,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "x": 1040,
        "y": 300,
        "wires": [
            [
                "d74ba333.870fc8"
            ]
        ]
    },
    {
        "id": "e070cc33.ebeea",
        "type": "link out",
        "z": "29662af6.96a146",
        "name": "start",
        "links": [
            "c1443e15.6728c8"
        ],
        "x": 295,
        "y": 180,
        "wires": []
    },
    {
        "id": "b7ffebb9.b20958",
        "type": "link out",
        "z": "29662af6.96a146",
        "name": "stop",
        "links": [
            "c1443e15.6728c8"
        ],
        "x": 735,
        "y": 180,
        "wires": []
    },
    {
        "id": "ec0c7f2f.eb6ab8",
        "type": "link out",
        "z": "29662af6.96a146",
        "name": "+1min",
        "links": [
            "c1443e15.6728c8"
        ],
        "x": 275,
        "y": 300,
        "wires": []
    },
    {
        "id": "16cc4509.c631bb",
        "type": "link out",
        "z": "29662af6.96a146",
        "name": "+5min",
        "links": [
            "c1443e15.6728c8"
        ],
        "x": 915,
        "y": 300,
        "wires": []
    },
    {
        "id": "e17ffd4e.433bf",
        "type": "change",
        "z": "29662af6.96a146",
        "name": "",
        "rules": [
            {
                "t": "set",
                "p": "timeRemaining",
                "pt": "flow",
                "to": "timeRemaining",
                "tot": "msg"
            }
        ],
        "action": "",
        "property": "",
        "from": "",
        "to": "",
        "reg": false,
        "x": 620,
        "y": 360,
        "wires": [
            []
        ]
    },
    {
        "id": "486e099f.3951b",
        "type": "ui_button",
        "z": "29662af6.96a146",
        "name": "",
        "group": "e28e9683.8bc2e8",
        "order": 10,
        "width": 2,
        "height": "2",
        "passthru": false,
        "label": "-1 min",
        "tooltip": "",
        "color": "",
        "bgcolor": "",
        "icon": "",
        "payload": "-1 min",
        "payloadType": "str",
        "topic": "TIMER",
        "x": 170,
        "y": 360,
        "wires": [
            [
                "af55c2c6.06b228",
                "ffd60e8e.f7d84"
            ]
        ]
    },
    {
        "id": "af55c2c6.06b228",
        "type": "function",
        "z": "29662af6.96a146",
        "name": "",
        "func": "var timeRemaining = flow.get(\"timeRemaining\") || 0;\nvar addOne = 60000\n\ntimeRemaining -= addOne;\n    \nflow.set(\"timeRemaining\", timeRemaining);\n\nmsg.timeRemaining = timeRemaining;\n\nreturn msg;",
        "outputs": 1,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "x": 420,
        "y": 360,
        "wires": [
            [
                "e17ffd4e.433bf"
            ]
        ]
    },
    {
        "id": "374eb5e7.9cc7a2",
        "type": "change",
        "z": "29662af6.96a146",
        "name": "",
        "rules": [
            {
                "t": "set",
                "p": "timeRemaining",
                "pt": "flow",
                "to": "timeRemaining",
                "tot": "msg"
            }
        ],
        "action": "",
        "property": "",
        "from": "",
        "to": "",
        "reg": false,
        "x": 1240,
        "y": 360,
        "wires": [
            []
        ]
    },
    {
        "id": "24347276.8a2706",
        "type": "ui_button",
        "z": "29662af6.96a146",
        "name": "",
        "group": "e28e9683.8bc2e8",
        "order": 11,
        "width": 2,
        "height": "2",
        "passthru": false,
        "label": "-5 min",
        "tooltip": "",
        "color": "",
        "bgcolor": "",
        "icon": "",
        "payload": "-5 min",
        "payloadType": "str",
        "topic": "TIMER",
        "x": 810,
        "y": 360,
        "wires": [
            [
                "dee13c37.ab0b",
                "60a4437.54128bc"
            ]
        ]
    },
    {
        "id": "dee13c37.ab0b",
        "type": "function",
        "z": "29662af6.96a146",
        "name": "",
        "func": "var timeRemaining = flow.get(\"timeRemaining\") || 0;\nvar addOne = 60000*5\n\ntimeRemaining -= addOne;\n    \nflow.set(\"timeRemaining\", timeRemaining);\n\nmsg.timeRemaining = timeRemaining;\n\nreturn msg;",
        "outputs": 1,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "x": 1040,
        "y": 360,
        "wires": [
            [
                "374eb5e7.9cc7a2"
            ]
        ]
    },
    {
        "id": "ffd60e8e.f7d84",
        "type": "link out",
        "z": "29662af6.96a146",
        "name": "-1min",
        "links": [
            "c1443e15.6728c8"
        ],
        "x": 275,
        "y": 360,
        "wires": []
    },
    {
        "id": "60a4437.54128bc",
        "type": "link out",
        "z": "29662af6.96a146",
        "name": "-5min",
        "links": [
            "c1443e15.6728c8"
        ],
        "x": 915,
        "y": 360,
        "wires": []
    },
    {
        "id": "cc5aea0b.1742e",
        "type": "link out",
        "z": "29662af6.96a146",
        "name": "",
        "links": [],
        "x": 495,
        "y": 100,
        "wires": []
    },
    {
        "id": "a1304d26.6c6b98",
        "type": "inject",
        "z": "29662af6.96a146",
        "name": "Loop",
        "props": [
            {
                "p": "payload"
            }
        ],
        "repeat": "1",
        "crontab": "",
        "once": true,
        "onceDelay": 0.1,
        "topic": "",
        "payload": "",
        "payloadType": "date",
        "x": 170,
        "y": 480,
        "wires": [
            [
                "f8f9c3af.4996c8"
            ]
        ]
    },
    {
        "id": "f8f9c3af.4996c8",
        "type": "function",
        "z": "29662af6.96a146",
        "name": "",
        "func": "var currentTime = msg.payload;\nvar lastTickTime = flow.get(\"lastTickTime\") || currentTime;\nvar timeRemaining = flow.get(\"timeRemaining\") || 0;\nvar gameState = flow.get(\"gameState\");\n\nif(gameState == \"playing\") {\n\n    timeRemaining -= (currentTime - lastTickTime);\n    \n    flow.set(\"timeRemaining\", timeRemaining);\n    \n}\n\nmsg.timeRemaining = timeRemaining;\nflow.set(\"lastTickTime\", currentTime);\n\nnode.status({fill:\"green\", shape:\"dot\", text:timeRemaining});\n\n\nreturn msg;",
        "outputs": 1,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "x": 340,
        "y": 480,
        "wires": [
            [
                "c7146f2b.9b30e"
            ]
        ]
    },
    {
        "id": "c7146f2b.9b30e",
        "type": "function",
        "z": "29662af6.96a146",
        "name": "Format time",
        "func": "var t = msg.timeRemaining / 1000;\nvar m = Math.floor(t / 60);\nvar s = Math.floor(t%60 % 60);\n\nmsg.formattedTimeRemaining = (\"0\" + m).slice(-2) + \":\" + (\"0\" + s).slice(-2);\n\nnode.status({fill:\"green\", shape:\"dot\", msg:flow.formattedTimeRemaining});\n\n\nreturn msg;",
        "outputs": 1,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "x": 530,
        "y": 480,
        "wires": [
            [
                "5970c5ac.84e9fc"
            ]
        ]
    },
    {
        "id": "5970c5ac.84e9fc",
        "type": "join",
        "z": "29662af6.96a146",
        "name": "",
        "mode": "custom",
        "build": "merged",
        "property": "",
        "propertyType": "full",
        "key": "topic",
        "joiner": "\\n",
        "joinerType": "str",
        "accumulate": true,
        "timeout": "",
        "count": "1",
        "reduceRight": false,
        "reduceExp": "",
        "reduceInit": "",
        "reduceInitType": "num",
        "reduceFixup": "",
        "x": 1090,
        "y": 480,
        "wires": [
            [
                "3e91b62d.cfd2e2"
            ]
        ]
    },
    {
        "id": "3e91b62d.cfd2e2",
        "type": "uibuilder",
        "z": "29662af6.96a146",
        "name": "Game Timer and Clue Display",
        "topic": "",
        "url": "atlantis",
        "fwdInMessages": false,
        "allowScripts": false,
        "allowStyles": false,
        "copyIndex": true,
        "showfolder": false,
        "x": 1300,
        "y": 480,
        "wires": [
            [],
            []
        ]
    },
    {
        "id": "e28e9683.8bc2e8",
        "type": "ui_group",
        "z": "",
        "name": "Timer",
        "tab": "759f7fcb.6ad23",
        "order": 1,
        "disp": false,
        "width": "8",
        "collapse": false
    },
    {
        "id": "759f7fcb.6ad23",
        "type": "ui_tab",
        "z": "",
        "name": "Atlantis",
        "icon": "dashboard",
        "disabled": false,
        "hidden": false
    }
]