feat(core): format-bytes - #205
Conversation
- Introduced `nve-format-bytes` component to convert byte counts into human-readable decimal or binary units Signed-off-by: Cory Rylan <crylan@nvidia.com>
📝 WalkthroughWalkthroughThe PR adds the ChangesFormat Bytes component
Estimated code review effort: 3 (Moderate) | ~25 minutes Sequence Diagram(s)sequenceDiagram
participant Consumer
participant FormatBytes
participant IntlNumberFormat
Consumer->>FormatBytes: Set value and formatting properties
FormatBytes->>IntlNumberFormat: Format value with locale and precision
IntlNumberFormat-->>FormatBytes: Return localized number
FormatBytes-->>Consumer: Render formatted value and unit label
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 ESLint
projects/core/package.jsonESLint skipped: missing config or dependency (missing-dependency). The ESLint configuration references a package that is not available in the sandbox. projects/core/src/bundle.tsESLint skipped: the ESLint configuration for this file references a package that is not available in the sandbox. projects/core/src/format-bytes/define.tsESLint skipped: the ESLint configuration for this file references a package that is not available in the sandbox.
Comment |
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@projects/core/src/format-bytes/format-bytes.test.axe.ts`:
- Around line 13-26: Update the stability setup in the test around
FormatBytes.metadata.tag to await elementIsStable for all three nve-format-bytes
fixture elements, rather than only the first querySelector result, before
calling runAxe. Preserve the existing fixture cleanup and accessibility
assertion.
In `@projects/core/src/format-bytes/format-bytes.ts`:
- Around line 130-146: Make display sourcing consistent between `#resolveAutoUnit`
and `#formatLabel`. Prefer passing the display value explicitly by updating
`#formatLabel` to accept a display parameter and updating its call in
`#formattedBytes` to pass this.display, while using that parameter for label
selection.
- Around line 148-167: Update `#warnInvalidOption` to remove the rawValue
parameter and return type/value, then adjust its callers in
`#hasValidConfiguration` to pass only the option name and value. Remove rawValue
from `#hasValidConfiguration` as well, and update its call site to invoke it
without arguments while preserving the existing invalid-configuration return
behavior.
- Around line 142-146: Update the formatting flow around `#formattedBytes` and
`#formatLabel` so the value is rounded once using the same fraction-digit settings
as `#formatNumber`, then pass that rounded value to both helpers. Select singular
versus plural in `#formatLabel` based on the rounded value while preserving
short-label behavior.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Enterprise
Run ID: b30e9a98-6f6c-4e84-ac6e-c5b9e9faad96
⛔ Files ignored due to path filters (2)
projects/core/.visual/format-bytes.dark.pngis excluded by!**/*.pngprojects/core/.visual/format-bytes.pngis excluded by!**/*.png
📒 Files selected for processing (15)
projects/core/package.jsonprojects/core/src/bundle.tsprojects/core/src/format-bytes/define.tsprojects/core/src/format-bytes/format-bytes.cssprojects/core/src/format-bytes/format-bytes.examples.tsprojects/core/src/format-bytes/format-bytes.test.axe.tsprojects/core/src/format-bytes/format-bytes.test.lighthouse.tsprojects/core/src/format-bytes/format-bytes.test.ssr.tsprojects/core/src/format-bytes/format-bytes.test.tsprojects/core/src/format-bytes/format-bytes.test.visual.tsprojects/core/src/format-bytes/format-bytes.tsprojects/core/src/format-bytes/index.tsprojects/core/src/index.test.lighthouse.tsprojects/site/src/_11ty/layouts/common.jsprojects/site/src/docs/elements/format-bytes.md
| const fixture = await createFixture(html` | ||
| <nve-format-bytes locale="en-US">1048576</nve-format-bytes> | ||
| <nve-format-bytes locale="en-US" display="binary" unit-display="long">1048576</nve-format-bytes> | ||
| <nve-format-bytes locale="de-DE" unit="kb">1048576</nve-format-bytes> | ||
| `); | ||
|
|
||
| try { | ||
| await elementIsStable(fixture.querySelector(FormatBytes.metadata.tag)); | ||
| const results = await runAxe([FormatBytes.metadata.tag]); | ||
| expect(results.violations.length).toBe(0); | ||
| } finally { | ||
| removeFixture(fixture); | ||
| } | ||
| }); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Await stability for all three fixture elements, not just the first.
The fixture creates three nve-format-bytes elements. Line 20 only awaits elementIsStable on fixture.querySelector(FormatBytes.metadata.tag), which resolves to the first matching element only. The axe scan on line 21 covers all three elements, but the second and third elements' render cycles are not confirmed stable. This can produce a flaky or false-negative accessibility result if axe scans an element mid-update.
🐛 Proposed fix to await stability for every element
try {
- await elementIsStable(fixture.querySelector(FormatBytes.metadata.tag));
+ await Promise.all(
+ [...fixture.querySelectorAll(FormatBytes.metadata.tag)].map((el) => elementIsStable(el))
+ );
const results = await runAxe([FormatBytes.metadata.tag]);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const fixture = await createFixture(html` | |
| <nve-format-bytes locale="en-US">1048576</nve-format-bytes> | |
| <nve-format-bytes locale="en-US" display="binary" unit-display="long">1048576</nve-format-bytes> | |
| <nve-format-bytes locale="de-DE" unit="kb">1048576</nve-format-bytes> | |
| `); | |
| try { | |
| await elementIsStable(fixture.querySelector(FormatBytes.metadata.tag)); | |
| const results = await runAxe([FormatBytes.metadata.tag]); | |
| expect(results.violations.length).toBe(0); | |
| } finally { | |
| removeFixture(fixture); | |
| } | |
| }); | |
| const fixture = await createFixture(html` | |
| <nve-format-bytes locale="en-US">1048576</nve-format-bytes> | |
| <nve-format-bytes locale="en-US" display="binary" unit-display="long">1048576</nve-format-bytes> | |
| <nve-format-bytes locale="de-DE" unit="kb">1048576</nve-format-bytes> | |
| `); | |
| try { | |
| await Promise.all( | |
| [...fixture.querySelectorAll(FormatBytes.metadata.tag)].map((el) => elementIsStable(el)) | |
| ); | |
| const results = await runAxe([FormatBytes.metadata.tag]); | |
| expect(results.violations.length).toBe(0); | |
| } finally { | |
| removeFixture(fixture); | |
| } | |
| }); |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@projects/core/src/format-bytes/format-bytes.test.axe.ts` around lines 13 -
26, Update the stability setup in the test around FormatBytes.metadata.tag to
await elementIsStable for all three nve-format-bytes fixture elements, rather
than only the first querySelector result, before calling runAxe. Preserve the
existing fixture cleanup and accessibility assertion.
| #resolveAutoUnit(value: number, display: FormatBytesDisplay): FormatBytesUnit { | ||
| const base = display === 'binary' ? 1024 : 1000; | ||
| const absoluteValue = Math.abs(value); | ||
|
|
||
| for (let index = UNITS.length - 1; index > 0; index--) { | ||
| const unit = UNITS[index]; | ||
| if (unit && absoluteValue >= base ** index) return unit; | ||
| } | ||
|
|
||
| return 'b'; | ||
| } | ||
|
|
||
| #formatLabel(unit: FormatBytesUnit, convertedValue: number): string { | ||
| const labels = this.display === 'binary' ? BINARY_LABELS[unit] : DECIMAL_LABELS[unit]; | ||
| if (this.unitDisplay === 'short') return labels.short; | ||
| return Math.abs(convertedValue) === 1 ? labels.singular : labels.plural; | ||
| } |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value
Make the display source consistent between the two helpers.
#resolveAutoUnit receives display as a parameter. #formatLabel reads this.display directly. The behavior is identical today because #formattedBytes passes this.display. The mixed style makes future refactors error-prone, because a caller can pass a display value to #resolveAutoUnit that #formatLabel then ignores. Pass display to both helpers, or read this.display in both.
♻️ Proposed refactor to pass `display` explicitly
- `#formatLabel`(unit: FormatBytesUnit, convertedValue: number): string {
- const labels = this.display === 'binary' ? BINARY_LABELS[unit] : DECIMAL_LABELS[unit];
+ `#formatLabel`(unit: FormatBytesUnit, convertedValue: number, display: FormatBytesDisplay): string {
+ const labels = display === 'binary' ? BINARY_LABELS[unit] : DECIMAL_LABELS[unit];
if (this.unitDisplay === 'short') return labels.short;
return Math.abs(convertedValue) === 1 ? labels.singular : labels.plural;
}Update the call site at line 191 to pass this.display.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@projects/core/src/format-bytes/format-bytes.ts` around lines 130 - 146, Make
display sourcing consistent between `#resolveAutoUnit` and `#formatLabel`. Prefer
passing the display value explicitly by updating `#formatLabel` to accept a
display parameter and updating its call in `#formattedBytes` to pass this.display,
while using that parameter for label selection.
| #formatLabel(unit: FormatBytesUnit, convertedValue: number): string { | ||
| const labels = this.display === 'binary' ? BINARY_LABELS[unit] : DECIMAL_LABELS[unit]; | ||
| if (this.unitDisplay === 'short') return labels.short; | ||
| return Math.abs(convertedValue) === 1 ? labels.singular : labels.plural; | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Pluralize the long label from the rounded number, not the unrounded value.
#formatLabel compares the unrounded convertedValue against 1, but #formatNumber rounds the displayed number independently. When the converted value rounds to 1 and is not exactly 1, the output pairs a singular number with a plural label.
Reproduction: set value = 1004000, unitDisplay = 'long', display = 'decimal'. The converted value is 1.004, so #formatNumber renders 1 with the default two fraction digits, and #formatLabel selects megabytes. The output is 1 megabytes.
Round the value with the same fraction-digit settings before you select the label.
🐛 Proposed fix to pluralize from the rounded value
- `#formatLabel`(unit: FormatBytesUnit, convertedValue: number): string {
+ `#formatLabel`(unit: FormatBytesUnit, roundedValue: number): string {
const labels = this.display === 'binary' ? BINARY_LABELS[unit] : DECIMAL_LABELS[unit];
if (this.unitDisplay === 'short') return labels.short;
- return Math.abs(convertedValue) === 1 ? labels.singular : labels.plural;
+ return Math.abs(roundedValue) === 1 ? labels.singular : labels.plural;
}Compute the rounded value once in #formattedBytes and pass it to both helpers:
const convertedValue = numericValue / base ** unitIndex;
+ const maximumFractionDigits = this.maximumFractionDigits ?? Math.max(this.minimumFractionDigits ?? 0, 2);
+ const factor = 10 ** maximumFractionDigits;
+ const roundedValue = Math.round(convertedValue * factor) / factor;
try {
- return `${this.#formatNumber(convertedValue)} ${this.#formatLabel(resolvedUnit, convertedValue)}`;
+ return `${this.#formatNumber(convertedValue)} ${this.#formatLabel(resolvedUnit, roundedValue)}`;🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@projects/core/src/format-bytes/format-bytes.ts` around lines 142 - 146,
Update the formatting flow around `#formattedBytes` and `#formatLabel` so the value
is rounded once using the same fraction-digit settings as `#formatNumber`, then
pass that rounded value to both helpers. Select singular versus plural in
`#formatLabel` based on the rounded value while preserving short-label behavior.
| #warnInvalidOption(name: string, value: unknown, rawValue: string): string { | ||
| LogService.warn(`format-bytes: invalid ${name} value "${String(value)}"`); | ||
| return rawValue; | ||
| } | ||
|
|
||
| #hasValidConfiguration(rawValue: string): boolean { | ||
| if (!isDisplay(this.display)) { | ||
| this.#warnInvalidOption('display', this.display, rawValue); | ||
| return false; | ||
| } | ||
| if (!isUnitDisplay(this.unitDisplay)) { | ||
| this.#warnInvalidOption('unit-display', this.unitDisplay, rawValue); | ||
| return false; | ||
| } | ||
| if (this.unit !== undefined && !isUnit(this.unit)) { | ||
| this.#warnInvalidOption('unit', this.unit, rawValue); | ||
| return false; | ||
| } | ||
| return true; | ||
| } |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win
Remove the unused return value and the threaded rawValue parameter.
#warnInvalidOption returns rawValue, but both callers in #hasValidConfiguration discard the result and return false. #hasValidConfiguration accepts rawValue only to forward it into that unused return. Drop the return value and the parameter to simplify both signatures.
♻️ Proposed refactor to drop the unused value
- `#warnInvalidOption`(name: string, value: unknown, rawValue: string): string {
+ `#warnInvalidOption`(name: string, value: unknown): void {
LogService.warn(`format-bytes: invalid ${name} value "${String(value)}"`);
- return rawValue;
}
- `#hasValidConfiguration`(rawValue: string): boolean {
+ `#hasValidConfiguration`(): boolean {
if (!isDisplay(this.display)) {
- this.#warnInvalidOption('display', this.display, rawValue);
+ this.#warnInvalidOption('display', this.display);
return false;
}
if (!isUnitDisplay(this.unitDisplay)) {
- this.#warnInvalidOption('unit-display', this.unitDisplay, rawValue);
+ this.#warnInvalidOption('unit-display', this.unitDisplay);
return false;
}
if (this.unit !== undefined && !isUnit(this.unit)) {
- this.#warnInvalidOption('unit', this.unit, rawValue);
+ this.#warnInvalidOption('unit', this.unit);
return false;
}
return true;
}Update the call site at line 183 to if (!this.#hasValidConfiguration()) return rawValue;.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@projects/core/src/format-bytes/format-bytes.ts` around lines 148 - 167,
Update `#warnInvalidOption` to remove the rawValue parameter and return
type/value, then adjust its callers in `#hasValidConfiguration` to pass only the
option name and value. Remove rawValue from `#hasValidConfiguration` as well, and
update its call site to invoke it without arguments while preserving the
existing invalid-configuration return behavior.
nve-format-bytescomponent to convert byte counts into human-readable decimal or binary unitsSummary by CodeRabbit
New Features
Format Byteselement for displaying byte values in decimal or binary units.Documentation
Tests