Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Format times in clarity stats as mm:ss #9534

Merged
merged 2 commits into from Jun 4, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 0 additions & 1 deletion config/i18n.json
Expand Up @@ -238,7 +238,7 @@
"HasOrnament": "Shows items that have an ornament applied.",
"InLoadout": "is:inloadout shows items that are included in any loadout. Searching with inloadout: shows items that are included in loadouts with matching titles. When used with a hashtag, inloadout: shows items whose loadouts have the hashtag in the title or notes.",
"InInGameLoadout": "is:iningameloadout shows items that are included in any in-game loadout.",
"Infusable": "Shows items that can be infused.",

Check warning on line 241 in config/i18n.json

View workflow job for this annotation

GitHub Actions / spellcheck

Unknown word (iningameloadout)
"IsAdept": "Shows weapons compatible with Adept mods.",
"IsCrafted": "Shows weapons that have been crafted.",
"IsSunset": "Shows items that have been sunset and can no longer be infused to max power.",
Expand Down Expand Up @@ -1123,7 +1123,6 @@
"TierProgress": "T{{tier}} {{statName}} ({{progress}}/60 for T{{nextTier}})\n",
"TierProgress_Max": "T{{tier}} {{statName}} ({{progress}}/300)\n",
"Total": "Total",
"Second": "s",
"MetersPerSecond": "m/s",
"Percentage": "%",
"HP": "HP",
Expand Down
11 changes: 6 additions & 5 deletions src/app/store-stats/ClarityCharacterStat.m.scss
Expand Up @@ -21,11 +21,6 @@
th,
td {
opacity: 0.5;
&:nth-child(2n) {
text-align: right;
padding-left: 6px;
font-variant-numeric: tabular-nums;
}
}

/* stylelint-disable-next-line no-descending-specificity */
Expand Down Expand Up @@ -63,3 +58,9 @@
display: block;
font-size: 11px;
}

.value {
text-align: right;
padding-left: 6px;
font-variant-numeric: tabular-nums;
}
1 change: 1 addition & 0 deletions src/app/store-stats/ClarityCharacterStat.m.scss.d.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

48 changes: 34 additions & 14 deletions src/app/store-stats/ClarityCharacterStat.tsx
Expand Up @@ -3,6 +3,7 @@ import { clarityCharacterStatsSelector } from 'app/clarity/selectors';
import BungieImage from 'app/dim-ui/BungieImage';
import { Tooltip } from 'app/dim-ui/PressTip';
import { useD2Definitions } from 'app/manifest/selectors';
import { timerDurationFromMsWithDecimal } from 'app/utils/time';
import { DestinyInventoryItemDefinition } from 'bungie-api-ts/destiny2';
import clsx from 'clsx';
import { StatHashes } from 'data/d2/generated-enums';
Expand Down Expand Up @@ -92,7 +93,7 @@ export default function ClarityCharacterStat({
name={t('Stats.TimeToFullHP')}
cooldowns={clarityStatData.TimeToFullHP}
tier={tier}
unit={t('Stats.Second')}
unit="s"
/>
);
} else if ('WalkingSpeed' in clarityStatData) {
Expand Down Expand Up @@ -158,16 +159,19 @@ export default function ClarityCharacterStat({
<th />
{tier - 1 >= 0 && (
<>
<th>{t('LoadoutBuilder.TierNumber', { tier: tier - 1 })}</th>
<th />
<th colSpan={2} className={styles.value}>
{t('LoadoutBuilder.TierNumber', { tier: tier - 1 })}
</th>
</>
)}
<th className={styles.currentColumn}>{t('LoadoutBuilder.TierNumber', { tier })}</th>
<th />
<th colSpan={2} className={clsx(styles.value, styles.currentColumn)}>
{t('LoadoutBuilder.TierNumber', { tier })}
</th>
{tier + 1 <= 10 && (
<>
<th>{t('LoadoutBuilder.TierNumber', { tier: tier + 1 })}</th>
<th />
<th colSpan={2} className={styles.value}>
{t('LoadoutBuilder.TierNumber', { tier: tier + 1 })}
</th>
</>
)}
</tr>
Expand All @@ -183,7 +187,7 @@ export default function ClarityCharacterStat({
cooldowns={cooldowns}
tier={tier}
overrides={overrides}
unit={t('Stats.Second')}
unit="s"
/>
))}
{intrinsicCooldowns}
Expand All @@ -209,6 +213,16 @@ function StatTableRow({
overrides?: DestinyInventoryItemDefinition[];
}) {
const unitEl = <td className={styles.unit}>{unit}</td>;
const seconds = unit === 's';

const formatValue = (val: number) => {
if (seconds) {
return timerDurationFromMsWithDecimal(val * 1000);
}
return val.toLocaleString();
};

const colspan = seconds ? 2 : 1;

return (
<tr>
Expand All @@ -229,16 +243,22 @@ function StatTableRow({
</th>
{tier - 1 >= 0 && (
<>
<td>{cooldowns[tier - 1].toLocaleString()}</td>
{unitEl}
<td className={styles.value} colSpan={colspan}>
{formatValue(cooldowns[tier - 1])}
</td>
{!seconds && unitEl}
</>
)}
<td className={styles.currentColumn}>{cooldowns[tier].toLocaleString()}</td>
<td className={clsx(styles.unit, styles.currentColumn)}>{unit}</td>
<td className={clsx(styles.value, styles.currentColumn)} colSpan={colspan}>
{formatValue(cooldowns[tier])}
</td>
{!seconds && <td className={clsx(styles.unit, styles.currentColumn)}>{unit}</td>}
{tier + 1 <= 10 && (
<>
<td>{cooldowns[tier + 1].toLocaleString()}</td>
{unitEl}
<td className={styles.value} colSpan={colspan}>
{formatValue(cooldowns[tier + 1])}
</td>
{!seconds && unitEl}
</>
)}
</tr>
Expand Down
16 changes: 15 additions & 1 deletion src/app/utils/time.test.ts
@@ -1,6 +1,11 @@
import i18next from 'i18next';
import { setupi18n } from 'testing/test-utils';
import { i15dDurationFromMs, i15dDurationFromMsWithSeconds, timerDurationFromMs } from './time';
import {
i15dDurationFromMs,
i15dDurationFromMsWithSeconds,
timerDurationFromMs,
timerDurationFromMsWithDecimal,
} from './time';

beforeAll(() => {
setupi18n();
Expand All @@ -15,6 +20,15 @@ test.each([
expect(timerDurationFromMs(timestamp)).toBe(expected);
});

test.each([
[1000, '0:01'],
[0, '0:00'],
[279241234, '3:05:34:01.234'],
[20041234, '5:34:01.234'],
])('timerDurationFromMs(%s) === "%s"', (timestamp, expected) => {
expect(timerDurationFromMsWithDecimal(timestamp)).toBe(expected);
});

describe('english localization', () => {
beforeEach(() => {
i18next.changeLanguage('en');
Expand Down
21 changes: 19 additions & 2 deletions src/app/utils/time.ts
Expand Up @@ -23,14 +23,31 @@ function durationFromMs(ms: number) {
*
* negative durations are treated as 0
*/
export function timerDurationFromMs(milliseconds: number) {
export function timerDurationFromMs(milliseconds: number, minSegments = 3) {
const duration = durationFromMs(milliseconds).slice(0, -1);
while (duration[0] === 0 && duration.length > 3) {
while (duration[0] === 0 && duration.length > minSegments) {
duration.shift();
}
return duration.map((u, i) => `${u}`.padStart(i === 0 ? 0 : 2, '0')).join(':');
}

/**
* print a number of milliseconds as m:s.ms
*
* negative durations are treated as 0
*/
export function timerDurationFromMsWithDecimal(milliseconds: number) {
const duration = durationFromMs(milliseconds);
while (duration[0] === 0 && duration.length > 3) {
duration.shift();
}

const ms = duration.pop()!;
duration[duration.length - 1] = (duration[duration.length - 1] * 1000 + ms) / 1000;

return duration.map((u, i) => (i !== 0 && u < 10 ? `0${u}` : u)).join(':');
}

/**
* print a number of milliseconds as something like "4d 0:51",
* containing days, minutes, and hours.
Expand Down
1 change: 0 additions & 1 deletion src/locale/en.json
Expand Up @@ -239,7 +239,7 @@
"HasShader": "Shows items that have a shader applied.",
"HoldsMod": "Shows armor compatible with a specific type of Seasonal Mod.",
"InInGameLoadout": "is:iningameloadout shows items that are included in any in-game loadout.",
"InLoadout": "is:inloadout shows items that are included in any loadout. Searching with inloadout: shows items that are included in loadouts with matching titles. When used with a hashtag, inloadout: shows items whose loadouts have the hashtag in the title or notes.",

Check warning on line 242 in src/locale/en.json

View workflow job for this annotation

GitHub Actions / spellcheck

Unknown word (iningameloadout)
"Infusable": "Shows items that can be infused.",
"InfusionFodder": "Shows items that could be infused into lower-power versions of the same item for only glimmer.",
"IsAdept": "Shows weapons compatible with Adept mods.",
Expand Down Expand Up @@ -1118,7 +1118,6 @@
"PowerModifier": "Power granted by seasonal experience progression",
"Prestige": "Prestige Level: {{level}}\n{{exp}}xp until 5 motes of light.",
"Quality": "Stats quality",
"Second": "s",
"StrafingSpeed": "Strafing",
"Strength": "Strength",
"Sunset": "Sunset",
Expand Down