From b1f63a320779c3098fd535d3d967dfaeaeef4b99 Mon Sep 17 00:00:00 2001 From: Mike Bricknell-Barlow Date: Tue, 2 Dec 2025 12:52:52 +0000 Subject: [PATCH 1/5] Add tool to convert human time to decimal time --- public/locales/en/time.json | 6 ++ .../convert-time-to-decimal.service.test.ts | 40 +++++++++++ .../time/convert-time-to-decimal/index.tsx | 72 +++++++++++++++++++ .../time/convert-time-to-decimal/meta.ts | 15 ++++ .../time/convert-time-to-decimal/service.ts | 24 +++++++ .../time/convert-time-to-decimal/types.ts | 3 + src/pages/tools/time/index.ts | 4 +- 7 files changed, 163 insertions(+), 1 deletion(-) create mode 100644 src/pages/tools/time/convert-time-to-decimal/convert-time-to-decimal.service.test.ts create mode 100644 src/pages/tools/time/convert-time-to-decimal/index.tsx create mode 100644 src/pages/tools/time/convert-time-to-decimal/meta.ts create mode 100644 src/pages/tools/time/convert-time-to-decimal/service.ts create mode 100644 src/pages/tools/time/convert-time-to-decimal/types.ts diff --git a/public/locales/en/time.json b/public/locales/en/time.json index 6bfb88e..9398b45 100644 --- a/public/locales/en/time.json +++ b/public/locales/en/time.json @@ -113,5 +113,11 @@ "zeroPaddingDescription": "Make all time components always be two digits wide.", "zeroPrintDescription": "Display the dropped parts as zero values \"00\".", "zeroPrintTruncatedParts": "Zero-print Truncated Parts" + }, + "convertTimeToDecimal": { + "title": "Convert time to decimal", + "description": "Convert formatted length of time (HH:MM:SS) to decimal format, e.g. 3.43 hours.", + "shortDescription": "Convert human time to decimal time", + "longDescription": "Convert formatted length of time (HH:MM:SS) to decimal format, e.g. 3.43 hours." } } diff --git a/src/pages/tools/time/convert-time-to-decimal/convert-time-to-decimal.service.test.ts b/src/pages/tools/time/convert-time-to-decimal/convert-time-to-decimal.service.test.ts new file mode 100644 index 0000000..9ca6839 --- /dev/null +++ b/src/pages/tools/time/convert-time-to-decimal/convert-time-to-decimal.service.test.ts @@ -0,0 +1,40 @@ +import { expect, describe, it } from 'vitest'; +import { convertTimeToDecimal } from './service'; + +describe('convert-time-to-decimal', () => { + it('should convert time to decimal with default decimal places', () => { + const input = '31:23:59'; + const result = convertTimeToDecimal(input, { decimalPlaces: '6' }); + expect(result).toBe('31.399722'); + }); + + it('should convert time to decimal with specified decimal places', () => { + const input = '31:23:59'; + const result = convertTimeToDecimal(input, { decimalPlaces: '10' }); + expect(result).toBe('31.3997222222'); + }); + + it('should convert time to decimal with supplied format of HH:MM:SS', () => { + const input = '13:25:30'; + const result = convertTimeToDecimal(input, { decimalPlaces: '6' }); + expect(result).toBe('13.425000'); + }); + + it('should convert time to decimal with supplied format of HH:MM', () => { + const input = '13:25'; + const result = convertTimeToDecimal(input, { decimalPlaces: '6' }); + expect(result).toBe('13.416667'); + }); + + it('should convert time to decimal with supplied format of HH:MM:', () => { + const input = '13:25'; + const result = convertTimeToDecimal(input, { decimalPlaces: '6' }); + expect(result).toBe('13.416667'); + }); + + it('should convert time to decimal with supplied format of HH.MM.SS', () => { + const input = '13.25.30'; + const result = convertTimeToDecimal(input, { decimalPlaces: '6' }); + expect(result).toBe('13.425000'); + }); +}); diff --git a/src/pages/tools/time/convert-time-to-decimal/index.tsx b/src/pages/tools/time/convert-time-to-decimal/index.tsx new file mode 100644 index 0000000..1e1c567 --- /dev/null +++ b/src/pages/tools/time/convert-time-to-decimal/index.tsx @@ -0,0 +1,72 @@ +import { Box } from '@mui/material'; +import React, { useState } from 'react'; +import ToolContent from '@components/ToolContent'; +import { ToolComponentProps } from '@tools/defineTool'; +import ToolTextInput from '@components/input/ToolTextInput'; +import ToolTextResult from '@components/result/ToolTextResult'; +import { GetGroupsType } from '@components/options/ToolOptions'; +import { CardExampleType } from '@components/examples/ToolExamples'; +import { convertTimeToDecimal } from './service'; +import { InitialValuesType } from './types'; +import TextFieldWithDesc from '@components/options/TextFieldWithDesc'; + +const initialValues: InitialValuesType = { + decimalPlaces: '6' +}; + +const exampleCards: CardExampleType[] = [ + { + title: 'Convert time to decimal', + description: + 'This example shows how to convert a formatted time (HH:MM:SS) to a decimal version.', + sampleText: '31:23:59', + sampleResult: `31.399722`, + sampleOptions: { + decimalPlaces: '6' + } + } +]; +export default function ConvertTimeToDecimal({ + title, + longDescription +}: ToolComponentProps) { + const [input, setInput] = useState(''); + const [result, setResult] = useState(''); + + const compute = (values: InitialValuesType, input: string) => { + setResult(convertTimeToDecimal(input, values)); + }; + + const getGroups: GetGroupsType | null = ({ + values, + updateField + }) => [ + { + title: 'Decimal places', + component: ( + + updateField('decimalPlaces', val)} + type={'text'} + /> + + ) + } + ]; + return ( + } + resultComponent={} + initialValues={initialValues} + exampleCards={exampleCards} + getGroups={getGroups} + setInput={setInput} + compute={compute} + toolInfo={{ title: `What is a ${title}?`, description: longDescription }} + /> + ); +} diff --git a/src/pages/tools/time/convert-time-to-decimal/meta.ts b/src/pages/tools/time/convert-time-to-decimal/meta.ts new file mode 100644 index 0000000..f7f8624 --- /dev/null +++ b/src/pages/tools/time/convert-time-to-decimal/meta.ts @@ -0,0 +1,15 @@ +import { defineTool } from '@tools/defineTool'; +import { lazy } from 'react'; + +export const tool = defineTool('time', { + i18n: { + name: 'time:convertTimeToDecimal.title', + description: 'time:convertTimeToDecimal.description', + shortDescription: 'time:convertTimeToDecimal.shortDescription', + longDescription: 'time:convertTimeToDecimal.longDescription' + }, + path: 'convert-time-to-decimal', + icon: 'material-symbols:schedule', + keywords: ['convert', 'time', 'to', 'decimal'], + component: lazy(() => import('./index')) +}); diff --git a/src/pages/tools/time/convert-time-to-decimal/service.ts b/src/pages/tools/time/convert-time-to-decimal/service.ts new file mode 100644 index 0000000..f7b8dab --- /dev/null +++ b/src/pages/tools/time/convert-time-to-decimal/service.ts @@ -0,0 +1,24 @@ +import { InitialValuesType } from './types'; + +export function convertTimeToDecimal( + input: string, + options: InitialValuesType +): string { + if (!input || (!input.includes(':') && !input.includes('.'))) { + return ''; + } + + let splitTime = input.split(/[.:]/); + + let hours = parseInt(splitTime[0]); + let minutes = parseInt(splitTime[1]); + let seconds = splitTime[2] ? parseInt(splitTime[2]) : 0; + + let decimalTime = hours + minutes / 60; + + if (seconds !== 0) { + decimalTime += seconds / 3600; + } + + return decimalTime.toFixed(parseInt(options.decimalPlaces)).toString(); +} diff --git a/src/pages/tools/time/convert-time-to-decimal/types.ts b/src/pages/tools/time/convert-time-to-decimal/types.ts new file mode 100644 index 0000000..f4bf30a --- /dev/null +++ b/src/pages/tools/time/convert-time-to-decimal/types.ts @@ -0,0 +1,3 @@ +export type InitialValuesType = { + decimalPlaces: string; +}; diff --git a/src/pages/tools/time/index.ts b/src/pages/tools/time/index.ts index a98d27e..fbbcff4 100644 --- a/src/pages/tools/time/index.ts +++ b/src/pages/tools/time/index.ts @@ -1,3 +1,4 @@ +import { tool as timeConvertTimeToDecimal } from './convert-time-to-decimal/meta'; import { tool as timeConvertUnixToDate } from './convert-unix-to-date/meta'; import { tool as timeCrontabGuru } from './crontab-guru/meta'; import { tool as timeBetweenDates } from './time-between-dates/meta'; @@ -17,5 +18,6 @@ export const timeTools = [ timeBetweenDates, timeCrontabGuru, checkLeapYear, - timeConvertUnixToDate + timeConvertUnixToDate, + timeConvertTimeToDecimal ]; From 9d25c37f4bf9252fe8db4bdf943606b90f68806f Mon Sep 17 00:00:00 2001 From: Chesterkxng Date: Fri, 5 Dec 2025 17:59:23 +0100 Subject: [PATCH 2/5] chore: - time util file created - time validation util function implemented --- src/utils/time.ts | 58 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 src/utils/time.ts diff --git a/src/utils/time.ts b/src/utils/time.ts new file mode 100644 index 0000000..e90782d --- /dev/null +++ b/src/utils/time.ts @@ -0,0 +1,58 @@ +type TimeValidationResult = { + isValid: boolean; + hours: number; + minutes: number; + seconds: number; +}; + +/** + * Validates human-readable time format (HH:MM or HH:MM:SS) + * Supports either ':' or '.' as a separator, but not both + * @param {string} input - string time format + * * @returns {{ + * isValid: boolean, // true if the input is a valid time + * hours: number, // parsed hours (0 or greater) + * minutes: number, // parsed minutes (0-59) + * seconds: number // parsed seconds (0-59, 0 if not provided) + * }} + */ +export function humanTimeValidation(input: string): TimeValidationResult { + const result = { isValid: false, hours: 0, minutes: 0, seconds: 0 }; + + if (!input) return result; + + input = input.trim(); + + // Operator use validation + // use of one between these two operators '.' or ':' + + const hasColon = input.includes(':'); + const hasDot = input.includes('.'); + + if (hasColon && hasDot) return result; + + if (!hasColon && !hasDot) return result; + + const separator = hasColon ? ':' : '.'; + + // Time parts validation + + const parts = input.split(separator); + + if (parts.length < 2 || parts.length > 3) return result; + + const [h, m, s = '0'] = parts; + + // every character should be a digit + if (![h, m, s].every((x) => /^\d+$/.test(x))) return result; + + const hours = parseInt(h); + const minutes = parseInt(m); + const seconds = parseInt(s); + + if (minutes < 0 || minutes > 59) return result; + if (seconds < 0 || seconds > 59) return result; + if (hours < 0) return result; + + return { isValid: true, hours, minutes, seconds }; +} From e67612424d4c804545a78f063e30d4e60c13c489 Mon Sep 17 00:00:00 2001 From: Chesterkxng Date: Fri, 5 Dec 2025 18:01:07 +0100 Subject: [PATCH 3/5] style: icon change --- src/pages/tools/time/convert-time-to-decimal/meta.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/tools/time/convert-time-to-decimal/meta.ts b/src/pages/tools/time/convert-time-to-decimal/meta.ts index f7f8624..889bb81 100644 --- a/src/pages/tools/time/convert-time-to-decimal/meta.ts +++ b/src/pages/tools/time/convert-time-to-decimal/meta.ts @@ -9,7 +9,7 @@ export const tool = defineTool('time', { longDescription: 'time:convertTimeToDecimal.longDescription' }, path: 'convert-time-to-decimal', - icon: 'material-symbols:schedule', + icon: 'material-symbols-light:decimal-increase-rounded', keywords: ['convert', 'time', 'to', 'decimal'], component: lazy(() => import('./index')) }); From 55354e3cd7fa7ec83419bfdbb973f9d4d10ead0e Mon Sep 17 00:00:00 2001 From: Chesterkxng Date: Fri, 5 Dec 2025 18:01:30 +0100 Subject: [PATCH 4/5] locales: enhanced tool description --- public/locales/en/time.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/locales/en/time.json b/public/locales/en/time.json index 9398b45..1b12044 100644 --- a/public/locales/en/time.json +++ b/public/locales/en/time.json @@ -116,8 +116,8 @@ }, "convertTimeToDecimal": { "title": "Convert time to decimal", - "description": "Convert formatted length of time (HH:MM:SS) to decimal format, e.g. 3.43 hours.", + "description": "Convert a formatted time duration (HH:MM:SS) into a decimal hour value.", "shortDescription": "Convert human time to decimal time", - "longDescription": "Convert formatted length of time (HH:MM:SS) to decimal format, e.g. 3.43 hours." + "longDescription": "Convert a formatted time string (HH:MM:SS or HH:MM) into its decimal-hour equivalent. Hours can be any positive number, while minutes and seconds accept values from 0-59 and can be single or double digits (e.g., '12:5' or '12:05'). This function interprets hours, minutes, and seconds, then calculates the total duration as a single decimal value. It is useful for productivity tracking, payroll calculations, time-based billing, data analysis, or any workflow that requires converting human-readable time into a numerical format that can be easily summed, compared, or processed. For example, '03:26:00' becomes 3.43 hours, and '12:5' becomes 12.08 hours." } } From d9917c6b3e5ca87846c8765f22f31fc9f830b42d Mon Sep 17 00:00:00 2001 From: Chesterkxng Date: Fri, 5 Dec 2025 18:03:55 +0100 Subject: [PATCH 5/5] chore: - validations added for edge cases handling - Multiple lines processing added --- .../time/convert-time-to-decimal/service.ts | 35 +++++++++++++------ 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/src/pages/tools/time/convert-time-to-decimal/service.ts b/src/pages/tools/time/convert-time-to-decimal/service.ts index f7b8dab..770ae18 100644 --- a/src/pages/tools/time/convert-time-to-decimal/service.ts +++ b/src/pages/tools/time/convert-time-to-decimal/service.ts @@ -1,24 +1,37 @@ import { InitialValuesType } from './types'; +import { humanTimeValidation } from 'utils/time'; export function convertTimeToDecimal( input: string, options: InitialValuesType ): string { - if (!input || (!input.includes(':') && !input.includes('.'))) { - return ''; + if (!input) return ''; + + const dp = parseInt(options.decimalPlaces, 10); + if (isNaN(dp) || dp < 0) { + return 'Invalid decimal places value.'; } - let splitTime = input.split(/[.:]/); + // Multiple lines processing + const lines = input.split('\n'); + if (!lines) return ''; - let hours = parseInt(splitTime[0]); - let minutes = parseInt(splitTime[1]); - let seconds = splitTime[2] ? parseInt(splitTime[2]) : 0; + const result: string[] = []; - let decimalTime = hours + minutes / 60; + lines.forEach((line) => { + line = line.trim(); + if (!line) return; - if (seconds !== 0) { - decimalTime += seconds / 3600; - } + const { isValid, hours, minutes, seconds } = humanTimeValidation(line); - return decimalTime.toFixed(parseInt(options.decimalPlaces)).toString(); + if (!isValid) { + result.push('Incorrect input format use `HH:MM:(SS)` or `HH.MM.(SS )`.'); + return; + } + + const decimalTime = hours + minutes / 60 + seconds / 3600; + result.push(decimalTime.toFixed(dp).toString()); + }); + + return result.join('\n'); }