mirror of
https://github.com/oven-sh/bun
synced 2026-02-09 02:18:47 +00:00
Compare commits
24 Commits
dylan/pyth
...
claude/ms-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
37a66dcc94 | ||
|
|
bace521b69 | ||
|
|
f0e6a89e59 | ||
|
|
086dc1c860 | ||
|
|
33e8e9bd50 | ||
|
|
2d892b5eb7 | ||
|
|
8b8c845708 | ||
|
|
f0be4443be | ||
|
|
0abed6e575 | ||
|
|
05c3a13366 | ||
|
|
1d95405da8 | ||
|
|
548a4f7859 | ||
|
|
709b8b9803 | ||
|
|
30f045dd41 | ||
|
|
6d1d14663b | ||
|
|
0ba7624824 | ||
|
|
f6d326d136 | ||
|
|
d85fd983f9 | ||
|
|
339a90ac1a | ||
|
|
33eac619ef | ||
|
|
b00bf53cb4 | ||
|
|
0972365627 | ||
|
|
bbf04de639 | ||
|
|
6bf431d707 |
@@ -18,6 +18,7 @@
|
||||
"fastify": "^5.0.0",
|
||||
"fdir": "^6.1.0",
|
||||
"mitata": "^1.0.25",
|
||||
"ms": "^4.0.0-nightly.202508271359",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"string-width": "7.1.0",
|
||||
@@ -363,7 +364,7 @@
|
||||
|
||||
"mitata": ["mitata@1.0.25", "", {}, "sha512-0v5qZtVW5vwj9FDvYfraR31BMDcRLkhSFWPTLaxx/Z3/EvScfVtAAWtMI2ArIbBcwh7P86dXh0lQWKiXQPlwYA=="],
|
||||
|
||||
"ms": ["ms@2.1.2", "", {}, "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="],
|
||||
"ms": ["ms@4.0.0-nightly.202508271359", "", {}, "sha512-WC/Eo7NzFrOV/RRrTaI0fxKVbNCzEy76j2VqNV8SxDf9D69gSE2Lh0QwYvDlhiYmheBYExAvEAxVf5NoN0cj2A=="],
|
||||
|
||||
"node-domexception": ["node-domexception@1.0.0", "", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="],
|
||||
|
||||
@@ -497,6 +498,8 @@
|
||||
|
||||
"cross-spawn/which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
|
||||
|
||||
"debug/ms": ["ms@2.1.2", "", {}, "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="],
|
||||
|
||||
"fastify/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
|
||||
|
||||
"light-my-request/process-warning": ["process-warning@4.0.1", "", {}, "sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q=="],
|
||||
|
||||
81
bench/ms/ms.mjs
Normal file
81
bench/ms/ms.mjs
Normal file
@@ -0,0 +1,81 @@
|
||||
import { ms } from "ms";
|
||||
import { bench, group, run } from "../runner.mjs";
|
||||
|
||||
const stringInputs = ["1s", "1m", "1h", "1d", "1w", "1y", "2 days", "10h", "2.5 hrs", "1.5h", "100ms"];
|
||||
const numberInputs = [1000, 60000, 3600000, 86400000, 604800000];
|
||||
|
||||
if (!process.argv.includes("--full")) {
|
||||
const str = ["1.5y"];
|
||||
if (typeof Bun === "undefined") {
|
||||
bench("ms (npm)", () => {
|
||||
ms(str[0]);
|
||||
});
|
||||
} else {
|
||||
bench("Bun.ms", () => {
|
||||
Bun.ms(str[0]);
|
||||
});
|
||||
bench("Bun.ms (statically inlined)", () => {
|
||||
Bun.ms("1.5y")
|
||||
})
|
||||
}
|
||||
} else {
|
||||
if (typeof Bun == "undefined" || process.argv.includes("--both")) {
|
||||
group("ms (npm)", () => {
|
||||
bench(`${stringInputs.length + numberInputs.length} inputs`, () => {
|
||||
for (const input of stringInputs) {
|
||||
ms(input);
|
||||
}
|
||||
for (const num of numberInputs) {
|
||||
ms(num);
|
||||
}
|
||||
});
|
||||
bench("string -> num", () => {
|
||||
ms(stringInputs[0]);
|
||||
});
|
||||
bench("num -> string", () => {
|
||||
ms(numberInputs[0]);
|
||||
});
|
||||
});
|
||||
}
|
||||
if (typeof Bun != "undefined") {
|
||||
group("Bun.ms", () => {
|
||||
bench(`${stringInputs.length + numberInputs.length} inputs`, () => {
|
||||
for (const input of stringInputs) {
|
||||
Bun.ms(input);
|
||||
}
|
||||
for (const num of numberInputs) {
|
||||
Bun.ms(num);
|
||||
}
|
||||
});
|
||||
|
||||
bench("string -> num", () => {
|
||||
Bun.ms(stringInputs[0]);
|
||||
});
|
||||
bench("num -> string", () => {
|
||||
Bun.ms(numberInputs[0]);
|
||||
});
|
||||
|
||||
bench("statically inlined", () => {
|
||||
Bun.ms("1s");
|
||||
Bun.ms("1m");
|
||||
Bun.ms("1h");
|
||||
Bun.ms("1d");
|
||||
Bun.ms("1w");
|
||||
Bun.ms("1y");
|
||||
Bun.ms("2 days");
|
||||
Bun.ms("10h");
|
||||
Bun.ms("2.5 hrs");
|
||||
Bun.ms("1.5h");
|
||||
Bun.ms("100ms");
|
||||
|
||||
Bun.ms(1000);
|
||||
Bun.ms(60000);
|
||||
Bun.ms(3600000);
|
||||
Bun.ms(86400000);
|
||||
Bun.ms(604800000);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
await run();
|
||||
@@ -15,6 +15,7 @@
|
||||
"fastify": "^5.0.0",
|
||||
"fdir": "^6.1.0",
|
||||
"mitata": "^1.0.25",
|
||||
"ms": "^4.0.0-nightly.202508271359",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"string-width": "7.1.0",
|
||||
|
||||
@@ -14,6 +14,4 @@ export function run(opts = {}) {
|
||||
|
||||
export const bench = Mitata.bench;
|
||||
|
||||
export function group(_name, fn) {
|
||||
return Mitata.group(fn);
|
||||
}
|
||||
export const group = Mitata.group;
|
||||
|
||||
37
packages/bun-types/bun.d.ts
vendored
37
packages/bun-types/bun.d.ts
vendored
@@ -5206,6 +5206,43 @@ declare module "bun" {
|
||||
*/
|
||||
function sleepSync(ms: number): void;
|
||||
|
||||
/**
|
||||
* Parse a time string and return milliseconds, or format milliseconds as a string.
|
||||
*
|
||||
* Drop-in replacement for the `ms` npm package with compile-time inlining support.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* Bun.ms("2d") // 172800000
|
||||
* Bun.ms("1.5h") // 5400000
|
||||
* Bun.ms("1m") // 60000
|
||||
* Bun.ms("5s") // 5000
|
||||
* Bun.ms("100ms") // 100
|
||||
* Bun.ms("1mo") // 2629800000
|
||||
* Bun.ms("1y") // 31557600000
|
||||
* Bun.ms("100") // 100
|
||||
* Bun.ms(60000) // "1m"
|
||||
* Bun.ms(60000, { long: true }) // "1 minute"
|
||||
* Bun.ms("invalid") // NaN
|
||||
* ```
|
||||
*
|
||||
* Supports these units:
|
||||
* - `ms`, `millisecond`, `milliseconds`
|
||||
* - `s`, `sec`, `second`, `seconds`
|
||||
* - `m`, `min`, `minute`, `minutes`
|
||||
* - `h`, `hr`, `hour`, `hours`
|
||||
* - `d`, `day`, `days`
|
||||
* - `w`, `week`, `weeks`
|
||||
* - `mo`, `month`, `months`
|
||||
* - `y`, `yr`, `year`, `years`
|
||||
*
|
||||
* @param value - Time string to parse or milliseconds to format
|
||||
* @param options - Formatting options (when value is a number)
|
||||
* @returns Milliseconds (for string) or formatted string (for number). Returns NaN for invalid strings.
|
||||
*/
|
||||
function ms(value: string): number;
|
||||
function ms(value: number, options?: { long?: boolean }): string;
|
||||
|
||||
/**
|
||||
* Hash `input` using [SHA-2 512/256](https://en.wikipedia.org/wiki/SHA-2#Comparison_of_SHA_functions)
|
||||
*
|
||||
|
||||
@@ -2771,6 +2771,18 @@ pub fn NewParser_(
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-inline Bun.ms when imported from "bun"
|
||||
// This allows: import { ms } from "bun"; ms("1s") -> 1000
|
||||
// without requiring `with { type: "macro" }`
|
||||
if (comptime allow_macros) {
|
||||
if (strings.eqlComptime(path.text, "bun") and strings.eqlComptime(item.alias, "ms")) {
|
||||
try p.macro.refs.put(ref, .{
|
||||
.import_record_id = stmt.import_record_index,
|
||||
.name = item.alias,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (macro_remap) |*remap| {
|
||||
if (remap.get(item.alias)) |remapped_path| {
|
||||
const new_import_id = p.addImportRecord(.stmt, path.loc, remapped_path);
|
||||
|
||||
@@ -1149,6 +1149,51 @@ pub fn VisitExpr(
|
||||
}
|
||||
return expr;
|
||||
}
|
||||
|
||||
/// Helper function to inline Bun.ms() and import { ms } from "bun" calls
|
||||
/// Returns the inlined expression if successful, null otherwise
|
||||
fn inlineMsCall(p: *P, e_: *const E.Call, loc: logger.Loc) ?Expr {
|
||||
const ms_module = @import("../bun.js/api/bun/ms.zig");
|
||||
|
||||
if (e_.args.len == 1) {
|
||||
const arg = e_.args.at(0).unwrapInlined();
|
||||
if (arg.data == .e_string and arg.data.e_string.isUTF8()) {
|
||||
// ms("string") -> number
|
||||
const str = arg.data.e_string.slice(p.allocator);
|
||||
const ms_value = ms_module.parse(str) orelse std.math.nan(f64);
|
||||
return p.newExpr(E.Number{ .value = ms_value }, loc);
|
||||
} else if (arg.data == .e_number) {
|
||||
// ms(number) -> "string"
|
||||
const num = arg.data.e_number.value;
|
||||
if (std.math.isNan(num) or std.math.isInf(num)) return null;
|
||||
if (ms_module.format(p.allocator, num, false)) |formatted| {
|
||||
return p.newExpr(E.String.init(formatted), loc);
|
||||
} else |_| return null;
|
||||
}
|
||||
} else if (e_.args.len == 2) {
|
||||
// ms(number, { long: true/false }) -> "string"
|
||||
const arg = e_.args.at(0).unwrapInlined();
|
||||
const opts = e_.args.at(1).unwrapInlined();
|
||||
|
||||
if (arg.data == .e_number and opts.data == .e_object) {
|
||||
const num = arg.data.e_number.value;
|
||||
if (std.math.isNan(num) or std.math.isInf(num)) return null;
|
||||
|
||||
var long = false;
|
||||
if (opts.data.e_object.get("long")) |val| {
|
||||
if (val.data == .e_boolean) {
|
||||
long = val.data.e_boolean.value;
|
||||
}
|
||||
}
|
||||
|
||||
if (ms_module.format(p.allocator, num, long)) |formatted| {
|
||||
return p.newExpr(E.String.init(formatted), loc);
|
||||
} else |_| return null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn e_call(p: *P, expr: Expr, in: ExprIn) Expr {
|
||||
const e_ = expr.data.e_call;
|
||||
p.call_target = e_.target.data;
|
||||
@@ -1387,6 +1432,16 @@ pub fn VisitExpr(
|
||||
|
||||
const name = macro_ref_data.name orelse e_.target.data.e_dot.name;
|
||||
const record = &p.import_records.items[macro_ref_data.import_record_id];
|
||||
|
||||
// Special case: import { ms } from "bun" gets compile-time constant folding
|
||||
// instead of macro execution. This allows ms("1s") -> 1000 inline optimization.
|
||||
if (record.tag == .bun and strings.eqlComptime(name, "ms")) {
|
||||
if (inlineMsCall(p, e_, expr.loc)) |result| {
|
||||
return result;
|
||||
}
|
||||
// If we can't inline (dynamic args), fall through to normal macro execution
|
||||
}
|
||||
|
||||
const copied = Expr{ .loc = expr.loc, .data = .{ .e_call = e_ } };
|
||||
const start_error_count = p.log.msgs.items.len;
|
||||
p.macro_call_count += 1;
|
||||
@@ -1497,6 +1552,20 @@ pub fn VisitExpr(
|
||||
}
|
||||
};
|
||||
|
||||
// Implement constant folding for Bun.ms("1s") -> 1000 and Bun.ms(1000) -> "1s"
|
||||
if (p.should_fold_typescript_constant_expressions or p.options.features.inlining) {
|
||||
if (e_.target.data.as(.e_dot)) |dot| {
|
||||
if (dot.target.data == .e_identifier and strings.eqlComptime(dot.name, "ms")) {
|
||||
const symbol = &p.symbols.items[dot.target.data.e_identifier.ref.innerIndex()];
|
||||
if (symbol.kind == .unbound and strings.eqlComptime(symbol.original_name, "Bun")) {
|
||||
if (inlineMsCall(p, e_, expr.loc)) |result| {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return expr;
|
||||
}
|
||||
pub fn e_new(p: *P, expr: Expr, _: ExprIn) Expr {
|
||||
|
||||
@@ -25,6 +25,7 @@ pub const BunObject = struct {
|
||||
pub const jest = toJSCallback(@import("../test/jest.zig").Jest.call);
|
||||
pub const listen = toJSCallback(host_fn.wrapStaticMethod(api.Listener, "listen", false));
|
||||
pub const mmap = toJSCallback(Bun.mmapFile);
|
||||
pub const ms = toJSCallback(@import("./bun/ms.zig").jsFunction);
|
||||
pub const nanoseconds = toJSCallback(Bun.nanoseconds);
|
||||
pub const openInEditor = toJSCallback(Bun.openInEditor);
|
||||
pub const registerMacro = toJSCallback(Bun.registerMacro);
|
||||
@@ -161,6 +162,7 @@ pub const BunObject = struct {
|
||||
@export(&BunObject.jest, .{ .name = callbackName("jest") });
|
||||
@export(&BunObject.listen, .{ .name = callbackName("listen") });
|
||||
@export(&BunObject.mmap, .{ .name = callbackName("mmap") });
|
||||
@export(&BunObject.ms, .{ .name = callbackName("ms") });
|
||||
@export(&BunObject.nanoseconds, .{ .name = callbackName("nanoseconds") });
|
||||
@export(&BunObject.openInEditor, .{ .name = callbackName("openInEditor") });
|
||||
@export(&BunObject.registerMacro, .{ .name = callbackName("registerMacro") });
|
||||
|
||||
238
src/bun.js/api/bun/ms.zig
Normal file
238
src/bun.js/api/bun/ms.zig
Normal file
@@ -0,0 +1,238 @@
|
||||
/// Parse a time string like "2d", "1.5h", "5m" to milliseconds
|
||||
pub fn parse(input: []const u8) ?f64 {
|
||||
if (input.len == 0 or input.len > 100) return null;
|
||||
|
||||
var i: usize = 0;
|
||||
|
||||
next: switch (input[i]) {
|
||||
'-',
|
||||
'.',
|
||||
'0'...'9',
|
||||
=> {
|
||||
i += 1;
|
||||
if (i < input.len) {
|
||||
continue :next input[i];
|
||||
}
|
||||
break :next;
|
||||
},
|
||||
' ',
|
||||
'a'...'z',
|
||||
'A'...'Z',
|
||||
=> {
|
||||
break :next;
|
||||
},
|
||||
else => {
|
||||
return null;
|
||||
},
|
||||
}
|
||||
|
||||
const value = std.fmt.parseFloat(f64, input[0..i]) catch return null;
|
||||
|
||||
const unit = strings.trimLeadingChar(input[i..], ' ');
|
||||
if (unit.len == 0) return value;
|
||||
|
||||
if (MultiplierMap.getASCIIICaseInsensitive(unit)) |m| {
|
||||
return value * m;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// Years (365.25 days to account for leap years)
|
||||
const ms_per_year = std.time.ms_per_day * 365.25;
|
||||
const ms_per_month = std.time.ms_per_day * (365.25 / 12.0);
|
||||
|
||||
const MultiplierMap = bun.ComptimeStringMap(f64, .{
|
||||
// Years (365.25 days to account for leap years)
|
||||
.{ "y", ms_per_year },
|
||||
.{ "yr", ms_per_year },
|
||||
.{ "yrs", ms_per_year },
|
||||
.{ "year", ms_per_year },
|
||||
.{ "years", ms_per_year },
|
||||
|
||||
// Months (30.4375 days average)
|
||||
.{ "mo", ms_per_month },
|
||||
.{ "month", ms_per_month },
|
||||
.{ "months", ms_per_month },
|
||||
|
||||
// Weeks
|
||||
.{ "w", std.time.ms_per_week },
|
||||
.{ "week", std.time.ms_per_week },
|
||||
.{ "weeks", std.time.ms_per_week },
|
||||
|
||||
// Days
|
||||
.{ "d", std.time.ms_per_day },
|
||||
.{ "day", std.time.ms_per_day },
|
||||
.{ "days", std.time.ms_per_day },
|
||||
|
||||
// Hours
|
||||
.{ "h", std.time.ms_per_hour },
|
||||
.{ "hr", std.time.ms_per_hour },
|
||||
.{ "hrs", std.time.ms_per_hour },
|
||||
.{ "hour", std.time.ms_per_hour },
|
||||
.{ "hours", std.time.ms_per_hour },
|
||||
|
||||
// Minutes
|
||||
.{ "m", std.time.ms_per_min },
|
||||
.{ "min", std.time.ms_per_min },
|
||||
.{ "mins", std.time.ms_per_min },
|
||||
.{ "minute", std.time.ms_per_min },
|
||||
.{ "minutes", std.time.ms_per_min },
|
||||
|
||||
// Seconds
|
||||
.{ "s", std.time.ms_per_s },
|
||||
.{ "sec", std.time.ms_per_s },
|
||||
.{ "secs", std.time.ms_per_s },
|
||||
.{ "second", std.time.ms_per_s },
|
||||
.{ "seconds", std.time.ms_per_s },
|
||||
|
||||
// Milliseconds
|
||||
.{ "ms", 1 },
|
||||
.{ "msec", 1 },
|
||||
.{ "msecs", 1 },
|
||||
.{ "millisecond", 1 },
|
||||
.{ "milliseconds", 1 },
|
||||
});
|
||||
|
||||
// To keep the behavior consistent with JavaScript, we can't use @round
|
||||
// Zig's @round uses "round half away from zero": ties round away from zero (2.5→3, -2.5→-3)
|
||||
// JavaScript's Math.round uses "round half toward +∞": ties round toward positive infinity (2.5→3, -2.5→-2)
|
||||
// This implementation: floor(x) + 1 if fractional part >= 0.5, else floor(x)
|
||||
fn jsMathRound(x: f64) i64 {
|
||||
const i: f64 = @ceil(x);
|
||||
if ((i - 0.5) > x) return @intFromFloat(i - 1.0);
|
||||
return @intFromFloat(i);
|
||||
}
|
||||
|
||||
/// Format milliseconds to a human-readable string
|
||||
pub fn format(allocator: std.mem.Allocator, ms: f64, long: bool) ![]u8 {
|
||||
const abs_ms = @abs(ms);
|
||||
|
||||
// Years
|
||||
if (abs_ms >= ms_per_year) {
|
||||
const years = jsMathRound(ms / ms_per_year);
|
||||
if (long) {
|
||||
const plural = abs_ms >= ms_per_year * 1.5;
|
||||
return std.fmt.allocPrint(allocator, "{d} year{s}", .{ years, if (plural) "s" else "" });
|
||||
}
|
||||
return std.fmt.allocPrint(allocator, "{d}y", .{years});
|
||||
}
|
||||
|
||||
// Months
|
||||
if (abs_ms >= ms_per_month) {
|
||||
const months = jsMathRound(ms / ms_per_month);
|
||||
if (long) {
|
||||
const plural = abs_ms >= ms_per_month * 1.5;
|
||||
return std.fmt.allocPrint(allocator, "{d} month{s}", .{ months, if (plural) "s" else "" });
|
||||
}
|
||||
return std.fmt.allocPrint(allocator, "{d}mo", .{months});
|
||||
}
|
||||
|
||||
// Weeks
|
||||
if (abs_ms >= std.time.ms_per_week) {
|
||||
const weeks = jsMathRound(ms / std.time.ms_per_week);
|
||||
if (long) {
|
||||
const plural = abs_ms >= std.time.ms_per_week * 1.5;
|
||||
return std.fmt.allocPrint(allocator, "{d} week{s}", .{ weeks, if (plural) "s" else "" });
|
||||
}
|
||||
return std.fmt.allocPrint(allocator, "{d}w", .{weeks});
|
||||
}
|
||||
|
||||
// Days
|
||||
if (abs_ms >= std.time.ms_per_day) {
|
||||
const days = jsMathRound(ms / std.time.ms_per_day);
|
||||
if (long) {
|
||||
const plural = abs_ms >= std.time.ms_per_day * 1.5;
|
||||
return std.fmt.allocPrint(allocator, "{d} day{s}", .{ days, if (plural) "s" else "" });
|
||||
}
|
||||
return std.fmt.allocPrint(allocator, "{d}d", .{days});
|
||||
}
|
||||
|
||||
// Hours
|
||||
if (abs_ms >= std.time.ms_per_hour) {
|
||||
const hours = jsMathRound(ms / std.time.ms_per_hour);
|
||||
if (long) {
|
||||
const plural = abs_ms >= std.time.ms_per_hour * 1.5;
|
||||
return std.fmt.allocPrint(allocator, "{d} hour{s}", .{ hours, if (plural) "s" else "" });
|
||||
}
|
||||
return std.fmt.allocPrint(allocator, "{d}h", .{hours});
|
||||
}
|
||||
|
||||
// Minutes
|
||||
if (abs_ms >= std.time.ms_per_min) {
|
||||
const minutes = jsMathRound(ms / std.time.ms_per_min);
|
||||
if (long) {
|
||||
const plural = abs_ms >= std.time.ms_per_min * 1.5;
|
||||
return std.fmt.allocPrint(allocator, "{d} minute{s}", .{ minutes, if (plural) "s" else "" });
|
||||
}
|
||||
return std.fmt.allocPrint(allocator, "{d}m", .{minutes});
|
||||
}
|
||||
|
||||
// Seconds
|
||||
if (abs_ms >= std.time.ms_per_s) {
|
||||
const seconds = jsMathRound(ms / std.time.ms_per_s);
|
||||
if (long) {
|
||||
const plural = abs_ms >= std.time.ms_per_s * 1.5;
|
||||
return std.fmt.allocPrint(allocator, "{d} second{s}", .{ seconds, if (plural) "s" else "" });
|
||||
}
|
||||
return std.fmt.allocPrint(allocator, "{d}s", .{seconds});
|
||||
}
|
||||
|
||||
// Milliseconds
|
||||
const ms_int: i64 = @intFromFloat(ms);
|
||||
if (long) {
|
||||
return std.fmt.allocPrint(allocator, "{d} ms", .{ms_int});
|
||||
}
|
||||
return std.fmt.allocPrint(allocator, "{d}ms", .{ms_int});
|
||||
}
|
||||
|
||||
/// JavaScript function: Bun.ms(value, options?)
|
||||
pub fn jsFunction(
|
||||
globalThis: *JSGlobalObject,
|
||||
callframe: *jsc.CallFrame,
|
||||
) JSError!jsc.JSValue {
|
||||
const input, const options = callframe.argumentsAsArray(2);
|
||||
|
||||
// If input is a number, format it to a string
|
||||
if (input.isNumber()) {
|
||||
const ms_value = input.asNumber();
|
||||
|
||||
if (std.math.isNan(ms_value) or std.math.isInf(ms_value)) {
|
||||
return globalThis.throwInvalidArguments("Value must be a finite number", .{});
|
||||
}
|
||||
|
||||
var long = false;
|
||||
if (options.isObject()) {
|
||||
if (try options.get(globalThis, "long")) |long_value| {
|
||||
long = long_value.toBoolean();
|
||||
}
|
||||
}
|
||||
|
||||
const result = try format(bun.default_allocator, ms_value, long);
|
||||
|
||||
var str = String.createExternalGloballyAllocated(.latin1, result);
|
||||
return str.transferToJS(globalThis);
|
||||
}
|
||||
|
||||
// If input is a string, parse it to milliseconds
|
||||
if (input.isString()) {
|
||||
const str = try input.toSlice(globalThis, bun.default_allocator);
|
||||
defer str.deinit();
|
||||
|
||||
const result = parse(str.slice()) orelse std.math.nan(f64);
|
||||
return JSValue.jsNumber(result);
|
||||
}
|
||||
|
||||
return globalThis.throwInvalidArguments("Bun.ms() expects a string or number", .{});
|
||||
}
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const bun = @import("bun");
|
||||
const JSError = bun.JSError;
|
||||
const String = bun.String;
|
||||
const strings = bun.strings;
|
||||
|
||||
const jsc = bun.jsc;
|
||||
const JSGlobalObject = jsc.JSGlobalObject;
|
||||
const JSValue = jsc.JSValue;
|
||||
@@ -55,6 +55,7 @@
|
||||
macro(jest) \
|
||||
macro(listen) \
|
||||
macro(mmap) \
|
||||
macro(ms) \
|
||||
macro(nanoseconds) \
|
||||
macro(openInEditor) \
|
||||
macro(registerMacro) \
|
||||
|
||||
@@ -759,6 +759,7 @@ JSC_DEFINE_HOST_FUNCTION(functionFileURLToPath, (JSC::JSGlobalObject * globalObj
|
||||
udpSocket BunObject_callback_udpSocket DontDelete|Function 1
|
||||
main bunObjectMain DontDelete|CustomAccessor
|
||||
mmap BunObject_callback_mmap DontDelete|Function 1
|
||||
ms BunObject_callback_ms DontDelete|Function 2
|
||||
nanoseconds functionBunNanoseconds DontDelete|Function 0
|
||||
openInEditor BunObject_callback_openInEditor DontDelete|Function 1
|
||||
origin BunObject_lazyPropCb_wrap_origin DontEnum|ReadOnly|DontDelete|PropertyCallback
|
||||
|
||||
65
test/integration/bun-types/fixture/ms.ts
Normal file
65
test/integration/bun-types/fixture/ms.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import { expectType } from "./utilities";
|
||||
|
||||
// Test string literal autocomplete and return types
|
||||
expectType(Bun.ms("1s")).is<number>();
|
||||
expectType(Bun.ms("1m")).is<number>();
|
||||
expectType(Bun.ms("1h")).is<number>();
|
||||
expectType(Bun.ms("1d")).is<number>();
|
||||
expectType(Bun.ms("1w")).is<number>();
|
||||
expectType(Bun.ms("1mo")).is<number>();
|
||||
expectType(Bun.ms("1y")).is<number>();
|
||||
|
||||
// Test with all unit variations
|
||||
expectType(Bun.ms("1ms")).is<number>();
|
||||
expectType(Bun.ms("1millisecond")).is<number>();
|
||||
expectType(Bun.ms("1milliseconds")).is<number>();
|
||||
expectType(Bun.ms("1second")).is<number>();
|
||||
expectType(Bun.ms("1 second")).is<number>();
|
||||
expectType(Bun.ms("1 seconds")).is<number>();
|
||||
expectType(Bun.ms("1minute")).is<number>();
|
||||
expectType(Bun.ms("1 minute")).is<number>();
|
||||
expectType(Bun.ms("1hour")).is<number>();
|
||||
expectType(Bun.ms("1 hour")).is<number>();
|
||||
expectType(Bun.ms("1day")).is<number>();
|
||||
expectType(Bun.ms("1 day")).is<number>();
|
||||
expectType(Bun.ms("1week")).is<number>();
|
||||
expectType(Bun.ms("1 week")).is<number>();
|
||||
expectType(Bun.ms("1month")).is<number>();
|
||||
expectType(Bun.ms("1 month")).is<number>();
|
||||
expectType(Bun.ms("1year")).is<number>();
|
||||
expectType(Bun.ms("1 year")).is<number>();
|
||||
|
||||
// Test with decimals and negatives
|
||||
expectType(Bun.ms("1.5h")).is<number>();
|
||||
expectType(Bun.ms("-1s")).is<number>();
|
||||
expectType(Bun.ms(".5m")).is<number>();
|
||||
expectType(Bun.ms("-.5h")).is<number>();
|
||||
|
||||
// Test number input (formatting)
|
||||
expectType(Bun.ms(1000)).is<string>();
|
||||
expectType(Bun.ms(60000)).is<string>();
|
||||
expectType(Bun.ms(3600000)).is<string>();
|
||||
|
||||
// Test with options
|
||||
expectType(Bun.ms(1000, { long: true })).is<string>();
|
||||
expectType(Bun.ms(60000, { long: false })).is<string>();
|
||||
|
||||
// Test generic string input (for dynamic values)
|
||||
const dynamicString: string = "1s";
|
||||
expectType(Bun.ms(dynamicString)).is<number>();
|
||||
|
||||
// Should NOT accept options with string input
|
||||
// @ts-expect-error - options only valid with number input
|
||||
Bun.ms("1s", { long: true });
|
||||
|
||||
// Number with options should work
|
||||
const formatted = Bun.ms(1000, { long: true });
|
||||
expectType(formatted).is<string>();
|
||||
|
||||
// Options should be optional
|
||||
const shortFormat = Bun.ms(1000);
|
||||
expectType(shortFormat).is<string>();
|
||||
|
||||
// Test that invalid inputs still return number (NaN is a number)
|
||||
expectType(Bun.ms("invalid")).is<number>();
|
||||
expectType(Bun.ms("")).is<number>();
|
||||
388
test/js/bun/util/ms.test.ts
Normal file
388
test/js/bun/util/ms.test.ts
Normal file
@@ -0,0 +1,388 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { tempDirWithFiles } from "harness";
|
||||
import { join } from "path";
|
||||
|
||||
describe("Bun.ms - parse (string to number)", () => {
|
||||
test("short strings", () => {
|
||||
const cases = [
|
||||
["100", 100],
|
||||
["1m", 60000],
|
||||
["1h", 3600000],
|
||||
["2d", 172800000],
|
||||
["3w", 1814400000],
|
||||
["1s", 1000],
|
||||
["100ms", 100],
|
||||
["1y", 31557600000],
|
||||
["1.5h", 5400000],
|
||||
["1 s", 1000],
|
||||
["-.5h", -1800000],
|
||||
["-1h", -3600000],
|
||||
["-200", -200],
|
||||
[".5ms", 0.5],
|
||||
] as const;
|
||||
|
||||
for (const [input, expected] of cases) {
|
||||
expect(Bun.ms(input)).toBe(expected);
|
||||
}
|
||||
});
|
||||
|
||||
test("long strings", () => {
|
||||
const cases = [
|
||||
["53 milliseconds", 53],
|
||||
["17 msecs", 17],
|
||||
["1 sec", 1000],
|
||||
["1 min", 60000],
|
||||
["1 hr", 3600000],
|
||||
["2 days", 172800000],
|
||||
["1 week", 604800000],
|
||||
["1 month", 2629800000],
|
||||
["1 year", 31557600000],
|
||||
["1.5 hours", 5400000],
|
||||
["-100 milliseconds", -100],
|
||||
["-1.5 hours", -5400000],
|
||||
["-10 minutes", -600000],
|
||||
] as const;
|
||||
|
||||
for (const [input, expected] of cases) {
|
||||
expect(Bun.ms(input)).toBe(expected);
|
||||
}
|
||||
});
|
||||
|
||||
test("case insensitive", () => {
|
||||
const cases = [
|
||||
["1M", 60000],
|
||||
["1H", 3600000],
|
||||
["2D", 172800000],
|
||||
["3W", 1814400000],
|
||||
["1S", 1000],
|
||||
["1MS", 1],
|
||||
["1Y", 31557600000],
|
||||
["1 HOUR", 3600000],
|
||||
["1 DAY", 86400000],
|
||||
["1 WEEK", 604800000],
|
||||
] as const;
|
||||
|
||||
for (const [input, expected] of cases) {
|
||||
expect(Bun.ms(input)).toBe(expected);
|
||||
}
|
||||
});
|
||||
|
||||
test("invalid inputs", () => {
|
||||
const cases = [
|
||||
["", "empty string"],
|
||||
[" ", "whitespace only"],
|
||||
["foo", "invalid unit"],
|
||||
["1x", "unknown unit"],
|
||||
["1.2.3s", "multiple dots"],
|
||||
] as const;
|
||||
|
||||
for (const [input] of cases) {
|
||||
expect(Bun.ms(input)).toBeNaN();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("Bun.ms - format (number to string)", () => {
|
||||
test("short format", () => {
|
||||
const cases = [
|
||||
[0, "0ms"],
|
||||
[500, "500ms"],
|
||||
[-500, "-500ms"],
|
||||
[1000, "1s"],
|
||||
[10000, "10s"],
|
||||
[60000, "1m"],
|
||||
[600000, "10m"],
|
||||
[3600000, "1h"],
|
||||
[86400000, "1d"],
|
||||
[604800000, "1w"],
|
||||
[2629800000, "1mo"],
|
||||
[31557600001, "1y"],
|
||||
[234234234, "3d"],
|
||||
[-234234234, "-3d"],
|
||||
] as const;
|
||||
|
||||
for (const [input, expected] of cases) {
|
||||
expect(Bun.ms(input)).toBe(expected);
|
||||
}
|
||||
});
|
||||
|
||||
test("long format", () => {
|
||||
const cases = [
|
||||
[0, "0 ms"],
|
||||
[500, "500 ms"],
|
||||
[-500, "-500 ms"],
|
||||
[1000, "1 second"],
|
||||
[1001, "1 second"],
|
||||
[1499, "1 second"],
|
||||
[1500, "2 seconds"],
|
||||
[10000, "10 seconds"],
|
||||
[60000, "1 minute"],
|
||||
[600000, "10 minutes"],
|
||||
[3600000, "1 hour"],
|
||||
[86400000, "1 day"],
|
||||
[172800000, "2 days"],
|
||||
[604800000, "1 week"],
|
||||
[2629800000, "1 month"],
|
||||
[31557600001, "1 year"],
|
||||
[234234234, "3 days"],
|
||||
[-234234234, "-3 days"],
|
||||
] as const;
|
||||
|
||||
for (const [input, expected] of cases) {
|
||||
expect(Bun.ms(input, { long: true })).toBe(expected);
|
||||
}
|
||||
});
|
||||
|
||||
test("rounding behavior matches JavaScript Math.round and npm ms", () => {
|
||||
// JavaScript Math.round uses "round half toward +∞"
|
||||
// Positive ties (X.5) round up (away from zero): 2.5 → 3
|
||||
// Negative ties (X.5) round up toward zero: -2.5 → -2
|
||||
// This is different from Zig's @round which rounds away from zero
|
||||
// (so we made our own jsMathRound function)
|
||||
const cases = [
|
||||
// Positive ties - should round up
|
||||
[1000, "1s", "1 second"],
|
||||
[1500, "2s", "2 seconds"],
|
||||
[2500, "3s", "3 seconds"],
|
||||
[3500, "4s", "4 seconds"],
|
||||
[4500, "5s", "5 seconds"],
|
||||
|
||||
// Negative ties - should round toward zero (toward +∞)
|
||||
[-1000, "-1s", "-1 second"],
|
||||
[-1500, "-1s", "-1 seconds"],
|
||||
[-2500, "-2s", "-2 seconds"],
|
||||
[-3500, "-3s", "-3 seconds"],
|
||||
[-4500, "-4s", "-4 seconds"],
|
||||
|
||||
[9000000, "3h", "3 hours"],
|
||||
[-9000000, "-2h", "-2 hours"],
|
||||
|
||||
[216000000, "3d", "3 days"],
|
||||
[-216000000, "-2d", "-2 days"],
|
||||
] as const;
|
||||
|
||||
for (const [input, expectedShort, expectedLong] of cases) {
|
||||
expect(Bun.ms(input)).toBe(expectedShort);
|
||||
expect(Bun.ms(input, { long: true })).toBe(expectedLong);
|
||||
}
|
||||
});
|
||||
|
||||
test("invalid number inputs", () => {
|
||||
expect(() => Bun.ms(NaN)).toThrow();
|
||||
expect(() => Bun.ms(Infinity)).toThrow();
|
||||
expect(() => Bun.ms(-Infinity)).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Bun.ms - comprehensive coverage", () => {
|
||||
test("all time units", () => {
|
||||
const cases = [
|
||||
// Milliseconds
|
||||
["1ms", 1],
|
||||
["1millisecond", 1],
|
||||
["1milliseconds", 1],
|
||||
["1msec", 1],
|
||||
["1msecs", 1],
|
||||
// Seconds
|
||||
["1s", 1000],
|
||||
["1sec", 1000],
|
||||
["1secs", 1000],
|
||||
["1second", 1000],
|
||||
["1seconds", 1000],
|
||||
["2seconds", 2000],
|
||||
// Minutes
|
||||
["1m", 60000],
|
||||
["1min", 60000],
|
||||
["1mins", 60000],
|
||||
["1minute", 60000],
|
||||
["1minutes", 60000],
|
||||
["2minutes", 120000],
|
||||
// Hours
|
||||
["1h", 3600000],
|
||||
["1hr", 3600000],
|
||||
["1hrs", 3600000],
|
||||
["1hour", 3600000],
|
||||
["1hours", 3600000],
|
||||
["2hours", 7200000],
|
||||
// Days
|
||||
["1d", 86400000],
|
||||
["1day", 86400000],
|
||||
["1days", 86400000],
|
||||
["2days", 172800000],
|
||||
// Weeks
|
||||
["1w", 604800000],
|
||||
["1week", 604800000],
|
||||
["1weeks", 604800000],
|
||||
["2weeks", 1209600000],
|
||||
// Months
|
||||
["1mo", 2629800000],
|
||||
["1month", 2629800000],
|
||||
["1months", 2629800000],
|
||||
["2months", 5259600000],
|
||||
// Years
|
||||
["1y", 31557600000],
|
||||
["1yr", 31557600000],
|
||||
["1yrs", 31557600000],
|
||||
["1year", 31557600000],
|
||||
["1years", 31557600000],
|
||||
["2years", 63115200000],
|
||||
] as const;
|
||||
|
||||
for (const [input, expected] of cases) {
|
||||
expect(Bun.ms(input)).toBe(expected);
|
||||
}
|
||||
});
|
||||
|
||||
test("decimals and negatives", () => {
|
||||
const cases = [
|
||||
["1.5s", 1500],
|
||||
["1.5h", 5400000],
|
||||
["0.5d", 43200000],
|
||||
["-1s", -1000],
|
||||
["-1.5h", -5400000],
|
||||
["-0.5d", -43200000],
|
||||
[".5s", 500],
|
||||
["-.5s", -500],
|
||||
] as const;
|
||||
|
||||
for (const [input, expected] of cases) {
|
||||
expect(Bun.ms(input)).toBe(expected);
|
||||
}
|
||||
});
|
||||
|
||||
test("whitespace handling", () => {
|
||||
const cases = [
|
||||
["1 s", 1000],
|
||||
["1 s", 1000],
|
||||
["1 s", 1000],
|
||||
[" 1s", NaN],
|
||||
["1s ", NaN],
|
||||
[" 1s ", NaN],
|
||||
["1 second", 1000],
|
||||
["1 seconds", 1000],
|
||||
[" 1 second ", NaN],
|
||||
] as const;
|
||||
|
||||
for (const [input, expected] of cases) {
|
||||
expect(Bun.ms(input)).toBe(expected);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test("Bun.ms - dynamic values at runtime", () => {
|
||||
{
|
||||
function getNumber() {
|
||||
return Math.random() > 0.5 ? 1 : 2;
|
||||
}
|
||||
const days = getNumber();
|
||||
const result = Bun.ms(days + "days");
|
||||
|
||||
// Should be either 1 day or 2 days
|
||||
expect(result === 86400000 || result === 172800000).toBe(true);
|
||||
}
|
||||
|
||||
{
|
||||
function getHours() {
|
||||
return 5;
|
||||
}
|
||||
const result = Bun.ms(String(getHours()) + "h");
|
||||
expect(result).toBe(18000000); // 5 hours
|
||||
}
|
||||
|
||||
{
|
||||
const timeStr = "10m";
|
||||
const result = Bun.ms(timeStr);
|
||||
expect(result).toBe(600000);
|
||||
}
|
||||
|
||||
{
|
||||
function getMs() {
|
||||
return 60000;
|
||||
}
|
||||
const result = Bun.ms(getMs());
|
||||
expect(result).toBe("1m");
|
||||
}
|
||||
});
|
||||
|
||||
test("Bun.ms - static string formatting", () => {
|
||||
expect(Bun.ms("5s")).toBe(5000);
|
||||
expect(Bun.ms(5000, { long: true })).toBe("5 seconds");
|
||||
expect(Bun.ms(5000, { long: false })).toBe("5s");
|
||||
});
|
||||
|
||||
test("Bun.ms - bundler output", async () => {
|
||||
const dir = tempDirWithFiles("ms-bundler", {
|
||||
"entry.ts": `
|
||||
const dynamic = () => Math.random() > 0.5 ? 1 : 2;
|
||||
|
||||
export const values = {
|
||||
// Valid strings - should inline to numbers
|
||||
oneSecond: Bun.ms("1s"),
|
||||
oneMinute: Bun.ms("1m"),
|
||||
oneHour: Bun.ms("1h"),
|
||||
oneDay: Bun.ms("1d"),
|
||||
twoWeeks: Bun.ms("2w"),
|
||||
halfYear: Bun.ms("0.5y"),
|
||||
withSpaces: Bun.ms("5 minutes"),
|
||||
negative: Bun.ms("-10s"),
|
||||
decimal: Bun.ms("1.5h"),
|
||||
justNumber: Bun.ms("100"),
|
||||
caseInsensitive: Bun.ms("2D"),
|
||||
|
||||
// Invalid strings - should inline to NaN
|
||||
invalid: Bun.ms("invalid"),
|
||||
empty: Bun.ms(""),
|
||||
|
||||
// Number inputs - should inline to strings
|
||||
formatShort: Bun.ms(1000),
|
||||
formatLong: Bun.ms(60000, { long: true }),
|
||||
|
||||
// dynamic should not inline
|
||||
dynamic: Bun.ms(\`\$\{dynamic()\}s\`),
|
||||
|
||||
// test
|
||||
dontBeWeird: abc.ms("1s"),
|
||||
};
|
||||
`,
|
||||
});
|
||||
|
||||
const result = await Bun.build({
|
||||
entrypoints: [join(dir, "entry.ts")],
|
||||
minify: {
|
||||
syntax: true,
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.outputs).toHaveLength(1);
|
||||
|
||||
let output = await result.outputs[0].text();
|
||||
output = output.replace(/\/\/.*?\/entry\.ts/, "// entry.ts");
|
||||
|
||||
expect(output).toMatchInlineSnapshot(`
|
||||
"// entry.ts
|
||||
var dynamic = () => Math.random() > 0.5 ? 1 : 2, values = {
|
||||
oneSecond: 1000,
|
||||
oneMinute: 60000,
|
||||
oneHour: 3600000,
|
||||
oneDay: 86400000,
|
||||
twoWeeks: 1209600000,
|
||||
halfYear: 15778800000,
|
||||
withSpaces: 300000,
|
||||
negative: -1e4,
|
||||
decimal: 5400000,
|
||||
justNumber: 100,
|
||||
caseInsensitive: 172800000,
|
||||
invalid: NaN,
|
||||
empty: NaN,
|
||||
formatShort: "1s",
|
||||
formatLong: "1 minute",
|
||||
dynamic: Bun.ms(\`\${dynamic()}s\`),
|
||||
dontBeWeird: abc.ms("1s")
|
||||
};
|
||||
export {
|
||||
values
|
||||
};
|
||||
"
|
||||
`);
|
||||
});
|
||||
Reference in New Issue
Block a user