TFMv2 - Format time with ease & flexibility

Hey y’all,

Exactly a year ago (yes to the minute), I released TFM to strictly format time into a handful of formats. When I look back upon it, the module is quite limited and forced you choose the exact format it provided, which is not user friendly. Ever since then, my programming skills improved dramatically and therefore, I rewrote the whole module from ground-up to be evermore flexible.

Inspired by @goldenstein64’s version of TFM, I present…

:floppy_disk: Source Code | :inbox_tray: Download | :package: Model

If you want, you can also check out my tweet.


The first label is a basic countdown utilizing seconds only and stops at 0. The second one is a stopwatch with milliseconds and runs indefinitely. Finally, the last is a sentence which has time embedded into it with correct grammatical singular-plural rules. As an example, it mimics a cooldown message in the form of “Try again in x minute(s) and y second(s)” except that instead of using (s) to account for potential plurals, it automatically appends an s when necessary!

This demo GUI can be found in the GitHub repository and the .rbxmx file as well. If you have any creations you want to share, please do so! It’s heartwarming to see your creation being used.

You may notice the stopwatch seconds slowly falling off-sync with the other two. This is since the code is very basic and is focusing more on showcasing formatting than the timer itself.


There are 3 ways to install the module. Method 1 is recommended for beginners (it only has the module script itself). If you want to fiddle with the test script and the GUI accompanying it, Method 2 is the best. Otherwise, use Method 3 without the GUI, but with the project file defining it (this is recommended for Rojo users).

Method 1: Roblox Model
  1. Go to the module’s published page (same as the “Model” link above) and click “Get.”
  2. In studio, open the Toolbox, head to My Models and click to insert TFMv2 into your place.
  3. You may move it to a location like ReplicatedStorage.
Method 2: Model File (.rbxmx)
  1. Download the model file (same as the “Download” link above and also found on the GitHub Releases Page).
  2. Right click any location in the Explorer and select Insert from file to upload the file directly.
  3. From the folder, move the module script into ReplicatedStorage. The local script and ScreenGui should both go in StarterGui for them to work.
Method 3: Filesystem
  1. Download the GitHub repository (same as the “Source Code” link above).
  2. Copy the src directory and merge it with your own src folder.
  3. Grab default.project.json from the main directory and merge it with your project file. It will place the scripts in their intended locations and will create the GUI for the test script.
  4. Use a plugin like Rojo to sync the files into a place.

See also: :wrench: Array Modifier Plugin | :books: ListLib

How to Use

If you want a tutorial-type view of how to use this module, here’s the guide:


As said in the above section, TFMv2 takes in seconds or milliseconds and spits out a table of sorted units. You can expect the table (specifically a dictionary) to look like this:


    yr: int,
    mon: int,
    day: int,
    hr: int,
    min: int,
    sec: int,
    ms: int? -- ms only outputted when you want to convert ms


Most times, you just want to convert a number of seconds. For that, use the Convert() function which receives the number of seconds and an optional “max unit” parameters. (All functions part of TFMv2 are static, meaning they are not methods. Call them using the . not :).

TFMv2.Convert(sec: int, max: string = 'hr') -> table

Note: this will not include the ms key since milliseconds are not used.

The max unit determines where to stop pushing to larger time units should the current one reach the max. For example, when you have 36 hours, it becomes 1 day and 12 hours (e.g. 1:12:00:00). But, if you have a max unit parameter set to hr, it leaves it as 36 hours (e.g. (36:00:00)):

TFMv2.Convert(36 * 3600) -- default max unit is 'hr'

--[[ prints

    yr = 0,
    mon = 0,
    day = 0,
    hr = 36,
    min = 0,
    sec = 0


Now that’s great. However sometimes, as with timers, you want to include milliseconds as well. That’s as simple as using the ConvertMil() function which expects the number of milliseconds as the first parameter:

TFMv2.ConvertMil(ms: int, max: string = 'hr') -> table

This time, in the table returned, you will have the ms key.

Beware though! Roblox’s engine supports a minimum yield of 0.03 seconds and even that can change! Therefore, take a quick look at the test script provided in the Source Code under the “Stopwatch” section. The trick is to use the first return value of wait(), which is exactly how long it yielded.

Milliseconds in action:

TFMv2.ConvertMil(1000 * (69) + 699)

--[[ prints

    yr = 0,
    mon = 0,
    day = 0,
    hr = 0,
    min = 1,
    sec = 9,
    ms = 699


Alright that’s all the converting functions. There are two left now: SetSeconds() and FormatStr(); the latter will be talked about in the Formatting Guide below this section.

The units after day become a bit blurred. Do you define a year as 12 months or 365 days? Well that depends on the month. By default, TFMv2 sets a month to exactly 30 days because this is a formatting module and does not complicate things with 30/31-day month, leap month, and all that mess. Also by default, TFMv2 defines a year as 365 days. This means that a year will be considered as 12 and one-third months. If you don’t plan on using the year unit at all, then this doesn’t impact you. However, if you do, then you have two choices: define a year as 365 days or 12 months.

Luckily, TFMv2 allows you to define these using the SetSeconds() function!

TFMv2.SetSeconds(def: table) -> void

This allows you to define months and years only (the other units are always what they are) in terms of seconds. The parameter accepts a dictionary with the keys you want to change. It will be merged with the existing dictionary, so any keys not given in the parameter dictionary will stay the same in the main one.

The keys must be in the format SECS_UNIT. Thus, you can only input keys SECS_YR and SECS_MON into the dictionary:

-- make a year exactly 12 months, but a month approximately 30.4 days.
    SECS_MON = 3600 * 24 * (365 / 12) -- use multiplication for coming up with number of seconds

-- makes a year exactly 12 months, keeps months 30 days long, but a year is now 360 days
    SECS_YR = 3600 * 24 * 30 * 12

Set them seconds as you see fit for your project! That concludes the main guide. To learn about FormatStr(), continue to the Formatting Guide below!

Formatting Guide

The module wouldn’t be called a Time Formatting Module without any formatting, would it? The previous TFM module already formatted the string internally based on a given mode (“colon” for mm:ss, etc.), which was too strict. In this one, you can use the FormatStr() function to do so using a format string:

TFMv2.FormatStr(converted: table, template: string) -> string

As the first parameter, you input the same table returned by Convert() and ConvertMil(). It is done this way instead of directly inputting the seconds/milliseconds into the format function because you can also create a dummy converted table to test out formatting. A dummy is simple you creating a dictionary manually. Any keys omitted will not appear after formatting.

Now, onto how to format your template string. Before continuing, make sure you have brushed up on Formatting Lua Strings.

Anyways, each unit has its own format class:

  • %y - year
  • %M - month
  • %d - day
  • %h - hour
  • %m - minute
  • %S - second
  • %s - millisecond

Recall that the formatting format is:


The specifier can only be one of the custom classes bulleted above. The flags and width can be anything you wish; .precision is also allowed, but it mostly doesn’t need to be used for TFMv2. Our main two are the flags and width, which useful for left-padding parts of a string with two or more 0s.

Left Pad 0s

Remember how you need to include 0s in front of single-digit numbers if they’re not the first unit (e.g. 3:06 not 3:6)? Well, the flag 0 can help with that, which left-pads the string with the given width number of 0s:

-- normal string formatting
string.format('%02d', 5) --> 05
string.format('%03d', 7) --> 007 -- James Bond!
string.format('%02d', 10) --> 10

-- TFMv2
TFMv2.FormatStr({ -- dummy table in use
    hr = 1,
    min = 15,
    sec = 6
    ms = 33
}, '%h:%02m:%02S.%03s') --> 1:15:06.033

Note: milliseconds are often padding with three 0s because the unit ranges from 0-999. Also, you have a choice of whether to leave the first unit as it is or left-pad that as well (e.g. 01:15 or 1:15, the latter is more common).

Auto-Clear Nonexistent Units

Just so that no ugly formatting parts make it to the end product, TFMv2 automatically clears up specifiers whose units are not given in the converted table (simply replace that with '' a blank string). For example:

    sec = 2
}, '%S.%03s') -- 2.

Since no milliseconds exist in the table, the %03s is removed. Now obviously, we don’t want that period chilling there for no reason, but don’t worry, there is an easy way to fix it (see “Include Characters If Unit Exists”).

Handle Singulars and Plurals

Say you want to write out a sentence, e.g. there are x seconds left. You can simply do "there are %S seconds left", but at exactly 1 second, the sentence grammatically doesn’t make sense. Therefore, there is custom formatting syntax specially designed to handle this:


Hold on, that look scary. Let’s break it down. The specifier, remember, is something like %h or %s, etc. (Here, you cannot have flags, width, or .precision like typical formatting). Then, you have a pair of parentheses incasing the rest of the syntax. Inside them, you put the string that you want to display when the unit is singular (exactly 1) and when it’s plural, in that order. They can be blank as well. Finally, to separate the two, use a control character (a “nothing” character, if you will, that doesn’t add a character, but exists as a signal, in our case, to separate the singular and plural strings). You can choose any control character, but I would recommend \1 since it’s the easiest.

You can read out this syntax as: if the specifier unit equals 1, the singular string shall be used, otherwise, the plural one will take precedence.

“O fellow Carbyne, why must thou use a control character, can’t thou simply just use a slash?” Well, due to how string patterns work, the singular and plural strings cannot be the class that the separator is. In this case, it can be anything but a control character. Had it been a slash, you would not be able to use a slash in your singular or plural strings. To provide this flexibility, control characters are used. You shouldn’t be using them anyways especially if you plan to display the string in any form (text, print, etc.), so restricting them is alright.

Anyways, here are a few examples to demonstrate this syntax:

-- simple "s" plural (notice empty singular string before \1, meaning if singular, just don't add anything)
    min = 1
    sec = 10
}, '%m minute%m(\1s) and %S second%S(\1s) left!') -- 1 minute and 10 seconds left

-- "is-are" plural
    sec = 10
}, 'there %S(is\1are) %S second%S(\1s) left!') -- there are 10 seconds left!

    sec = 1
}, 'there %S(is\1are) %S second%S(\1s) left!') -- there is 1 second left!

It’s not difficult once you start noticing \1 (or any control character of your choice) is the separator.

Auto-clearing works for this syntax as well, which brings us onto the last feature of formatting:

Include Characters if Unit Exists

Believe it or not, there is no new syntax. It’s using the singular-plural syntax and taking advantage of auto-clearing if a unit doesn’t exist. You have to provide the same exact string for both singular and plural; here is the same example with the remaining period from the “auto-clearing” section:

   sec = 10
}, '%S%s(.\1.)%03s') --> 10

   sec = 10
   ms = 219
}, '%S%s(.\1.)%03s') --> 10.219

Notice on the left and right of \1, there is a period.

This may all look very overwhelming with all the percent symbols, forward slashes, parentheses all in an ugly mess, but once you break down the string into separate elements, it’s actually quite simple. If you have any trouble with this custom formatting, do not hesitate to ask as a reply!

Otherwise, here’s the straight up API reference:

API Reference

All functions are static, meaning they should not be called with :, but rather . only.


TFMv2.Convert(sec: int, max: string = 'hr') -> table

Receives the number of seconds to convert and an optional max unit parameter where the given unit is NOT allowed to overflow (e.g. setting this to ‘day’ will keep 31 days as it is, whereas setting it to anything higher will overflow it to the next unit, with the result being 1 month and 1 day). A dictionary is returned with the following keys:

  • yr
  • mon
  • day
  • hr
  • min
  • sec


TFMv2.ConvertMil(ms: int, max: string = 'hr') -> table

Similar to Convert, except it expects the first parameter to be in milliseconds. Returns a dictionary as well, but with an added key of ms.


TFMv2.FormatStr(converted: table, template: string) -> string

Receives the converted table (returned from Convert, ConvertedMil, or simply a dummy) along with a template string to format; the result is returned. The specifier for the units are:

  • %y - year
  • %M - month
  • %d - day
  • %h - hour
  • %m - minute
  • %S - second
  • %s - millisecond

For an in-depth explanation on formatting, take a look at the Formatting Guide.


TFMv2.SetSeconds(def: table) -> void

Receives a dictionary with the following keys to define that corresponding unit in seconds for conversion:


By default, TFMv2 defines a month as exactly 30 days and a year exactly as 365 days. This leaves a loophole of a year being 12 1/3 months. You can tweak this and redirect the flaw to something else (e.g. set a month as 365/12 days of a year and leave a year as 365 days - this will mean 12 months make up a year with the downside of a month being 30.4 days):

-- use multiplication for coming up with number of seconds
    SECS_MON = 3600 * 24 * (365 / 12)

This does not need to be used if you are sticking to small units, which is most cases.


Just like how you can use SetSeconds, you an acquire their values via simply indexing the keys:


Feedback & Contributions

This is a very simple module so far, so if you want to contribute, please do so! If you want to tweak it for personal use, feel free to fork the repository. I am also open to pull requests.

Otherwise, simple feedback is also very appreciated. Is something inconvenient to use? Is the formatting too strict? Reply here and I will respond as soon as possible!

Also, consider voting on this poll:

How was your experience with TFMv2 so far?
1 = absolutely terrible, 10 = absolutely amazing

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

0 voters

Thank you very much,
Happy April Fools!


I love this! Thank you so much!!! it’s really useful