diff --git a/public/locales/en/time.json b/public/locales/en/time.json index 6bfb88e..1b12044 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 a formatted time duration (HH:MM:SS) into a decimal hour value.", + "shortDescription": "Convert human time to decimal time", + "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." } } 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..889bb81 --- /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-light:decimal-increase-rounded', + 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..770ae18 --- /dev/null +++ b/src/pages/tools/time/convert-time-to-decimal/service.ts @@ -0,0 +1,37 @@ +import { InitialValuesType } from './types'; +import { humanTimeValidation } from 'utils/time'; + +export function convertTimeToDecimal( + input: string, + options: InitialValuesType +): string { + if (!input) return ''; + + const dp = parseInt(options.decimalPlaces, 10); + if (isNaN(dp) || dp < 0) { + return 'Invalid decimal places value.'; + } + + // Multiple lines processing + const lines = input.split('\n'); + if (!lines) return ''; + + const result: string[] = []; + + lines.forEach((line) => { + line = line.trim(); + if (!line) return; + + const { isValid, hours, minutes, seconds } = humanTimeValidation(line); + + 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'); +} 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 ]; 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 }; +}