added time between 2 dates #51
This commit is contained in:
Ibrahima G. Coulibaly
2025-03-27 20:40:19 +00:00
committed by GitHub
8 changed files with 644 additions and 124 deletions

259
.idea/workspace.xml generated
View File

@@ -4,9 +4,10 @@
<option name="autoReloadType" value="SELECTIVE" /> <option name="autoReloadType" value="SELECTIVE" />
</component> </component>
<component name="ChangeListManager"> <component name="ChangeListManager">
<list default="true" id="b30e2810-c4c1-4aad-b134-794e52cc1c7d" name="Changes" comment="chore: show new tools in landing"> <list default="true" id="b30e2810-c4c1-4aad-b134-794e52cc1c7d" name="Changes" comment="refactor: time between dates">
<change afterPath="$PROJECT_DIR$/src/pages/tools/time/time-between-dates/time-between-dates.service.test.ts" afterDir="false" />
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" /> <change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/components/allTools/ToolCard.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/src/components/allTools/ToolCard.tsx" afterDir="false" /> <change beforePath="$PROJECT_DIR$/src/pages/tools/time/time-between-dates/service.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/pages/tools/time/time-between-dates/service.ts" afterDir="false" />
</list> </list>
<option name="SHOW_DIALOG" value="false" /> <option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" /> <option name="HIGHLIGHT_CONFLICTS" value="true" />
@@ -23,7 +24,7 @@
<option name="PUSH_AUTO_UPDATE" value="true" /> <option name="PUSH_AUTO_UPDATE" value="true" />
<option name="RECENT_BRANCH_BY_REPOSITORY"> <option name="RECENT_BRANCH_BY_REPOSITORY">
<map> <map>
<entry key="$PROJECT_DIR$" value="chesterkxng" /> <entry key="$PROJECT_DIR$" value="main" />
</map> </map>
</option> </option>
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" /> <option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
@@ -42,59 +43,66 @@
&quot;state&quot;: &quot;OPEN&quot; &quot;state&quot;: &quot;OPEN&quot;
} }
}</component> }</component>
<component name="GitHubPullRequestState">{ <component name="GitHubPullRequestState"><![CDATA[{
&quot;prStates&quot;: [ "prStates": [
{ {
&quot;id&quot;: { "id": {
&quot;id&quot;: &quot;PR_kwDOMJIfts51PkS9&quot;, "id": "PR_kwDOMJIfts51PkS9",
&quot;number&quot;: 22 "number": 22
}, },
&quot;lastSeen&quot;: 1741207144695 "lastSeen": 1741207144695
}, },
{ {
&quot;id&quot;: { "id": {
&quot;id&quot;: &quot;PR_kwDOMJIfts6NiNYl&quot;, "id": "PR_kwDOMJIfts6NiNYl",
&quot;number&quot;: 32 "number": 32
}, },
&quot;lastSeen&quot;: 1741209723869 "lastSeen": 1741209723869
}, },
{ {
&quot;id&quot;: { "id": {
&quot;id&quot;: &quot;PR_kwDOMJIfts6Nheyd&quot;, "id": "PR_kwDOMJIfts6Nheyd",
&quot;number&quot;: 31 "number": 31
}, },
&quot;lastSeen&quot;: 1741213371410 "lastSeen": 1741213371410
}, },
{ {
&quot;id&quot;: { "id": {
&quot;id&quot;: &quot;PR_kwDOMJIfts6NmRBs&quot;, "id": "PR_kwDOMJIfts6NmRBs",
&quot;number&quot;: 33 "number": 33
}, },
&quot;lastSeen&quot;: 1741282429036 "lastSeen": 1741282429036
}, },
{ {
&quot;id&quot;: { "id": {
&quot;id&quot;: &quot;PR_kwDOMJIfts5zyFTs&quot;, "id": "PR_kwDOMJIfts5zyFTs",
&quot;number&quot;: 15 "number": 15
}, },
&quot;lastSeen&quot;: 1741535540953 "lastSeen": 1741535540953
}, },
{ {
&quot;id&quot;: { "id": {
&quot;id&quot;: &quot;PR_kwDOMJIfts6QQB3c&quot;, "id": "PR_kwDOMJIfts6QQB3c",
&quot;number&quot;: 59 "number": 59
}, },
&quot;lastSeen&quot;: 1743018960900 "lastSeen": 1743018960900
}, },
{ {
&quot;id&quot;: { "id": {
&quot;id&quot;: &quot;PR_kwDOMJIfts6QMPEg&quot;, "id": "PR_kwDOMJIfts6QMPEg",
&quot;number&quot;: 58 "number": 58
}, },
&quot;lastSeen&quot;: 1743019452983 "lastSeen": 1743019452983
},
{
"id": {
"id": "PR_kwDOMJIfts6QZvRI",
"number": 61
},
"lastSeen": 1743103196866
} }
] ]
}</component> }]]></component>
<component name="GithubPullRequestsUISettings">{ <component name="GithubPullRequestsUISettings">{
&quot;selectedUrlAndAccountId&quot;: { &quot;selectedUrlAndAccountId&quot;: {
&quot;url&quot;: &quot;https://github.com/iib0011/omni-tools.git&quot;, &quot;url&quot;: &quot;https://github.com/iib0011/omni-tools.git&quot;,
@@ -123,55 +131,56 @@
<option name="hideEmptyMiddlePackages" value="true" /> <option name="hideEmptyMiddlePackages" value="true" />
<option name="showLibraryContents" value="true" /> <option name="showLibraryContents" value="true" />
</component> </component>
<component name="PropertiesComponent">{ <component name="PropertiesComponent"><![CDATA[{
&quot;keyToString&quot;: { "keyToString": {
&quot;ASKED_ADD_EXTERNAL_FILES&quot;: &quot;true&quot;, "ASKED_ADD_EXTERNAL_FILES": "true",
&quot;ASKED_SHARE_PROJECT_CONFIGURATION_FILES&quot;: &quot;true&quot;, "ASKED_SHARE_PROJECT_CONFIGURATION_FILES": "true",
&quot;Docker.Dockerfile build.executor&quot;: &quot;Run&quot;, "Docker.Dockerfile build.executor": "Run",
&quot;Docker.Dockerfile.executor&quot;: &quot;Run&quot;, "Docker.Dockerfile.executor": "Run",
&quot;Playwright.Create transparent PNG.should make png color transparent.executor&quot;: &quot;Run&quot;, "Playwright.Create transparent PNG.should make png color transparent.executor": "Run",
&quot;Playwright.JoinText Component.executor&quot;: &quot;Run&quot;, "Playwright.JoinText Component.executor": "Run",
&quot;Playwright.JoinText Component.should merge text pieces with specified join character.executor&quot;: &quot;Run&quot;, "Playwright.JoinText Component.should merge text pieces with specified join character.executor": "Run",
&quot;RunOnceActivity.OpenProjectViewOnStart&quot;: &quot;true&quot;, "RunOnceActivity.OpenProjectViewOnStart": "true",
&quot;RunOnceActivity.ShowReadmeOnStart&quot;: &quot;true&quot;, "RunOnceActivity.ShowReadmeOnStart": "true",
&quot;RunOnceActivity.git.unshallow&quot;: &quot;true&quot;, "RunOnceActivity.git.unshallow": "true",
&quot;Vitest.compute function (1).executor&quot;: &quot;Run&quot;, "Vitest.compute function (1).executor": "Run",
&quot;Vitest.compute function.executor&quot;: &quot;Run&quot;, "Vitest.compute function.executor": "Run",
&quot;Vitest.mergeText.executor&quot;: &quot;Run&quot;, "Vitest.mergeText.executor": "Run",
&quot;Vitest.mergeText.should merge lines and preserve blank lines when deleteBlankLines is false.executor&quot;: &quot;Run&quot;, "Vitest.mergeText.should merge lines and preserve blank lines when deleteBlankLines is false.executor": "Run",
&quot;Vitest.mergeText.should merge lines, preserve blank lines and trailing spaces when both deleteBlankLines and deleteTrailingSpaces are false.executor&quot;: &quot;Run&quot;, "Vitest.mergeText.should merge lines, preserve blank lines and trailing spaces when both deleteBlankLines and deleteTrailingSpaces are false.executor": "Run",
&quot;Vitest.parsePageRanges.executor&quot;: &quot;Run&quot;, "Vitest.parsePageRanges.executor": "Run",
&quot;Vitest.removeDuplicateLines function.executor&quot;: &quot;Run&quot;, "Vitest.removeDuplicateLines function.executor": "Run",
&quot;Vitest.removeDuplicateLines function.newlines option.executor&quot;: &quot;Run&quot;, "Vitest.removeDuplicateLines function.newlines option.executor": "Run",
&quot;Vitest.removeDuplicateLines function.newlines option.should filter newlines when newlines is set to filter.executor&quot;: &quot;Run&quot;, "Vitest.removeDuplicateLines function.newlines option.should filter newlines when newlines is set to filter.executor": "Run",
&quot;Vitest.replaceText function (regexp mode).should return the original text when passed an invalid regexp.executor&quot;: &quot;Run&quot;, "Vitest.replaceText function (regexp mode).should return the original text when passed an invalid regexp.executor": "Run",
&quot;Vitest.replaceText function.executor&quot;: &quot;Run&quot;, "Vitest.replaceText function.executor": "Run",
&quot;git-widget-placeholder&quot;: &quot;main&quot;, "Vitest.timeBetweenDates.executor": "Run",
&quot;ignore.virus.scanning.warn.message&quot;: &quot;true&quot;, "git-widget-placeholder": "fork/TheLukasHenry/days-calculator-#51",
&quot;kotlin-language-version-configured&quot;: &quot;true&quot;, "ignore.virus.scanning.warn.message": "true",
&quot;last_opened_file_path&quot;: &quot;C:/Users/Ibrahima/IdeaProjects/omni-tools/src/components/input&quot;, "kotlin-language-version-configured": "true",
&quot;node.js.detected.package.eslint&quot;: &quot;true&quot;, "last_opened_file_path": "C:/Users/Ibrahima/IdeaProjects/omni-tools/src/components/input",
&quot;node.js.detected.package.tslint&quot;: &quot;true&quot;, "node.js.detected.package.eslint": "true",
&quot;node.js.selected.package.eslint&quot;: &quot;(autodetect)&quot;, "node.js.detected.package.tslint": "true",
&quot;node.js.selected.package.tslint&quot;: &quot;(autodetect)&quot;, "node.js.selected.package.eslint": "(autodetect)",
&quot;nodejs_package_manager_path&quot;: &quot;npm&quot;, "node.js.selected.package.tslint": "(autodetect)",
&quot;npm.build.executor&quot;: &quot;Run&quot;, "nodejs_package_manager_path": "npm",
&quot;npm.dev.executor&quot;: &quot;Run&quot;, "npm.build.executor": "Run",
&quot;npm.lint.executor&quot;: &quot;Run&quot;, "npm.dev.executor": "Run",
&quot;npm.prebuild.executor&quot;: &quot;Run&quot;, "npm.lint.executor": "Run",
&quot;npm.script:create:tool.executor&quot;: &quot;Run&quot;, "npm.prebuild.executor": "Run",
&quot;npm.test.executor&quot;: &quot;Run&quot;, "npm.script:create:tool.executor": "Run",
&quot;npm.test:e2e.executor&quot;: &quot;Run&quot;, "npm.test.executor": "Run",
&quot;npm.test:e2e:run.executor&quot;: &quot;Run&quot;, "npm.test:e2e.executor": "Run",
&quot;prettierjs.PrettierConfiguration.Package&quot;: &quot;C:\\Users\\Ibrahima\\IdeaProjects\\omni-tools\\node_modules\\prettier&quot;, "npm.test:e2e:run.executor": "Run",
&quot;project.structure.last.edited&quot;: &quot;Problems&quot;, "prettierjs.PrettierConfiguration.Package": "C:\\Users\\Ibrahima\\IdeaProjects\\omni-tools\\node_modules\\prettier",
&quot;project.structure.proportion&quot;: &quot;0.0&quot;, "project.structure.last.edited": "Problems",
&quot;project.structure.side.proportion&quot;: &quot;0.2&quot;, "project.structure.proportion": "0.0",
&quot;settings.editor.selected.configurable&quot;: &quot;refactai_advanced_settings&quot;, "project.structure.side.proportion": "0.2",
&quot;ts.external.directory.path&quot;: &quot;C:\\Users\\Ibrahima\\IdeaProjects\\omni-tools\\node_modules\\typescript\\lib&quot;, "settings.editor.selected.configurable": "refactai_advanced_settings",
&quot;vue.rearranger.settings.migration&quot;: &quot;true&quot; "ts.external.directory.path": "C:\\Users\\Ibrahima\\IdeaProjects\\omni-tools\\node_modules\\typescript\\lib",
"vue.rearranger.settings.migration": "true"
} }
}</component> }]]></component>
<component name="ReactDesignerToolWindowState"> <component name="ReactDesignerToolWindowState">
<option name="myId2Visible"> <option name="myId2Visible">
<map> <map>
@@ -197,17 +206,17 @@
<recent name="C:\Users\HP\IdeaProjects\omni-tools\src\components\options" /> <recent name="C:\Users\HP\IdeaProjects\omni-tools\src\components\options" />
</key> </key>
</component> </component>
<component name="RunManager" selected="npm.dev"> <component name="RunManager" selected="Vitest.calculateTimeBetweenDates">
<configuration name="Create transparent PNG.should make png color transparent" type="JavaScriptTestRunnerPlaywright" temporary="true" nameIsGenerated="true"> <configuration name="calculateTimeBetweenDates" type="JavaScriptTestRunnerVitest" temporary="true" nameIsGenerated="true">
<node-interpreter value="project" /> <node-interpreter value="project" />
<playwright-package value="$PROJECT_DIR$/node_modules/@playwright/test" /> <vitest-package value="$PROJECT_DIR$/node_modules/vitest" />
<working-dir value="$PROJECT_DIR$" /> <working-dir value="$PROJECT_DIR$" />
<vitest-options value="--run" />
<envs /> <envs />
<scope-kind value="TEST" /> <scope-kind value="SUITE" />
<test-file value="$PROJECT_DIR$/src/pages/tools/image/png/create-transparent/create-transparent.e2e.spec.ts" /> <test-file value="$PROJECT_DIR$/src/pages/tools/time/time-between-dates/time-between-dates.service.test.ts" />
<test-names> <test-names>
<test-name value="Create transparent PNG" /> <test-name value="calculateTimeBetweenDates" />
<test-name value="should make png color transparent" />
</test-names> </test-names>
<method v="2" /> <method v="2" />
</configuration> </configuration>
@@ -238,6 +247,19 @@
</test-names> </test-names>
<method v="2" /> <method v="2" />
</configuration> </configuration>
<configuration name="timeBetweenDates" type="JavaScriptTestRunnerVitest" temporary="true" nameIsGenerated="true">
<node-interpreter value="project" />
<vitest-package value="$PROJECT_DIR$/node_modules/vitest" />
<working-dir value="$PROJECT_DIR$" />
<vitest-options value="--run" />
<envs />
<scope-kind value="SUITE" />
<test-file value="$PROJECT_DIR$/src/pages/tools/time/time-between-dates/time-between-dates.service.test.ts" />
<test-names>
<test-name value="timeBetweenDates" />
</test-names>
<method v="2" />
</configuration>
<configuration default="true" type="docker-deploy" factoryName="dockerfile" temporary="true"> <configuration default="true" type="docker-deploy" factoryName="dockerfile" temporary="true">
<deployment type="dockerfile"> <deployment type="dockerfile">
<settings /> <settings />
@@ -254,30 +276,20 @@
<envs /> <envs />
<method v="2" /> <method v="2" />
</configuration> </configuration>
<configuration name="test:e2e" type="js.build_tools.npm" temporary="true" nameIsGenerated="true">
<package-json value="$PROJECT_DIR$/package.json" />
<command value="run" />
<scripts>
<script value="test:e2e" />
</scripts>
<node-interpreter value="project" />
<envs />
<method v="2" />
</configuration>
<list> <list>
<item itemvalue="npm.test:e2e" />
<item itemvalue="npm.dev" /> <item itemvalue="npm.dev" />
<item itemvalue="Playwright.Create transparent PNG.should make png color transparent" /> <item itemvalue="Vitest.calculateTimeBetweenDates" />
<item itemvalue="Vitest.timeBetweenDates" />
<item itemvalue="Vitest.parsePageRanges" /> <item itemvalue="Vitest.parsePageRanges" />
<item itemvalue="Vitest.replaceText function (regexp mode).should return the original text when passed an invalid regexp" /> <item itemvalue="Vitest.replaceText function (regexp mode).should return the original text when passed an invalid regexp" />
</list> </list>
<recent_temporary> <recent_temporary>
<list> <list>
<item itemvalue="Vitest.calculateTimeBetweenDates" />
<item itemvalue="Vitest.timeBetweenDates" />
<item itemvalue="npm.dev" /> <item itemvalue="npm.dev" />
<item itemvalue="Vitest.replaceText function (regexp mode).should return the original text when passed an invalid regexp" /> <item itemvalue="Vitest.replaceText function (regexp mode).should return the original text when passed an invalid regexp" />
<item itemvalue="Vitest.parsePageRanges" /> <item itemvalue="Vitest.parsePageRanges" />
<item itemvalue="Playwright.Create transparent PNG.should make png color transparent" />
<item itemvalue="npm.test:e2e" />
</list> </list>
</recent_temporary> </recent_temporary>
</component> </component>
@@ -363,22 +375,7 @@
<workItem from="1741971589699" duration="371000" /> <workItem from="1741971589699" duration="371000" />
<workItem from="1743018497879" duration="3895000" /> <workItem from="1743018497879" duration="3895000" />
<workItem from="1743047367993" duration="986000" /> <workItem from="1743047367993" duration="986000" />
</task> <workItem from="1743103182313" duration="4264000" />
<task id="LOCAL-00122" summary="fix: bg">
<option name="closed" value="true" />
<created>1740504100676</created>
<option name="number" value="00122" />
<option name="presentableId" value="LOCAL-00122" />
<option name="project" value="LOCAL" />
<updated>1740504100676</updated>
</task>
<task id="LOCAL-00123" summary="fix: bg">
<option name="closed" value="true" />
<created>1740505390205</created>
<option name="number" value="00123" />
<option name="presentableId" value="LOCAL-00123" />
<option name="project" value="LOCAL" />
<updated>1740505390205</updated>
</task> </task>
<task id="LOCAL-00124" summary="docs: readme"> <task id="LOCAL-00124" summary="docs: readme">
<option name="closed" value="true" /> <option name="closed" value="true" />
@@ -756,7 +753,23 @@
<option name="project" value="LOCAL" /> <option name="project" value="LOCAL" />
<updated>1743051792459</updated> <updated>1743051792459</updated>
</task> </task>
<option name="localTasksCounter" value="171" /> <task id="LOCAL-00171" summary="chore: zoom on hover">
<option name="closed" value="true" />
<created>1743052111988</created>
<option name="number" value="00171" />
<option name="presentableId" value="LOCAL-00171" />
<option name="project" value="LOCAL" />
<updated>1743052111988</updated>
</task>
<task id="LOCAL-00172" summary="refactor: time between dates">
<option name="closed" value="true" />
<created>1743106796406</created>
<option name="number" value="00172" />
<option name="presentableId" value="LOCAL-00172" />
<option name="project" value="LOCAL" />
<updated>1743106796406</updated>
</task>
<option name="localTasksCounter" value="173" />
<servers /> <servers />
</component> </component>
<component name="TypeScriptGeneratedFilesManager"> <component name="TypeScriptGeneratedFilesManager">
@@ -803,8 +816,6 @@
<option name="CHECK_CODE_SMELLS_BEFORE_PROJECT_COMMIT" value="false" /> <option name="CHECK_CODE_SMELLS_BEFORE_PROJECT_COMMIT" value="false" />
<option name="CHECK_NEW_TODO" value="false" /> <option name="CHECK_NEW_TODO" value="false" />
<option name="ADD_EXTERNAL_FILES_SILENTLY" value="true" /> <option name="ADD_EXTERNAL_FILES_SILENTLY" value="true" />
<MESSAGE value="fix: replace text service" />
<MESSAGE value="chore: smooth scroll for use this tool and examles" />
<MESSAGE value="feat: minify json" /> <MESSAGE value="feat: minify json" />
<MESSAGE value="feat: stringify json" /> <MESSAGE value="feat: stringify json" />
<MESSAGE value="feat: arithmetic sequence" /> <MESSAGE value="feat: arithmetic sequence" />
@@ -828,7 +839,9 @@
<MESSAGE value="chore: result file name" /> <MESSAGE value="chore: result file name" />
<MESSAGE value="chore: text result extensions" /> <MESSAGE value="chore: text result extensions" />
<MESSAGE value="chore: show new tools in landing" /> <MESSAGE value="chore: show new tools in landing" />
<option name="LAST_COMMIT_MESSAGE" value="chore: show new tools in landing" /> <MESSAGE value="chore: zoom on hover" />
<MESSAGE value="refactor: time between dates" />
<option name="LAST_COMMIT_MESSAGE" value="refactor: time between dates" />
</component> </component>
<component name="XSLT-Support.FileAssociations.UIState"> <component name="XSLT-Support.FileAssociations.UIState">
<expand /> <expand />

View File

@@ -1,13 +1,16 @@
import { tool as timeBetweenDates } from './time-between-dates/meta';
import { tool as daysDoHours } from './convert-days-to-hours/meta'; import { tool as daysDoHours } from './convert-days-to-hours/meta';
import { tool as hoursToDays } from './convert-hours-to-days/meta'; import { tool as hoursToDays } from './convert-hours-to-days/meta';
import { tool as convertSecondsToTime } from './convert-seconds-to-time/meta'; import { tool as convertSecondsToTime } from './convert-seconds-to-time/meta';
import { tool as convertTimetoSeconds } from './convert-time-to-seconds/meta'; import { tool as convertTimetoSeconds } from './convert-time-to-seconds/meta';
import { tool as truncateClockTime } from './truncate-clock-time/meta'; import { tool as truncateClockTime } from './truncate-clock-time/meta';
export const timeTools = [ export const timeTools = [
daysDoHours, daysDoHours,
hoursToDays, hoursToDays,
convertSecondsToTime, convertSecondsToTime,
convertTimetoSeconds, convertTimetoSeconds,
truncateClockTime truncateClockTime,
timeBetweenDates
]; ];

View File

@@ -0,0 +1,250 @@
import { Box, Paper, Typography } from '@mui/material';
import React, { useState } from 'react';
import ToolContent from '@components/ToolContent';
import TextFieldWithDesc from '@components/options/TextFieldWithDesc';
import SelectWithDesc from '@components/options/SelectWithDesc';
import {
calculateTimeBetweenDates,
formatTimeWithLargestUnit,
getTimeWithTimezone,
unitHierarchy
} from './service';
import * as Yup from 'yup';
import { CardExampleType } from '@components/examples/ToolExamples';
type TimeUnit =
| 'milliseconds'
| 'seconds'
| 'minutes'
| 'hours'
| 'days'
| 'months'
| 'years';
type InitialValuesType = {
startDate: string;
startTime: string;
endDate: string;
endTime: string;
startTimezone: string;
endTimezone: string;
};
const initialValues: InitialValuesType = {
startDate: new Date().toISOString().split('T')[0],
startTime: '00:00',
endDate: new Date().toISOString().split('T')[0],
endTime: '12:00',
startTimezone: 'local',
endTimezone: 'local'
};
const validationSchema = Yup.object({
startDate: Yup.string().required('Start date is required'),
startTime: Yup.string().required('Start time is required'),
endDate: Yup.string().required('End date is required'),
endTime: Yup.string().required('End time is required'),
startTimezone: Yup.string(),
endTimezone: Yup.string()
});
const timezoneOptions = [
{ value: 'local', label: 'Local Time' },
...Intl.supportedValuesOf('timeZone')
.map((tz) => {
const formatter = new Intl.DateTimeFormat('en', {
timeZone: tz,
timeZoneName: 'shortOffset'
});
const offset =
formatter
.formatToParts(new Date())
.find((part) => part.type === 'timeZoneName')?.value || '';
return {
value: offset.replace('UTC', 'GMT'),
label: `${offset.replace('UTC', 'GMT')} (${tz})`
};
})
.sort((a, b) =>
a.value.localeCompare(b.value, undefined, { numeric: true })
)
];
const exampleCards: CardExampleType<InitialValuesType>[] = [
{
title: 'One Year Difference',
description: 'Calculate the time between dates that are one year apart',
sampleOptions: {
startDate: '2023-01-01',
startTime: '12:00',
endDate: '2024-01-01',
endTime: '12:00',
startTimezone: 'local',
endTimezone: 'local'
},
sampleResult: '1 year'
},
{
title: 'Different Timezones',
description: 'Calculate the time difference between New York and London',
sampleOptions: {
startDate: '2023-01-01',
startTime: '12:00',
endDate: '2023-01-01',
endTime: '12:00',
startTimezone: 'GMT-5',
endTimezone: 'GMT'
},
sampleResult: '5 hours'
},
{
title: 'Detailed Time Breakdown',
description: 'Show a detailed breakdown of a time difference',
sampleOptions: {
startDate: '2023-01-01',
startTime: '09:30',
endDate: '2023-01-03',
endTime: '14:45',
startTimezone: 'local',
endTimezone: 'local'
},
sampleResult: '2 days, 5 hours, 15 minutes'
}
];
export default function TimeBetweenDates() {
const [result, setResult] = useState<string>('');
return (
<ToolContent
title="Time Between Dates"
inputComponent={null}
resultComponent={
result ? (
<Paper
elevation={3}
sx={{
p: 3,
borderLeft: '5px solid',
borderColor: 'primary.main',
bgcolor: 'background.paper',
maxWidth: '100%',
mx: 'auto'
}}
>
<Typography
variant="h4"
align="center"
sx={{ fontWeight: 'bold', color: 'primary.main' }}
>
{result}
</Typography>
</Paper>
) : null
}
initialValues={initialValues}
validationSchema={validationSchema}
exampleCards={exampleCards}
toolInfo={{
title: 'Time Between Dates Calculator',
description:
'Calculate the exact time difference between two dates and times, with support for different timezones. This tool provides a detailed breakdown of the time difference in various units (years, months, days, hours, minutes, and seconds).'
}}
getGroups={({ values, updateField }) => [
{
title: 'Start Date & Time',
component: (
<Box>
<TextFieldWithDesc
description="Start Date"
value={values.startDate}
onOwnChange={(val) => updateField('startDate', val)}
type="date"
/>
<TextFieldWithDesc
description="Start Time"
value={values.startTime}
onOwnChange={(val) => updateField('startTime', val)}
type="time"
/>
<SelectWithDesc
description="Start Timezone"
selected={values.startTimezone}
onChange={(val: string) => updateField('startTimezone', val)}
options={timezoneOptions}
/>
</Box>
)
},
{
title: 'End Date & Time',
component: (
<Box>
<TextFieldWithDesc
description="End Date"
value={values.endDate}
onOwnChange={(val) => updateField('endDate', val)}
type="date"
/>
<TextFieldWithDesc
description="End Time"
value={values.endTime}
onOwnChange={(val) => updateField('endTime', val)}
type="time"
/>
<SelectWithDesc
description="End Timezone"
selected={values.endTimezone}
onChange={(val: string) => updateField('endTimezone', val)}
options={timezoneOptions}
/>
</Box>
)
}
]}
compute={(values) => {
try {
const startDateTime = getTimeWithTimezone(
values.startDate,
values.startTime,
values.startTimezone
);
const endDateTime = getTimeWithTimezone(
values.endDate,
values.endTime,
values.endTimezone
);
// Calculate time difference
const difference = calculateTimeBetweenDates(
startDateTime,
endDateTime
);
// Auto-determine the best unit to display based on the time difference
const bestUnit: TimeUnit =
unitHierarchy.find((unit) => difference[unit] > 0) ||
'milliseconds';
const formattedDifference = formatTimeWithLargestUnit(
difference,
bestUnit
);
setResult(formattedDifference);
} catch (error) {
setResult(
`Error: ${
error instanceof Error
? error.message
: 'Failed to calculate time difference'
}`
);
}
}}
/>
);
}

View File

@@ -0,0 +1,22 @@
import { defineTool } from '@tools/defineTool';
import { lazy } from 'react';
export const tool = defineTool('time', {
name: 'Time Between Dates',
path: 'time-between-dates',
icon: 'mdi:calendar-clock',
description:
'Calculate the exact time difference between two dates and times, with support for different timezones. This tool provides a detailed breakdown of the time difference in various units (years, months, days, hours, minutes, and seconds).',
shortDescription:
'Calculate the precise time duration between two dates with timezone support.',
keywords: [
'time',
'dates',
'difference',
'duration',
'calculator',
'timezones',
'interval'
],
component: lazy(() => import('./index'))
});

View File

@@ -0,0 +1,129 @@
export const unitHierarchy = [
'years',
'months',
'days',
'hours',
'minutes',
'seconds',
'milliseconds'
] as const;
export type TimeUnit = (typeof unitHierarchy)[number];
export type TimeDifference = Record<TimeUnit, number>;
export const calculateTimeBetweenDates = (
startDate: Date,
endDate: Date
): TimeDifference => {
if (endDate < startDate) {
const temp = startDate;
startDate = endDate;
endDate = temp;
}
const milliseconds = endDate.getTime() - startDate.getTime();
const seconds = Math.floor(milliseconds / 1000);
const minutes = Math.floor(seconds / 60);
const hours = Math.floor(minutes / 60);
const days = Math.floor(hours / 24);
// Approximate months and years
const startYear = startDate.getFullYear();
const startMonth = startDate.getMonth();
const endYear = endDate.getFullYear();
const endMonth = endDate.getMonth();
const months = (endYear - startYear) * 12 + (endMonth - startMonth);
const years = Math.floor(months / 12);
return {
milliseconds,
seconds,
minutes,
hours,
days,
months,
years
};
};
export const formatTimeDifference = (
difference: TimeDifference,
includeUnits: TimeUnit[] = unitHierarchy.slice(0, -1)
): string => {
const timeUnits: { key: TimeUnit; value: number; divisor?: number }[] = [
{ key: 'years', value: difference.years },
{ key: 'months', value: difference.months, divisor: 12 },
{ key: 'days', value: difference.days, divisor: 30 },
{ key: 'hours', value: difference.hours, divisor: 24 },
{ key: 'minutes', value: difference.minutes, divisor: 60 },
{ key: 'seconds', value: difference.seconds, divisor: 60 }
];
const parts = timeUnits
.filter(({ key }) => includeUnits.includes(key))
.map(({ key, value, divisor }) => {
const remaining = divisor ? value % divisor : value;
return remaining > 0 ? `${remaining} ${key}` : '';
})
.filter(Boolean);
if (parts.length === 0) {
if (includeUnits.includes('milliseconds')) {
return `${difference.milliseconds} millisecond${
difference.milliseconds === 1 ? '' : 's'
}`;
}
return '0 seconds';
}
return parts.join(', ');
};
export const getTimeWithTimezone = (
dateString: string,
timeString: string,
timezone: string
): Date => {
// Combine date and time
const dateTimeString = `${dateString}T${timeString}Z`; // Append 'Z' to enforce UTC parsing
const utcDate = new Date(dateTimeString);
if (isNaN(utcDate.getTime())) {
throw new Error('Invalid date or time format');
}
// If timezone is "local", return the local date
if (timezone === 'local') {
return utcDate;
}
// Extract offset from timezone (e.g., "GMT+5:30" or "GMT-4")
const match = timezone.match(/^GMT(?:([+-]\d{1,2})(?::(\d{2}))?)?$/);
if (!match) {
throw new Error('Invalid timezone format');
}
const offsetHours = match[1] ? parseInt(match[1], 10) : 0;
const offsetMinutes = match[2] ? parseInt(match[2], 10) : 0;
const totalOffsetMinutes =
offsetHours * 60 + (offsetHours < 0 ? -offsetMinutes : offsetMinutes);
// Adjust the UTC date by the timezone offset
return new Date(utcDate.getTime() - totalOffsetMinutes * 60 * 1000);
};
// Helper function to format time based on largest unit
export const formatTimeWithLargestUnit = (
difference: TimeDifference,
largestUnit: TimeUnit
): string => {
const largestUnitIndex = unitHierarchy.indexOf(largestUnit);
const unitsToInclude = unitHierarchy.slice(largestUnitIndex);
// Preserve only whole values, do not apply fractional conversions
const adjustedDifference: TimeDifference = { ...difference };
return formatTimeDifference(adjustedDifference, unitsToInclude);
};

View File

@@ -0,0 +1,96 @@
import { describe, expect, it } from 'vitest';
import {
calculateTimeBetweenDates,
formatTimeDifference,
formatTimeWithLargestUnit,
getTimeWithTimezone
} from './service';
// Utility function to create a date
const createDate = (
year: number,
month: number,
day: number,
hours = 0,
minutes = 0,
seconds = 0
) => new Date(Date.UTC(year, month - 1, day, hours, minutes, seconds));
describe('calculateTimeBetweenDates', () => {
it('should calculate the correct time difference', () => {
const startDate = createDate(2023, 1, 1);
const endDate = createDate(2024, 1, 1);
const result = calculateTimeBetweenDates(startDate, endDate);
expect(result.years).toBe(1);
expect(result.months).toBe(12);
expect(result.days).toBeGreaterThanOrEqual(365);
});
it('should swap dates if startDate is after endDate', () => {
const startDate = createDate(2024, 1, 1);
const endDate = createDate(2023, 1, 1);
const result = calculateTimeBetweenDates(startDate, endDate);
expect(result.years).toBe(1);
});
});
describe('formatTimeDifference', () => {
it('should format time difference correctly', () => {
const difference = {
years: 1,
months: 2,
days: 10,
hours: 5,
minutes: 30,
seconds: 0,
milliseconds: 0
};
expect(formatTimeDifference(difference)).toBe(
'1 years, 2 months, 10 days, 5 hours, 30 minutes'
);
});
it('should return 0 seconds if all values are zero', () => {
expect(
formatTimeDifference({
years: 0,
months: 0,
days: 0,
hours: 0,
minutes: 0,
seconds: 0,
milliseconds: 0
})
).toBe('0 seconds');
});
});
describe('getTimeWithTimezone', () => {
it('should convert UTC date to specified timezone', () => {
const date = getTimeWithTimezone('2025-03-27', '12:00:00', 'GMT+2');
expect(date.getUTCHours()).toBe(10); // 12:00 GMT+2 is 10:00 UTC
});
it('should throw error for invalid timezone', () => {
expect(() =>
getTimeWithTimezone('2025-03-27', '12:00:00', 'INVALID')
).toThrow('Invalid timezone format');
});
});
describe('formatTimeWithLargestUnit', () => {
it('should format time with the largest unit', () => {
const difference = {
years: 0,
months: 1,
days: 15,
hours: 12,
minutes: 0,
seconds: 0,
milliseconds: 0
};
expect(formatTimeWithLargestUnit(difference, 'days')).toContain(
'15 days, 12 hours'
);
});
});

View File

@@ -21,6 +21,7 @@ export type ToolCategory =
| 'video' | 'video'
| 'list' | 'list'
| 'json' | 'json'
| 'time'
| 'csv' | 'csv'
| 'time' | 'time'
| 'pdf'; | 'pdf';

View File

@@ -66,6 +66,12 @@ const categoriesConfig: {
value: value:
'Tools for working with JSON data structures prettify and minify JSON objects, flatten JSON arrays, stringify JSON values, analyze data, and much more' 'Tools for working with JSON data structures prettify and minify JSON objects, flatten JSON arrays, stringify JSON values, analyze data, and much more'
}, },
{
type: 'time',
icon: 'mdi:clock-time-five',
value:
'Tools for working with time and date calculate time differences, convert between time zones, format dates, generate date sequences, and much more.'
},
{ {
type: 'csv', type: 'csv',
icon: 'material-symbols-light:csv-outline', icon: 'material-symbols-light:csv-outline',