diff --git a/docs/api/calendar.md b/docs/api/calendar.md new file mode 100644 index 0000000..99c1756 --- /dev/null +++ b/docs/api/calendar.md @@ -0,0 +1,328 @@ +--- +description: "Calendar and date helpers." +--- + +# `calendar` + +Calendar and date helpers. + +> [!WARNING] +> +> This module is still under development and may not be stable. The API is +> incomplete and may change in future versions. + +## Usage + +```lua +cal = require "mods.calendar" + +print(cal.weekday(2026, 3, 26)) --> 4 +``` + +## Functions + +| Function | Description | +| ------------------------------------------------------ | --------------------------------- | +| [`getfirstweekday()`](#fn-getfirstweekday) | Return the default first weekday. | +| [`setfirstweekday(firstweekday)`](#fn-setfirstweekday) | Set the default first weekday. | + +**Calendar Calculations**: + +| Function | Description | +| ------------------------------------------- | ----------------------------------------------------------------------- | +| [`isleap(year)`](#fn-isleap) | Return `true` for leap years. | +| [`leapdays(y1, y2)`](#fn-leapdays) | Return the number of leap years from `y1` up to but not including `y2`. | +| [`monthrange(year, month)`](#fn-monthrange) | Return the first weekday and number of days for a month. | +| [`weekday(year, month, day)`](#fn-weekday) | Return weekday number where Monday is `1` and Sunday is `7`. | + +**Formatting**: + +| Function | Description | +| ----------------------------------------------------- | ------------------------------------------- | +| [`weekheader(width?, firstweekday?)`](#fn-weekheader) | Return the formatted weekday header string. | + +**Iterators**: + +| Function | Description | +| -------------------------------------------------------- | ---------------------------------------------------------------------- | +| [`monthdays(year, month, firstweekday?)`](#fn-monthdays) | Iterate `(year, month, day, weekday)` tuples for a full calendar grid. | +| [`weekdays(firstweekday?)`](#fn-weekdays) | Iterate weekday numbers for one full week. | + + + +### `getfirstweekday()` + +Return the default first weekday. + +**Return**: + +- `firstweekday` (`1|2|3|4|5|6|7`) + +**Example**: + +```lua +local cal = mods.calendar +print(cal.getfirstweekday()) --> 1 +``` + +> [!NOTE] +> +> This returns the same value as `cal.firstweekday`. + + + +### `setfirstweekday(firstweekday)` + +Set the default first weekday. + +**Parameters**: + +- `firstweekday` (`1|2|3|4|5|6|7`) + +**Example**: + +```lua +local cal = mods.calendar +cal.setfirstweekday(cal.SUNDAY) +``` + +> [!NOTE] +> +> This updates the same value as `cal.firstweekday = ...`. + +### Calendar Calculations + + + +#### `isleap(year)` + +Return `true` for leap years. + +**Parameters**: + +- `year` (`integer`) + +**Return**: + +- `isLeap` (`boolean`) + +**Example**: + +```lua +local cal = mods.calendar +print(cal.isleap(2024)) --> true +``` + + + +#### `leapdays(y1, y2)` + +Return the number of leap years from `y1` up to but not including `y2`. + +**Parameters**: + +- `y1` (`integer`) +- `y2` (`integer`) + +**Return**: + +- `count` (`integer`) + +**Example**: + +```lua +local cal = mods.calendar +print(cal.leapdays(2000, 2025)) --> 7 +``` + + + +#### `monthrange(year, month)` + +Return the first weekday and number of days for a month. + +**Parameters**: + +- `year` (`integer`) +- `month` (`integer`) + +**Return**: + +- `weekday` (`1|2|3|4|5|6|7`) +- `ndays` (`integer`) + +**Example**: + +```lua +local cal = mods.calendar +wday, ndays = cal.monthrange(2026, 2) +print(wday, ndays) --> 7 28 +``` + + + +#### `weekday(year, month, day)` + +Return weekday number where Monday is `1` and Sunday is `7`. + +**Parameters**: + +- `year` (`integer`) +- `month` (`1|2|3|4|5|6|7|8|9|10|11|12`) +- `day` + (`1|2|3|4|5|6|7|8|9|10|11|12|13|14|15|16|17|18|19|20|21|22|23|24|25|26|27|28|29|30|31`) + +**Return**: + +- `weekday` (`1|2|3|4|5|6|7`) + +**Example**: + +```lua +local cal = mods.calendar +print(cal.weekday(2026, 3, 26)) --> 4 +``` + +### Formatting + + + +#### `weekheader(width?, firstweekday?)` + +Return the formatted weekday header string. + +**Parameters**: + +- `width?` (`integer`) +- `firstweekday?` (`1|2|3|4|5|6|7`) + +**Return**: + +- `header` (`string`) + +**Example**: + +```lua +local cal = mods.calendar + +print(cal.weekheader(1, cal.SUNDAY)) --> "S M T W T F S" +print(cal.weekheader(2, cal.SUNDAY)) --> "Su Mo Tu We Th Fr Sa" +print(cal.weekheader(3, cal.SUNDAY)) --> "Sun Mon Tue Wed Thu Fri Sat" +``` + +### Iterators + + + +#### `monthdays(year, month, firstweekday?)` + +Iterate `(year, month, day, weekday)` tuples for a full calendar grid. + +**Parameters**: + +- `year` (`integer`) +- `month` (`1|2|3|4|5|6|7|8|9|10|11|12`) +- `firstweekday?` (`1|2|3|4|5|6|7`) + +**Return**: + +- `iter` + (`fun():year:integer,month:modsCalendarMonth,day:modsCalendarMonthday,weekday:modsCalendarWeekday`) + +**Example**: + +```lua +local List = mods.list +local cal = mods.calendar +local str = mods.str + +local header = cal.weekheader(2) +local lines = List({ + str.center(("%s %d"):format(cal.months[cal.FEBRUARY], 2026), #header), + header, +}) + +local cells = List() +for _, m, d, _ in cal.monthdays(2026, cal.FEBRUARY) do + cells:append(m == cal.FEBRUARY and ("%2d"):format(d) or " ") + if #cells == 7 then + lines:append(cells:join(" ")) + cells = List() + end +end + +print(lines:join("\n")) +-- February 2026 +-- Mo Tu We Th Fr Sa Su +-- 1 +-- 2 3 4 5 6 7 8 +-- 9 10 11 12 13 14 15 +-- 16 17 18 19 20 21 22 +-- 23 24 25 26 27 28 +``` + + + +#### `weekdays(firstweekday?)` + +Iterate weekday numbers for one full week. + +**Parameters**: + +- `firstweekday?` (`1|2|3|4|5|6|7`) + +**Return**: + +- `iter` (`fun():modsCalendarWeekday`) + +**Example**: + +```lua +local cal = mods.calendar +local weekdays = {} +for day in cal.weekdays() do + weekdays[#weekdays + 1] = day +end +print(table.concat(weekdays, ", ")) --> "1, 2, 3, 4, 5, 6, 7" +``` + +## Fields + + + +### `days` (`mods.List`) + +Weekday names indexed from `1` (Monday) to `7` (Sunday). + +```lua +print(cal.days[1]) --> Monday +print(cal.days[7]) --> Sunday +``` + + + +### `firstweekday` (`1|2|3|4|5|6|7`) + +The default first weekday field. + +```lua +print(cal.firstweekday) --> 1 +cal.firstweekday = cal.SUNDAY +print(cal.firstweekday) --> 7 +``` + +> [!NOTE] +> +> Reading or writing this property is equivalent to calling `getfirstweekday()` +> or `setfirstweekday()`. + + + +### `months` (`mods.List`) + +Month names indexed from `1` to `12`. + +```lua +print(cal.months[1]) --> January +print(cal.months[12]) --> December +``` diff --git a/docs/api/date.md b/docs/api/date.md new file mode 100644 index 0000000..c39f439 --- /dev/null +++ b/docs/api/date.md @@ -0,0 +1,1392 @@ +--- +description: "Timezone-naive date helpers and immutable date values." +--- + +# `date` + +Timezone-naive date helpers and immutable date values. + +## Usage + +```lua +local Date = require "mods.date" + +local a = Date("2026-03-30T14:45:06") +local b = Date("2026-03-30 14:45:06.123") +local c = Date("2026-03-31") +local d = Date("12-25-1995", "MM-DD-YYYY") +local e = Date({ year = 2026, month = 3, day = 30, hour = 14, min = 45 }) +local f = Date({ year = 2026 }) + +print(a) --> 2026-03-30 14:45:06 +print(b.ms) --> 123 +print(a:format("YYYY/MM/DD HH:mm:ss")) --> 2026/03/30 14:45:06 +print(a:is_before(c)) --> true +print(a < c) --> true +print(d) --> 1995-12-25 00:00:00 +print(e.sec) --> 0 +print(f) --> 2026-01-01 00:00:00 +``` + +> [!NOTE] +> +> - String inputs accept [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) +> forms, variants using a space instead of `T`, and custom formats via +> `Date(input, pattern)`: +> +> ```lua +> Date("2026-03-30T14:45:06") +> Date("2026-03-30 14:45:06") +> Date("12/25/1995", "MM/DD/YYYY") +> ``` +> +> - When `input` is a number, it is treated as Unix milliseconds. Use +> [`Date.unix(ts)`](#fn-unix) if you have a timestamp in seconds. +> +> ```lua +> local a = Date(1745155206123) -- Milliseconds +> local b = Date.unix(1745155206.123) -- Seconds +> print(a == b) --> true +> ``` +> +> - When calling `Date` without arguments, it uses +> [mstime](https://github.com/luamod/mstime) for millisecond precision if +> installed; otherwise it falls back to +> [`os.time`](https://www.lua.org/manual/5.1/manual.html#pdf-os.time). + +## Functions + +| Function | Description | +| --------------------------------- | ------------------------------------------------------------------------ | +| [`new(input, pattern?)`](#fn-new) | Create a Date from a string using an optional pattern. | +| [`new(input?)`](#fn-new) | Create a Date from a Unix timestamp (milliseconds) or a DateParts table. | + +**Arithmetic**: + +| Function | Description | +| ----------------------------------------- | ------------------------------------------------------------------- | +| [`add(amount, unit?)`](#fn-add) | Return a copy shifted by the given amount and unit. | +| [`diff(date, unit?)`](#fn-diff) | Return the signed difference to another Date in the requested unit. | +| [`subtract(amount, unit?)`](#fn-subtract) | Return a copy shifted backward by the given amount and unit. | + +**Boundaries**: + +| Function | Description | +| ------------------------------ | --------------------------------------------- | +| [`endof(unit)`](#fn-endof) | Return the end boundary for the given unit. | +| [`startof(unit)`](#fn-startof) | Return the start boundary for the given unit. | + +**Calendar**: + +| Function | Description | +| ----------------------------------------------------- | --------------------------------------------------------------------------- | +| [`day_of_year(day_of_year_number?)`](#fn-day-of-year) | Return or set the day of the year. | +| [`is_leap_year()`](#fn-is-leap-year) | Return `true` when the value's year is a leap year. | +| [`iso_week(iso_week_number?)`](#fn-iso-week) | Return or set the ISO week-of-year number. | +| [`iso_week_year()`](#fn-iso-week-year) | Return the ISO week-year for the current date. | +| [`iso_weekday(iso_weekday_number?)`](#fn-iso-weekday) | Return or set the ISO weekday number where Monday is `1` and Sunday is `7`. | +| [`iso_weeks_in_year()`](#fn-iso-weeks-in-year) | Return the number of ISO weeks in the current date's calendar year. | +| [`month_days()`](#fn-month-days) | Return the number of days in the value's month. | +| [`quarter(quarter_number?)`](#fn-quarter) | Return or set the quarter of the year. | +| [`week(week_number?)`](#fn-week) | Return or set the non-ISO week-of-year number. | +| [`week_year()`](#fn-week-year) | Return the non-ISO week-year for the current date. | +| [`weekday(weekday_number?)`](#fn-weekday) | Return or set the locale-relative weekday like Day.js `weekday()`. | +| [`weeks_in_year()`](#fn-weeks-in-year) | Return the number of weeks in the current locale week-year. | + +**Compare**: + +| Function | Description | +| --------------------------- | ----------------------------------------------------------- | +| [`max(...)`](#fn-max) | Return the latest value from the given dates. | +| [`min(...)`](#fn-min) | Return the earliest value from the given dates. | +| [`minmax(...)`](#fn-minmax) | Return the earliest and latest values from the given dates. | + +**Comparison**: + +| Function | Description | +| ---------------------------------------------------------------- | ----------------------------------------------------------------- | +| [`is_after(date)`](#fn-is-after) | Return `true` when the value is later than `other`. | +| [`is_before(date)`](#fn-is-before) | Return `true` when the value is earlier than `other`. | +| [`is_between(start_date, end_date, inclusive?)`](#fn-is-between) | Return `true` when the value lies between two bounds. | +| [`is_same(date)`](#fn-is-same) | Return `true` when the value is equal to `other`. | +| [`is_same_or_after(date)`](#fn-is-same-or-after) | Return `true` when the value is later than or equal to `other`. | +| [`is_same_or_before(date)`](#fn-is-same-or-before) | Return `true` when the value is earlier than or equal to `other`. | +| [`is_today()`](#fn-is-today) | Return `true` when the value falls on the current local day. | +| [`is_tomorrow()`](#fn-is-tomorrow) | Return `true` when the value falls on the next local day. | +| [`is_yesterday()`](#fn-is-yesterday) | Return `true` when the value falls on the previous local day. | + +**Duration**: + +| Function | Description | +| --------------------------------------- | --------------------------------------------------------------------------- | +| [`is_duration(value)`](#fn-is-duration) | Return `true` when the value is a duration created by `date.duration(...)`. | + +**Formatting**: + +| Function | Description | +| ------------------------------- | ------------------------------------------------------------------------------------------------------- | +| [`format(pattern)`](#fn-format) | Format the Date with tokens like `YYYY`, `MMM`, `dddd`, `Do`, `Q`, `hh`, `k`, `X`, `x`, `A`, and `SSS`. | +| [`tostring()`](#fn-tostring) | Return the default string form `YYYY-MM-DD HH:mm:ss`. | + +**Relative Time**: + +| Function | Description | +| ------------------------------------------- | -------------------------------------------------------------- | +| [`from(date, without_suffix?)`](#fn-from) | Return relative time from another Date to this one. | +| [`from_now(without_suffix?)`](#fn-from-now) | Return relative time from the current local time to this Date. | +| [`to(date, without_suffix?)`](#fn-to) | Return relative time from this Date to another one. | +| [`to_now(without_suffix?)`](#fn-to-now) | Return relative time from this Date to the current local time. | + +**Unix**: + +| Function | Description | +| ----------------------------- | ------------------------------------------------------------------- | +| [`unix(timestamp)`](#fn-unix) | Create a Date from a Unix timestamp in whole or fractional seconds. | + +**Validation**: + +| Function | Description | +| -------------------------------------------- | ----------------------------------------------------------- | +| [`is_valid(input?, pattern?)`](#fn-is-valid) | Return `true` when the input can be parsed as a valid Date. | + +**Metamethods**: + +| Function | Description | +| ------------------------------ | ----------------------------------------------------------------------- | +| [`__add(a, b)`](#fn-add) | Return a copy shifted by integer milliseconds. | +| [`__eq(date)`](#fn-eq) | Return `true` when both Date values have identical components. | +| [`__le(date)`](#fn-le) | Return `true` when the left Date is earlier than or equal to the right. | +| [`__lt(date)`](#fn-lt) | Return `true` when the left Date is earlier than the right. | +| [`__sub(a, b)`](#fn-sub) | Return either a shifted copy or a millisecond delta. | +| [`__tostring()`](#fn-tostring) | Return the same result as `tostring()` when coerced to a string. | + + + +### `new(input, pattern?)` + +Create a Date from a string using an optional pattern. + +**Parameters**: + +- `input` (`string`): The date string to parse. +- `pattern?` (`string`): The format pattern. + +**Return**: + +- **value** (`mods.Date`) + +**Example**: + +```lua +local d1 = Date("2026-03-30") +local d2 = Date("12-25-1995", "MM-DD-YYYY") +``` + + + +### `new(input?)` + +Create a Date from a Unix timestamp (milliseconds) or a DateParts table. + +**Parameters**: + +- `input?` (`number|mods.DateParts`): Unix timestamp (ms) or table of date + components. + +**Return**: + +- **value** (`mods.Date`) + +**Example**: + +```lua +local d1 = Date(1745155206123) +local d2 = Date({ year = 2026, month = 3 }) +``` + +### Arithmetic + + + +#### `add(amount, unit?)` + +Return a copy shifted by the given amount and unit. + +**Parameters**: + +- `amount` (`integer|mods.DateDurationParts`): Signed amount to add, or a + duration-style table. +- `unit?` + (`'ms'|'milliseconds'|'millisecond'|'s'|'secs'|'sec'|'seconds'|'second'|'m'|'mins'|'min'|'minutes'|'minute'|'h'|'hours'|'hour'|'d'|'days'|'day'|'w'|'weeks'|'week'|'M'|'months'|'month'|'q'|'quarters'|'quarter'|'y'|'years'|'year'`): + Unit for the addition. + +**Return**: + +- `shifted` (`mods.Date`): Shifted date value. + +**Example**: + +```lua +local d = Date("2026-03-30T14:45:06") + +print(d:add(2, "day")) --> 2026-04-01 14:45:06 +print(d:add(1, "quarter")) --> 2026-06-30 14:45:06 +print(d:add(1, "month")) --> 2026-04-30 14:45:06 +print(d:add(250, "ms")) --> 2026-03-30 14:45:06.250 +print(d:add({ month = 1, day = 2 })) --> 2026-05-02 14:45:06 +``` + + + +#### `diff(date, unit?)` + +Return the signed difference to another Date in the requested unit. + +**Parameters**: + +- `date` (`mods.Date`): Date to compare against. +- `unit?` + (`'ms'|'milliseconds'|'millisecond'|'s'|'secs'|'sec'|'seconds'|'second'|'m'|'mins'|'min'|'minutes'|'minute'|'h'|'hours'|'hour'|'d'|'days'|'day'|'w'|'weeks'|'week'|'M'|'months'|'month'|'q'|'quarters'|'quarter'|'y'|'years'|'year'`): + Unit used for the difference. Defaults to `"ms"`. + +**Return**: + +- `delta` (`integer`): Signed difference in whole units. + +**Example**: + +```lua +local a = Date("2026-03-30T12:00:00") +local b = Date("2026-02-28T12:00:00") +print(a:diff(b, "month")) --> 1 +print(a:diff(b, "day")) --> 30 +``` + + + +#### `subtract(amount, unit?)` + +Return a copy shifted backward by the given amount and unit. + +**Parameters**: + +- `amount` (`integer|mods.DateDurationParts`): Signed amount to subtract, or a + duration-style table. +- `unit?` + (`'ms'|'milliseconds'|'millisecond'|'s'|'secs'|'sec'|'seconds'|'second'|'m'|'mins'|'min'|'minutes'|'minute'|'h'|'hours'|'hour'|'d'|'days'|'day'|'w'|'weeks'|'week'|'M'|'months'|'month'|'q'|'quarters'|'quarter'|'y'|'years'|'year'`): + Unit for the subtraction. + +**Return**: + +- `shifted` (`mods.Date`): Shifted date value. + +**Example**: + +```lua +local d = Date("2026-03-30T14:45:06") +print(d:subtract(2, "day")) --> 2026-03-28 14:45:06 +print(d:subtract(1, "quarter")) --> 2025-12-30 14:45:06 +print(d:subtract(1, "month")) --> 2026-02-28 14:45:06 +print(d:subtract(250, "ms")) --> 2026-03-30 14:45:05.750 +print(d:subtract({ month = 1, day = 1 })) --> 2026-02-27 14:45:06 +``` + +### Boundaries + + + +#### `endof(unit)` + +Return the end boundary for the given unit. + +**Parameters**: + +- `unit` (`mods.DateUnit|"isoWeek"`): Boundary unit. + +**Return**: + +- `bounded` (`mods.Date`): Date clamped to the end of the unit. + +**Example**: + +```lua +local d = Date("2026-03-30T14:45:06") +print(d:endof("month")) --> 2026-03-31 23:59:59 +print(d:endof("week")) --> 2026-04-05 23:59:59 +print(d:endof("isoWeek")) --> 2026-04-05 23:59:59 +``` + +`"isoWeek"` is also supported here as a boundary-only unit. + + + +#### `startof(unit)` + +Return the start boundary for the given unit. + +**Parameters**: + +- `unit` (`mods.DateUnit|"isoWeek"`): Boundary unit. + +**Return**: + +- `bounded` (`mods.Date`): Date clamped to the start of the unit. + +**Example**: + +```lua +local d = Date("2026-03-30T14:45:06") +print(d:startof("day")) --> 2026-03-30 00:00:00 +print(d:startof("quarter")) --> 2026-01-01 00:00:00 +print(d:startof("isoWeek")) --> 2026-03-30 00:00:00 +``` + +`"isoWeek"` is also supported here as a boundary-only unit. + +### Calendar + + + +#### `day_of_year(day_of_year_number?)` + +Return or set the day of the year. + +**Parameters**: + +- `day_of_year_number?` (`integer`): Day-of-year to set. + +**Return**: + +- `dayOrDate` (`integer|mods.Date`): Current day-of-year number, or a shifted + Date when `day_of_year_number` is provided. + +**Example**: + +```lua +local d = Date("2026-03-30") +print(d:day_of_year()) --> 89 +print(d:day_of_year(1)) --> 2026-01-01 00:00:00 +``` + + + +#### `is_leap_year()` + +Return `true` when the value's year is a leap year. + +**Return**: + +- `isLeapYear` (`boolean`): Whether the year is a leap year. + +**Example**: + +```lua +print(Date("2024-02-29"):is_leap_year()) --> true +``` + + + +#### `iso_week(iso_week_number?)` + +Return or set the ISO week-of-year number. + +**Parameters**: + +- `iso_week_number?` (`integer`): ISO week number to set. + +**Return**: + +- `isoWeekOrDate` (`integer|mods.Date`): Current ISO week number, or a shifted + Date when `iso_week_number` is provided. + +**Example**: + +```lua +local d = Date("2026-03-30") +print(d:iso_week()) --> 14 +print(d:iso_week(15)) --> 2026-04-06 00:00:00 +``` + + + +#### `iso_week_year()` + +Return the ISO week-year for the current date. + +**Return**: + +- `isoWeekYear` (`integer`): ISO week-year. + +**Example**: + +```lua +print(Date("2021-01-01"):iso_week_year()) --> 2020 +``` + + + +#### `iso_weekday(iso_weekday_number?)` + +Return or set the ISO weekday number where Monday is `1` and Sunday is `7`. + +**Parameters**: + +- `iso_weekday_number?` (`integer`): ISO weekday to set. + +**Return**: + +- `isoWeekdayOrDate` (`modsCalendarWeekday|mods.Date`): Current ISO weekday + number, or a shifted Date when `iso_weekday_number` is provided. + +**Example**: + +```lua +local d = Date("2026-03-30") +print(d:iso_weekday()) --> 1 +print(d:iso_weekday(7)) --> 2026-04-05 00:00:00 +``` + + + +#### `iso_weeks_in_year()` + +Return the number of ISO weeks in the current date's calendar year. + +**Return**: + +- `isoWeeksInYear` (`integer`): Number of ISO weeks in the current date's + calendar year. + +**Example**: + +```lua +local d = Date("2016-01-01") +print(Date("2016-01-01"):iso_weeks_in_year()) --> 52 +print(Date("2016-06-01"):iso_weeks_in_year()) --> 52 +``` + + + +#### `month_days()` + +Return the number of days in the value's month. + +**Return**: + +- `ndays` (`modsCalendarMonthday`): Number of days in the current month. + +**Example**: + +```lua +print(Date("2024-02-01"):month_days()) --> 29 +``` + + + +#### `quarter(quarter_number?)` + +Return or set the quarter of the year. + +**Parameters**: + +- `quarter_number?` (`integer`): Quarter to set. + +**Return**: + +- `quarterOrDate` (`integer|mods.Date`): Current quarter number, or a shifted + Date when `quarter_number` is provided. + +**Example**: + +```lua +local d = Date("2026-03-30") +print(d:quarter()) --> 1 +print(d:quarter(2)) --> 2026-06-30 00:00:00 +``` + + + +#### `week(week_number?)` + +Return or set the non-ISO week-of-year number. + +**Parameters**: + +- `week_number?` (`integer`): Week number to set. + +**Return**: + +- `weekOrDate` (`integer|mods.Date`): Current week-of-year number, or a shifted + Date when `week_number` is provided. + +**Example**: + +```lua +local d = Date("2026-03-30") +print(d:week()) --> 14 +print(d:week(15)) --> 2026-04-06 00:00:00 +``` + + + +#### `week_year()` + +Return the non-ISO week-year for the current date. + +**Return**: + +- `weekYear` (`integer`): Week-year. + +**Example**: + +```lua +print(Date("2021-01-01"):week_year()) --> 2021 +``` + + + +#### `weekday(weekday_number?)` + +Return or set the locale-relative weekday like Day.js `weekday()`. + +**Parameters**: + +- `weekday_number?` (`integer`): Locale-relative weekday to set. + +**Return**: + +- `weekdayOrDate` (`integer|mods.Date`): Current locale-relative weekday number, + or a shifted Date when `weekday_number` is provided. + +**Example**: + +```lua +local d = Date("2026-03-30") +print(d:weekday()) --> 0 +print(d:weekday(7)) --> 2026-04-06 00:00:00 +print(d:weekday(-7)) --> 2026-03-23 00:00:00 +``` + +The getter returns a number in the range `0..6`, relative to the current +`mods.calendar.firstweekday`. Passing an integer returns a shifted copy in the +same locale-relative week space, with negative and overflow values moving into +previous or next weeks. + + + +#### `weeks_in_year()` + +Return the number of weeks in the current locale week-year. + +**Return**: + +- `weeksInYear` (`integer`): Number of weeks. + +**Example**: + +```lua +print(Date("2026-03-30"):weeks_in_year()) --> 52 +``` + +### Compare + + + +#### `max(...)` + +Return the latest value from the given dates. + +**Parameters**: + +- `...` (`mods.Date|mods.Date[]`): Date values to compare. Each argument may be + a date or a list of dates. + +**Return**: + +- `date` (`mods.Date`): Latest date value. + +**Example**: + +```lua +local a = Date("2026-03-30") +local b = Date("2026-03-31") +print(Date.max(a, b)) --> 2026-03-31 00:00:00 +print(Date.max({ a, b })) --> 2026-03-31 00:00:00 +``` + + + +#### `min(...)` + +Return the earliest value from the given dates. + +**Parameters**: + +- `...` (`mods.Date|mods.Date[]`): Date values to compare. Each argument may be + a date or a list of dates. + +**Return**: + +- `date` (`mods.Date`): Earliest date value. + +**Example**: + +```lua +local a = Date("2026-03-30") +local b = Date("2026-03-28") +print(Date.min(a, b)) --> 2026-03-28 00:00:00 +print(Date.min({ a, b })) --> 2026-03-28 00:00:00 +``` + + + +#### `minmax(...)` + +Return the earliest and latest values from the given dates. + +**Parameters**: + +- `...` (`mods.Date|mods.Date[]`): Date values to compare. Each argument may be + a date or a list of dates. + +**Return**: + +- `minDate` (`mods.Date`): Earliest date value. +- `maxDate` (`mods.Date`): Latest date value. + +**Example**: + +```lua +local a = Date("2026-03-30") +local b = Date("2026-03-28") +local c = Date("2026-03-31") +local min_date, max_date = Date.minmax(a, b, c) +print(min_date) --> 2026-03-28 00:00:00 +print(max_date) --> 2026-03-31 00:00:00 +``` + +### Comparison + + + +#### `is_after(date)` + +Return `true` when the value is later than `other`. + +**Parameters**: + +- `date` (`mods.Date`): Date to compare against. + +**Return**: + +- `isAfter` (`boolean`): Whether the value is later than `date`. + +**Example**: + +```lua +local a = Date("2026-03-31T12:00:00") +local b = Date("2026-03-30T12:00:00") +print(a:is_after(b)) --> true +``` + + + +#### `is_before(date)` + +Return `true` when the value is earlier than `other`. + +**Parameters**: + +- `date` (`mods.Date`): Date to compare against. + +**Return**: + +- `isBefore` (`boolean`): Whether the value is earlier than `date`. + +**Example**: + +```lua +local a = Date("2026-03-30T12:00:00") +local b = Date("2026-03-31T12:00:00") +print(a:is_before(b)) --> true +``` + + + +#### `is_between(start_date, end_date, inclusive?)` + +Return `true` when the value lies between two bounds. + +Bounds may be passed in either order. By default the comparison is exclusive; +pass `true` as the third argument to include the endpoints. + +**Parameters**: + +- `start_date` (`mods.Date`): One bound. +- `end_date` (`mods.Date`): The other bound. +- `inclusive?` (`boolean`): Whether to include the endpoints. Defaults to + `false`. + +**Return**: + +- `isBetween` (`boolean`): Whether the value lies between the two bounds. + +**Example**: + +```lua +local d = Date("2026-03-30T12:00:00") +local a = Date("2026-03-30T00:00:00") +local b = Date("2026-03-31T00:00:00") +print(d:is_between(a, b)) --> true +print(a:is_between(a, b)) --> false +print(a:is_between(a, b, true)) --> true +``` + + + +#### `is_same(date)` + +Return `true` when the value is equal to `other`. + +**Parameters**: + +- `date` (`mods.Date`): Date to compare against. + +**Return**: + +- `isSame` (`boolean`): Whether the value is equal to `date`. + +**Example**: + +```lua +local a = Date("2026-03-30T12:00:00") +local b = Date("2026-03-30T12:00:00") +print(a:is_same(b)) --> true +``` + + + +#### `is_same_or_after(date)` + +Return `true` when the value is later than or equal to `other`. + +**Parameters**: + +- `date` (`mods.Date`): Date to compare against. + +**Return**: + +- `isSameOrAfter` (`boolean`): Whether the value is later than or equal to + `date`. + +**Example**: + +```lua +local a = Date("2026-03-31T12:00:00") +local b = Date("2026-03-30T12:00:00") +local c = Date("2026-03-31T12:00:00") +print(a:is_same_or_after(b)) --> true +print(a:is_same_or_after(c)) --> true +``` + + + +#### `is_same_or_before(date)` + +Return `true` when the value is earlier than or equal to `other`. + +**Parameters**: + +- `date` (`mods.Date`): Date to compare against. + +**Return**: + +- `isSameOrBefore` (`boolean`): Whether the value is earlier than or equal to + `date`. + +**Example**: + +```lua +local a = Date("2026-03-30T12:00:00") +local b = Date("2026-03-30T12:00:00") +local c = Date("2026-03-31T12:00:00") +print(a:is_same_or_before(b)) --> true +print(a:is_same_or_before(c)) --> true +``` + + + +#### `is_today()` + +Return `true` when the value falls on the current local day. + +**Return**: + +- `isToday` (`boolean`): Whether the value is on today in local time. + +**Example**: + +```lua +print(Date():is_today()) --> true +``` + + + +#### `is_tomorrow()` + +Return `true` when the value falls on the next local day. + +**Return**: + +- `isTomorrow` (`boolean`): Whether the value is on tomorrow in local time. + +**Example**: + +```lua +print(Date():add(1, "day"):is_tomorrow()) --> true +``` + + + +#### `is_yesterday()` + +Return `true` when the value falls on the previous local day. + +**Return**: + +- `isYesterday` (`boolean`): Whether the value is on yesterday in local time. + +**Example**: + +```lua +print(Date():subtract(1, "day"):is_yesterday()) --> true +``` + +### Duration + + + +#### `is_duration(value)` + +Return `true` when the value is a duration created by `date.duration(...)`. + +**Parameters**: + +- `value` (`any`): Value to test. + +**Return**: + +- `isDuration` (`boolean`): Whether the value is a `mods.Duration`. + +**Example**: + +```lua +local shift = date.duration({ day = 2 }) +print(date.is_duration(shift)) --> true +print(date.is_duration({ day = 2 })) --> false +``` + +### Formatting + + + +#### `format(pattern)` + +Format the Date with tokens like `YYYY`, `MMM`, `dddd`, `Do`, `Q`, `hh`, `k`, +`X`, `x`, `A`, and `SSS`. + +**Parameters**: + +- `pattern` (`string`): Format pattern using supported tokens. + +**Return**: + +- `formatted` (`string`): Formatted datetime string. + +**Example**: + +```lua +local d = Date("2026-03-30T14:45:06.123") +local ts = Date(1523520536123) +print(d:format("YYYY/MM/DD HH:mm:ss.SSS")) --> 2026/03/30 14:45:06.123 +print(d:format("ddd, MMM Do YYYY h:mm A")) --> Mon, Mar 30th 2026 2:45 PM +print(d:format("LLLL")) --> Monday, March 30, 2026 2:45 PM +print(d:format("GGGG-[W]WW")) --> 2026-W14 +print(ts:format("X x")) --> 1523520536 1523520536123 +``` + +> [!NOTE] +> +> Wrap literal text in `[...]` to escape it, for example: +> +> ```lua +> d:format("GGGG-[W]WW") -- 2026-W14 +> d:format("[hours:]HH") -- hours:14 +> ``` + +**Supported tokens**: + +| Token | Example | Meaning | +| ------ | ------------------ | ---------------------------------- | +| `YY` | `26` | 2-digit year | +| `YYYY` | `2026` | 4-digit year | +| `Q` | `1-4` | Quarter | +| `Qo` | `1st..4th` | Ordinal quarter | +| `M` | `1-12` | Month | +| `MM` | `03-12` | Month, zero-padded | +| `MMM` | `Jan-Dec` | Short month name | +| `MMMM` | `January-December` | Full month name | +| `D` | `1-31` | Day of month | +| `DD` | `01-31` | Day of month, zero-padded | +| `DDD` | `1-366` | Day of year | +| `DDDD` | `001-366` | Day of year, zero-padded | +| `d` | `0-6` | Weekday number where Sunday is `0` | +| `e` | `0-6` | Weekday number where Sunday is `0` | +| `E` | `1-7` | ISO weekday number | +| `dd` | `Su-Sa` | Minimal weekday name | +| `ddd` | `Sun-Sat` | Short weekday name | +| `dddd` | `Sunday-Saturday` | Full weekday name | +| `Do` | `1st..31th` | Ordinal day of month | +| `H` | `0-23` | 24-hour | +| `HH` | `00-23` | 24-hour, zero-padded | +| `h` | `1-12` | 12-hour | +| `hh` | `01-12` | 12-hour, zero-padded | +| `k` | `1-24` | 1-24 hour | +| `kk` | `01-24` | 1-24 hour, zero-padded | +| `m` | `0-59` | Minute | +| `mm` | `00-59` | Minute, zero-padded | +| `s` | `0-59` | Second | +| `ss` | `00-59` | Second, zero-padded | +| `S` | `0-9` | Hundreds digit of milliseconds | +| `SS` | `00-99` | First two digits of milliseconds | +| `SSS` | `000-999` | Millisecond, zero-padded | +| `w` | `1-53` | Week of year | +| `ww` | `01-53` | Week of year, zero-padded | +| `wo` | `1st..53rd` | Ordinal week of year | +| `W` | `1-53` | ISO week of year | +| `WW` | `01-53` | ISO week of year, zero-padded | +| `GG` | `26` | 2-digit ISO week-year | +| `GGGG` | `2026` | ISO week-year | +| `gggg` | `2026` | Week-year | +| `a` | `am pm` | Meridiem lowercase | +| `A` | `AM PM` | Meridiem uppercase | +| `x` | `1523520536123` | Unix timestamp in milliseconds | +| `X` | `1523520536` | Unix timestamp in seconds | + +English preset aliases: + +| Alias | Expands to | +| ------ | --------------------------- | +| `LT` | `h:mm A` | +| `LTS` | `h:mm:ss A` | +| `L` | `MM/DD/YYYY` | +| `LL` | `MMMM D, YYYY` | +| `LLL` | `MMMM D, YYYY h:mm A` | +| `LLLL` | `dddd, MMMM D, YYYY h:mm A` | +| `l` | `M/D/YYYY` | +| `ll` | `MMM D, YYYY` | +| `lll` | `MMM D, YYYY h:mm A` | +| `llll` | `ddd, MMM D, YYYY h:mm A` | + + + +#### `tostring()` + +Return the default string form `YYYY-MM-DD HH:mm:ss`. + +**Return**: + +- `s` (`string`): Default datetime string. + +**Example**: + +```lua +print(Date("2026-03-30T14:45:06.123")) --> 2026-03-30 14:45:06.123 +``` + +### Relative Time + + + +#### `from(date, without_suffix?)` + +Return relative time from another Date to this one. + +By default the result includes a suffix like `ago` or a prefix like `in`. Pass +`true` to omit that suffix or prefix. + +**Parameters**: + +- `date` (`mods.Date`): Reference date. +- `without_suffix?` (`boolean`): Whether to omit `ago` / `in`. + +**Return**: + +- `relative` (`string`): Relative time string. + +**Example**: + +```lua +local a = date("2026-03-30T14:45:06") +local b = date("2026-03-30T12:45:06") +print(a:from(b)) --> in 2 hours +print(a:from(b, true)) --> 2 hours +``` + + + +#### `from_now(without_suffix?)` + +Return relative time from the current local time to this Date. + +**Parameters**: + +- `without_suffix?` (`boolean`): Whether to omit `ago` / `in`. + +**Return**: + +- `relative` (`string`): Relative time string. + +**Example**: + +```lua +local d = date():add(1, "day") +print(d:from_now()) --> in a day +``` + + + +#### `to(date, without_suffix?)` + +Return relative time from this Date to another one. + +By default the result includes a suffix like `ago` or a prefix like `in`. Pass +`true` to omit that suffix or prefix. + +**Parameters**: + +- `date` (`mods.Date`): Reference date. +- `without_suffix?` (`boolean`): Whether to omit `ago` / `in`. + +**Return**: + +- `relative` (`string`): Relative time string. + +**Example**: + +```lua +local a = date("2026-03-30T12:45:06") +local b = date("2026-03-30T14:45:06") +print(a:to(b)) --> in 2 hours +print(a:to(b, true)) --> 2 hours +``` + + + +#### `to_now(without_suffix?)` + +Return relative time from this Date to the current local time. + +**Parameters**: + +- `without_suffix?` (`boolean`): Whether to omit `ago` / `in`. + +**Return**: + +- `relative` (`string`): Relative time string. + +**Example**: + +```lua +local d = date():subtract(1, "day") +print(d:to_now()) --> in a day +``` + +### Unix + + + +#### `unix(timestamp)` + +Create a Date from a Unix timestamp in whole or fractional seconds. + +**Parameters**: + +- `timestamp` (`number`): Unix timestamp in whole or fractional seconds. + +**Return**: + +- `date` (`mods.Date`): Date value for the given Unix timestamp. + +**Example**: + +```lua +print(Date.unix(1318781876)) --> 2011-10-16 18:17:56 +print(Date.unix(1318781876.721).year) --> 2011 +``` + +### Validation + + + +#### `is_valid(input?, pattern?)` + +Return `true` when the input can be parsed as a valid Date. + +Unlike `Date(...)`, this helper never raises for invalid input; it just returns +`false`. + +**Parameters**: + +- `input?` (`string|number|mods.DateParts`): Value accepted by `Date(...)`. + `nil` returns `false`. +- `pattern?` (`string`): Custom parse pattern used for string input. + +**Return**: + +- `isValid` (`boolean`): Whether the input is parseable as a valid Date. + +**Example**: + +```lua +print(Date.is_valid()) --> false +print(Date.is_valid("2026-03-30")) --> true +print(Date.is_valid("2026-02-29")) --> false +print(Date.is_valid("12-25-1995", "MM-DD-YYYY")) --> true +``` + +### Metamethods + + + +#### `__add(a, b)` + +Return a copy shifted by integer milliseconds. + +This works as either `date + ms` or `ms + date`. + +**Parameters**: + +- `a` (`integer|mods.Date`): Milliseconds to add, or another Date. +- `b` (`integer|mods.Date`): Milliseconds to add, or another Date. + +**Return**: + +- `sum` (`mods.Date`): Sum of the two dates. + +**Example**: + +```lua +local d = Date("2026-03-30T14:45:06") +print((d + 250)) --> 2026-03-30 14:45:06.250 +print((250 + d)) --> 2026-03-30 14:45:06.250 +``` + + + +#### `__eq(date)` + +Return `true` when both Date values have identical components. + +**Parameters**: + +- `date` (`mods.Date`): Date to compare against. + +**Return**: + +- `isEqual` (`boolean`): `true` if both dates are equal, `false` otherwise. + +**Example**: + +```lua +print(Date("2026-03-30") == Date("2026-03-30")) --> true +``` + + + +#### `__le(date)` + +Return `true` when the left Date is earlier than or equal to the right. + +**Parameters**: + +- `date` (`mods.Date`): Date to compare against. + +**Return**: + +- `isEarlierOrEqual` (`boolean`): `true` if the left date is earlier or equal, + `false` otherwise. + +**Example**: + +```lua +print(Date("2026-03-30") <= Date("2026-03-30")) --> true +``` + + + +#### `__lt(date)` + +Return `true` when the left Date is earlier than the right. + +**Parameters**: + +- `date` (`mods.Date`): Date to compare against. + +**Return**: + +- `isEarlier` (`boolean`): `true` if the left date is earlier, `false` + otherwise. + +**Example**: + +```lua +print(Date("2026-03-30") < Date("2026-03-31")) --> true +``` + + + +#### `__sub(a, b)` + +Return either a shifted copy or a millisecond delta. + +When subtracting an integer, it shifts by that many milliseconds. When +subtracting another Date, it returns the signed millisecond difference. + +**Parameters**: + +- `a` (`integer|mods.Date`): Milliseconds to subtract, or another Date. +- `b` (`integer|mods.Date`): Milliseconds to subtract, or another Date. + +**Return**: + +- `delta` (`mods.Date|integer`): Difference between dates. + +**Example**: + +```lua +local a = Date("2026-03-30T14:45:06.250") +local b = Date("2026-03-30T14:45:06") +print((a - 250)) --> 2026-03-30 14:45:06 +print(a - b) --> 250 +``` + + + +#### `__tostring()` + +Return the same result as `tostring()` when coerced to a string. + +**Return**: + +- `string` (`string`): representation of the date. + +**Example**: + +```lua +print(Date("2026-03-30T14:45:06")) --> 2026-03-30 14:45:06 +``` + +## Fields + +| Field | Description | +| ----------------- | ------------------------------------------------------------ | +| [`day`](#day) | Day-of-month component. | +| [`hour`](#hour) | Hour component. | +| [`min`](#min) | Minute component. | +| [`month`](#month) | Month component. | +| [`ms`](#ms) | Millisecond component. | +| [`sec`](#sec) | Second component. | +| [`wday`](#wday) | ISO weekday component where Monday is `1` and Sunday is `7`. | +| [`yday`](#yday) | Day-of-year component starting at `1`. | +| [`year`](#year) | Year component. | + + + +### `day` (`modsCalendarMonthday`) + +Day-of-month component. + +```lua +print(Date("2026-03-30").day) --> 30 +``` + + + +### `hour` (`integer`) + +Hour component. + +```lua +print(Date("2026-03-30T14:45:06").hour) --> 14 +``` + + + +### `min` (`integer`) + +Minute component. + +```lua +print(Date("2026-03-30T14:45:06").min) --> 45 +``` + + + +### `month` (`modsCalendarMonth`) + +Month component. + +```lua +print(Date("2026-03-30").month) --> 3 +``` + + + +### `ms` (`integer`) + +Millisecond component. + +```lua +print(Date("2026-03-30T14:45:06.123").ms) --> 123 +``` + + + +### `sec` (`integer`) + +Second component. + +```lua +print(Date("2026-03-30T14:45:06").sec) --> 6 +``` + + + +### `wday` (`modsCalendarWeekday`) + +ISO weekday component where Monday is `1` and Sunday is `7`. + +```lua +print(Date("2026-03-30").wday) --> 1 +``` + + + +### `yday` (`integer`) + +Day-of-year component starting at `1`. + +```lua +print(Date("2026-03-30").yday) --> 89 +``` + + + +### `year` (`integer`) + +Year component. + +```lua +print(Date("2026-03-30").year) --> 2026 +``` diff --git a/docs/api/duration.md b/docs/api/duration.md new file mode 100644 index 0000000..3386860 --- /dev/null +++ b/docs/api/duration.md @@ -0,0 +1,446 @@ +--- +description: + "Reusable immutable duration values for date arithmetic and formatting." +--- + +# `duration` + +Reusable immutable duration values for date arithmetic and formatting. + +## Usage + +```lua +local Duration = require "mods.duration" + +local shift = Duration({ day = 2, hour = 3 }) +print(shift:format("D [days] HH:mm")) --> 2 days 03:00 +``` + +## Functions + +| Function | Description | +| --------------------------------------- | -------------------------------------------------------------------------------------------------------- | +| [`is_duration(value)`](#fn-is-duration) | Return `true` when the value is a duration created by `mods.duration(...)` or `mods.date.duration(...)`. | +| [`new(input, unit?)`](#fn-new) | Create a duration from a numeric amount and unit. | +| [`new(input?)`](#fn-new) | Create a duration from numeric parts, an ISO 8601 string, or another duration. | + +**Duration**: + +| Function | Description | +| ------------------------------------------------------------- | ------------------------------------------------------------------------- | +| [`add(value, unit?)`](#fn-add) | Return a new duration with another duration or unit amount added. | +| [`as(unit)`](#fn-as) | Return the duration expressed in the requested unit. | +| [`clone()`](#fn-clone) | Return a shallow copy of the duration value. | +| [`compare(other)`](#fn-compare) | Compare this duration to another duration-like value. | +| [`equals(other)`](#fn-equals) | Return `true` when both duration values have identical components. | +| [`format(pattern)`](#fn-format) | Format the duration using duration tokens like `Y`, `MM`, `DD`, and `HH`. | +| [`humanize(with_suffix_or_options?, options?)`](#fn-humanize) | Return a human-readable relative-style phrase for the duration. | +| [`normalize()`](#fn-normalize) | Return a compacted duration using the module's canonical carry rules. | +| [`subtract(value, unit?)`](#fn-subtract) | Return a new duration with another duration or unit amount subtracted. | +| [`to_iso()`](#fn-to-iso) | Return an ISO 8601 duration string. | +| [`tostring()`](#fn-tostring) | Return a debug-friendly string representation of the duration. | + +**Metamethods**: + +| Function | Description | +| ---------------------------------- | ------------------------------------------------------------------------------ | +| [`__call(input, unit?)`](#fn-call) | Create a duration from a numeric amount and unit. | +| [`__call(input?)`](#fn-call) | Create a duration from numeric parts, an ISO 8601 string, or another duration. | +| [`__eq(duration)`](#fn-eq) | Return `true` when both duration values have identical components. | +| [`__tostring()`](#fn-tostring) | Return the same result as `tostring()` when coerced to a string. | + + + +### `is_duration(value)` + +Return `true` when the value is a duration created by `mods.duration(...)` or +`mods.date.duration(...)`. + +**Parameters**: + +- `value` (`any`) + +**Return**: + +- `isDuration` (`boolean`) + +**Example**: + +```lua +local Duration = require "mods.duration" +print(Duration.is_duration(Duration({ day = 2 }))) --> true +print(Duration.is_duration({ day = 2 })) --> false +``` + + + +### `new(input, unit?)` + +Create a duration from a numeric amount and unit. + +**Parameters**: + +- `input` (`number`): Numeric amount to convert into a duration. +- `unit?` (`mods.DateUnit`): Unit used with the numeric amount. Defaults to + `"ms"`. + +**Return**: + +- `duration` (`mods.Duration`) + +**Example**: + +```lua +local d = Duration(90, "minute") +``` + + + +### `new(input?)` + +Create a duration from numeric parts, an ISO 8601 string, or another duration. + +**Parameters**: + +- `input?` (`string|mods.DurationParts|mods.Duration`): Duration parts, an ISO + 8601 string, or another duration. + +**Return**: + +- `duration` (`mods.Duration`) + +**Example**: + +```lua +local a = Duration({ day = 2, hour = 3 }) +local b = Duration("PT1H30M") +``` + +### Duration + + + +#### `add(value, unit?)` + +Return a new duration with another duration or unit amount added. + +**Parameters**: + +- `value` (`number|mods.DurationParts|mods.Duration`): Signed amount to add, or + another duration value. +- `unit?` (`mods.DateUnit`): Unit used when `value` is a number. + +**Return**: + +- `duration` (`mods.Duration`) + +**Example**: + +```lua +local Duration = require "mods.duration" +local a = Duration({ day = 2 }) +local b = a:add(3, "hour") +print(b:format("D [days] HH:mm:ss")) --> 2 days 03:00:00 +``` + + + +#### `as(unit)` + +Return the duration expressed in the requested unit. + +**Parameters**: + +- `unit` (`mods.DateUnit`) + +**Return**: + +- `amount` (`number`) + +**Example**: + +```lua +local Duration = require "mods.duration" +local d = Duration({ day = 1, hour = 12 }) +print(d:as("hour")) --> 36 +``` + + + +#### `clone()` + +Return a shallow copy of the duration value. + +**Return**: + +- `duration` (`mods.Duration`) + +**Example**: + +```lua +local Duration = require "mods.duration" +local d = Duration({ month = 1, day = 2 }) +local copy = d:clone() +print(copy == d, rawequal(copy, d)) --> true false +``` + + + +#### `compare(other)` + +Compare this duration to another duration-like value. + +Returns `-1` when smaller, `0` when equal, and `1` when larger. + +**Parameters**: + +- `other` (`number|string|mods.DurationParts|mods.Duration`) + +**Return**: + +- `ordering` (`integer`) + +**Example**: + +```lua +local Duration = require "mods.duration" +print(Duration({ day = 1 }):compare({ hour = 24 })) --> 0 +``` + + + +#### `equals(other)` + +Return `true` when both duration values have identical components. + +**Parameters**: + +- `other` (`any`): Value to compare against. + +**Return**: + +- `isEqual` (`boolean`) + +**Example**: + +```lua +local Duration = require "mods.duration" +local a = Duration({ day = 2 }) +local b = Duration({ day = 2 }) +print(a:equals(b)) --> true +``` + + + +#### `format(pattern)` + +Format the duration using duration tokens like `Y`, `MM`, `DD`, and `HH`. + +**Parameters**: + +- `pattern` (`string`): Format pattern using supported duration tokens. + +**Return**: + +- `formatted` (`string`) + +**Example**: + +```lua +local Duration = require "mods.duration" +local d = Duration({ day = 2, hour = 3, minute = 4 }) +print(d:format("D [days] HH:mm")) --> 2 days 03:04 +``` + + + +#### `humanize(with_suffix_or_options?, options?)` + +Return a human-readable relative-style phrase for the duration. + +By default this returns the bare phrase without `ago` / `in`. Pass `true` to +include relative wording. You can also pass an options table for abbreviated +output or explicit unit clamping. + +**Parameters**: + +- `with_suffix_or_options?` (`boolean|mods.DurationHumanizeOptions`): Whether to + include `ago` / `in` style wording, or an options table. +- `options?` (`mods.DurationHumanizeOptions`): Additional options when the first + argument is a boolean. + +**Return**: + +- `humanized` (`string`) + +**Example**: + +```lua +local Duration = require "mods.duration" +local d = Duration({ day = 3 }) +print(d:humanize()) --> 3 days +print(d:humanize(true)) --> in 3 days +print(d:humanize({ short = true })) --> 3d +``` + + + +#### `normalize()` + +Return a compacted duration using the module's canonical carry rules. + +**Return**: + +- `duration` (`mods.Duration`) + +**Example**: + +```lua +local Duration = require "mods.duration" +print(Duration({ minute = 90 }):normalize()) --> duration(hours=1, minutes=30) +``` + + + +#### `subtract(value, unit?)` + +Return a new duration with another duration or unit amount subtracted. + +**Parameters**: + +- `value` (`number|mods.DurationParts|mods.Duration`): Signed amount to + subtract, or another duration value. +- `unit?` (`mods.DateUnit`): Unit used when `value` is a number. + +**Return**: + +- `duration` (`mods.Duration`) + +**Example**: + +```lua +local Duration = require "mods.duration" +local a = Duration({ day = 2, hour = 3 }) +local b = a:subtract(3, "hour") +print(b:format("D [days] HH:mm:ss")) --> 2 days 00:00:00 +``` + + + +#### `to_iso()` + +Return an ISO 8601 duration string. + +**Return**: + +- `iso` (`string`) + +**Example**: + +```lua +local Duration = require "mods.duration" +print(Duration({ hour = 1, minute = 30 }):to_iso()) --> PT1H30M +``` + + + +#### `tostring()` + +Return a debug-friendly string representation of the duration. + +**Return**: + +- `s` (`string`) + +**Example**: + +```lua +local Duration = require "mods.duration" +print(Duration({ day = 2, hour = 3 })) --> duration(days=2, hours=3) +``` + +### Metamethods + + + +#### `__call(input, unit?)` + +Create a duration from a numeric amount and unit. + +**Parameters**: + +- `input` (`number`): Numeric amount to convert into a duration. +- `unit?` (`mods.DateUnit`): Unit used with the numeric amount. Defaults to + `"ms"`. + +**Return**: + +- `duration` (`mods.Duration`) + +**Example**: + +```lua +local Duration = require "mods.duration" +local d = Duration(90, "minute") +``` + + + +#### `__call(input?)` + +Create a duration from numeric parts, an ISO 8601 string, or another duration. + +**Parameters**: + +- `input?` (`string|mods.DurationParts|mods.Duration`): Duration parts, an ISO + 8601 string, or another duration. + +**Return**: + +- `duration` (`mods.Duration`) + +**Example**: + +```lua +local Duration = require "mods.duration" +local a = Duration({ day = 2, hour = 3 }) +local b = Duration("PT1H30M") +``` + + + +#### `__eq(duration)` + +Return `true` when both duration values have identical components. + +**Parameters**: + +- `duration` (`mods.Duration`): Duration to compare against. + +**Return**: + +- `isEqual` (`boolean`) + +**Example**: + +```lua +local Duration = require "mods.duration" +print(Duration({ day = 2 }) == Duration({ day = 2 })) --> true +``` + + + +#### `__tostring()` + +Return the same result as `tostring()` when coerced to a string. + +**Return**: + +- `s` (`string`) + +**Example**: + +```lua +local Duration = require "mods.duration" +print(Duration({ day = 2 })) --> duration(days=2) +``` diff --git a/docs/api/fs.md b/docs/api/fs.md new file mode 100644 index 0000000..a646a9b --- /dev/null +++ b/docs/api/fs.md @@ -0,0 +1,653 @@ +--- +description: "Filesystem I/O, metadata, and filesystem path operations." +--- + +# `fs` + +Filesystem I/O, metadata, and filesystem path operations. + +> [!NOTE] +> +> This module requires +> [LuaFileSystem (`lfs`)](https://github.com/lunarmodules/luafilesystem). + +## Usage + +```lua +fs = require "mods.fs" + +fs.mkdir("tmp/cache/app", true) +fs.write_text("tmp/cache/app/data.txt", "hello") +print(fs.read_text("tmp/cache/app/data.txt")) --> "hello" +``` + +## Functions + +**Existence Checks**: + +| Function | Description | +| ------------------------------ | ------------------------------------------------------------ | +| [`exists(path)`](#fn-exists) | Return `true` when a path exists. | +| [`lexists(path)`](#fn-lexists) | Return `true` when a path exists without following symlinks. | + +**Filesystem Mutations**: + +| Function | Description | +| -------------------------------------------- | ----------------------------------------------------------------------------- | +| [`cd(path)`](#fn-cd) | Change the current working directory. | +| [`cp(src, dst)`](#fn-cp) | Copy a file or directory tree. | +| [`cwd()`](#fn-cwd) | Return the current working directory. | +| [`link(path, linkpath)`](#fn-link) | Create a hard link. | +| [`mkdir(path, parents?)`](#fn-mkdir) | Create a directory. | +| [`rename(oldname, newname)`](#fn-rename) | Rename or move a filesystem entry. | +| [`rm(path, recursive?)`](#fn-rm) | Remove a filesystem entry, or a directory tree when `recursive` is `true`. | +| [`symlink(path, linkpath)`](#fn-symlink) | Create a symbolic link. | +| [`touch(path)`](#fn-touch) | Create file if missing without truncating, or update timestamps if it exists. | +| [`write_bytes(path, data)`](#fn-write-bytes) | Write full file in binary mode. | +| [`write_text(path, data)`](#fn-write-text) | Write full file in text mode. | + +**Metadata**: + +| Function | Description | +| -------------------------------- | ---------------------------------------------------------------------------------- | +| [`getatime(path)`](#fn-getatime) | Return last access time. | +| [`getctime(path)`](#fn-getctime) | Return metadata change time. | +| [`getmtime(path)`](#fn-getmtime) | Return last modification time. | +| [`getsize(path)`](#fn-getsize) | Return file size in bytes. | +| [`lstat(path)`](#fn-lstat) | Return symlink-aware file attributes. | +| [`samefile(a, b)`](#fn-samefile) | Return whether two paths refer to the same file, or `nil` and an error on failure. | +| [`stat(path)`](#fn-stat) | Return file attributes. | + +**Reading**: + +| Function | Description | +| ------------------------------------- | -------------------------------------- | +| [`dir(path, opts?)`](#fn-dir) | Iterator over items in `path`. | +| [`listdir(path, opts?)`](#fn-listdir) | Return direct children of a directory. | +| [`read_bytes(path)`](#fn-read-bytes) | Read full file in binary mode. | +| [`read_text(path)`](#fn-read-text) | Read full file in text mode. | + +### Existence Checks + + + +#### `exists(path)` + +Return `true` when a path exists. + +**Parameters**: + +- `path` (`string`): Input path. + +**Return**: + +- `exists` (`boolean`): True when the path exists. + +**Example**: + +```lua +fs.exists("README.md") --> true +``` + +> [!NOTE] +> +> Broken symlinks return `false`. + + + +#### `lexists(path)` + +Return `true` when a path exists without following symlinks. + +**Parameters**: + +- `path` (`string`): Input path. + +**Return**: + +- `exists` (`boolean`): True when the path or symlink entry exists. + +**Example**: + +```lua +fs.lexists("README.md") --> true +``` + +> [!NOTE] +> +> Broken symlinks return `true`. + +### Filesystem Mutations + + + +#### `cd(path)` + +Change the current working directory. + +**Parameters**: + +- `path` (`string`): Directory path to switch into. + +**Return**: + +- `changed` (`true?`): `true` when the directory change succeeds, or `nil` on + failure. +- `errmsg` (`string?`): Error message when the change fails. + +**Example**: + +```lua +fs.cd("src") +``` + + + +#### `cp(src, dst)` + +Copy a file or directory tree. + +**Parameters**: + +- `src` (`string`): Source path. +- `dst` (`string`): Destination path. + +**Return**: + +- `copied` (`true?`): `true` when copying succeeds, or `nil` on failure. +- `errmsg` (`string?`): Error message when the check fails. +- `errcode` (`integer?`): OS error code when available. + +**Example**: + +```lua +fs.cp("a.txt", "b.txt") +fs.cp("src", "backup/src") +``` + + + +#### `cwd()` + +Return the current working directory. + +**Return**: + +- `cwd` (`string?`): Current working directory, or `nil` on failure. +- `errmsg` (`string?`): Error message when the lookup fails. +- `errcode` (`integer?`): OS error code when available. + +**Example**: + +```lua +fs.cwd() +``` + + + +#### `link(path, linkpath)` + +Create a hard link. + +**Parameters**: + +- `path` (`string`): Existing path to link to. +- `linkpath` (`string`): New link path to create. + +**Return**: + +- `linked` (`true?`): `true` when link creation succeeds, or `nil` on failure. +- `errmsg` (`string?`): Error message when the check fails. +- `errcode` (`integer?`): OS error code when available. + +**Example**: + +```lua +fs.link("target.txt", "hardlink.txt") +``` + + + +#### `mkdir(path, parents?)` + +Create a directory. + +**Parameters**: + +- `path` (`string`): Input path. +- `parents?` (`boolean`): Create missing parent directories when `true`. + +**Return**: + +- `created` (`true?`): `true` when directory creation succeeds, or `nil` on + failure. +- `errmsg` (`string?`): Error message when the check fails. +- `errcode` (`integer?`): OS error code when available. + +**Example**: + +```lua +fs.mkdir("tmp/a/b", true) +``` + + + +#### `rename(oldname, newname)` + +Rename or move a filesystem entry. + +**Parameters**: + +- `oldname` (`string`): Existing path. +- `newname` (`string`): Replacement path. + +**Return**: + +- `renamed` (`true?`): `true` when the rename succeeds, or `nil` on failure. +- `errmsg` (`string?`): Error message when the check fails. +- `errcode` (`integer?`): OS error code when available. + +**Example**: + +```lua +fs.rename("old.txt", "new.txt") +``` + +> [!NOTE] +> +> This is an alias for `os.rename`. + + + +#### `rm(path, recursive?)` + +Remove a filesystem entry, or a directory tree when `recursive` is `true`. + +**Parameters**: + +- `path` (`string`): Input path. +- `recursive?` (`boolean`): Remove a directory tree recursively when `true`. + +**Return**: + +- `removed` (`true?`): `true` when removal succeeds, or `nil` on failure. +- `errmsg` (`string?`): Error message when the check fails. +- `errcode` (`integer?`): OS error code when available. + +**Example**: + +```lua +fs.rm("tmp.txt") --> true, nil +fs.rm("tmp/cache", true) --> true, nil +``` + + + +#### `symlink(path, linkpath)` + +Create a symbolic link. + +**Parameters**: + +- `path` (`string`): Path to reference from the new symlink. +- `linkpath` (`string`): New symlink path to create. + +**Return**: + +- `linked` (`true?`): `true` when link creation succeeds, or `nil` on failure. +- `errmsg` (`string?`): Error message when the check fails. +- `errcode` (`integer?`): OS error code when available. + +**Example**: + +```lua +fs.symlink("target.txt", "symlink.txt") +``` + + + +#### `touch(path)` + +Create file if missing without truncating, or update timestamps if it exists. + +**Parameters**: + +- `path` (`string`): Input path. + +**Return**: + +- `touched` (`true?`): `true` when the file exists after touch, or `nil` on + failure. +- `errmsg` (`string?`): Error message when the check fails. +- `errcode` (`integer?`): OS error code when available. + +**Example**: + +```lua +fs.touch("tmp.txt") --> true, nil +``` + + + +#### `write_bytes(path, data)` + +Write full file in binary mode. + +**Parameters**: + +- `path` (`string`): Input path. +- `data` (`string`): Input data. + +**Return**: + +- `written` (`true?`): `true` when writing succeeds, or `nil` on failure. +- `errmsg` (`string?`): Error message when the check fails. +- `errcode` (`integer?`): OS error code when available. + +**Example**: + +```lua +fs.write_bytes("tmp.bin", "abc") --> true, nil +``` + + + +#### `write_text(path, data)` + +Write full file in text mode. + +**Parameters**: + +- `path` (`string`): Input path. +- `data` (`string`): Input data. + +**Return**: + +- `written` (`true?`): `true` when writing succeeds, or `nil` on failure. +- `errmsg` (`string?`): Error message when the check fails. +- `errcode` (`integer?`): OS error code when available. + +**Example**: + +```lua +fs.write_text("tmp.txt", "abc") --> true, nil +``` + +### Metadata + + + +#### `getatime(path)` + +Return last access time. + +**Parameters**: + +- `path` (`string`): Input path. + +**Return**: + +- `timestamp` (`number?`): Access time (seconds since epoch). +- `errmsg` (`string?`): Error message when the check fails. +- `errcode` (`integer?`): OS error code when available. + +**Example**: + +```lua +fs.getatime("README.md") --> 1712345678 +``` + + + +#### `getctime(path)` + +Return metadata change time. + +**Parameters**: + +- `path` (`string`): Input path. + +**Return**: + +- `timestamp` (`number?`): Change time (seconds since epoch). +- `errmsg` (`string?`): Error message when the check fails. +- `errcode` (`integer?`): OS error code when available. + +**Example**: + +```lua +fs.getctime("README.md") --> 1712345678 +``` + + + +#### `getmtime(path)` + +Return last modification time. + +**Parameters**: + +- `path` (`string`): Input path. + +**Return**: + +- `timestamp` (`number?`): Modification time (seconds since epoch). +- `errmsg` (`string?`): Error message when the check fails. +- `errcode` (`integer?`): OS error code when available. + +**Example**: + +```lua +fs.getmtime("README.md") --> 1712345678 +``` + + + +#### `getsize(path)` + +Return file size in bytes. + +**Parameters**: + +- `path` (`string`): Input path. + +**Return**: + +- `size` (`integer?`): File size in bytes. +- `errmsg` (`string?`): Error message when the check fails. +- `errcode` (`integer?`): OS error code when available. + +**Example**: + +```lua +fs.getsize("README.md") --> 1234 +``` + + + +#### `lstat(path)` + +Return symlink-aware file attributes. + +**Parameters**: + +- `path` (`string`): Input path. + +**Return**: + +- `attrs` (`LuaFileSystem.Attributes?`): Symlink-aware attributes, or `nil` on + failure. +- `errmsg` (`string?`): Error message when the check fails. +- `errcode` (`integer?`): OS error code when available. + +**Example**: + +```lua +fs.lstat("README.md") +``` + + + +#### `samefile(a, b)` + +Return whether two paths refer to the same file, or `nil` and an error on +failure. + +**Parameters**: + +- `a` (`string`): Input path. +- `b` (`string`): Input path. + +**Return**: + +- `isSameFile` (`boolean?`): True when both paths refer to the same file. +- `errmsg` (`string?`): Error message when the check fails. +- `errcode` (`integer?`): OS error code when available. + +**Example**: + +```lua +fs.samefile("README.md", "README.md") --> true +``` + + + +#### `stat(path)` + +Return file attributes. + +**Parameters**: + +- `path` (`string`): Input path. + +**Return**: + +- `attrs` + (`string|integer|LuaFileSystem.AttributeMode|LuaFileSystem.Attributes?`): File + attributes, or `nil` on failure. +- `errmsg` (`string?`): Error message when the check fails. +- `errcode` (`integer?`): OS error code when available. + +**Example**: + +```lua +fs.stat("README.md") +``` + +### Reading + + + +#### `dir(path, opts?)` + +Iterator over items in `path`. + +**Options**: + +- `recursive`: recurse into subdirectories; defaults to `false`. +- `hidden`: include hidden entries; defaults to `true`. +- `follow`: recurse into symlinked directories; defaults to `false`. +- `type`: filter by entry type, such as `"file"` or `"directory"`; defaults to + `nil`. + +**Parameters**: + +- `path` (`string`): Input path. +- `opts?` + (`{hidden?:boolean, recursive?:boolean, follow?:boolean, type?:modsFsEntryType}`): + Optional traversal options. + +**Return**: + +- `iterator` + (`(fun(state:table, prev?:string):basename:string?, type:modsFsEntryType?)?`): + Iterator, or `nil` on failure. +- `state` (`table|string`): Iterator state on success, or error message on + failure. + +**Example**: + +```lua +for name, type in fs.dir(path.cwd(), { recursive = true }) do + print(name, type) +end +``` + + + +#### `listdir(path, opts?)` + +Return direct children of a directory. + +**Options**: + +- `recursive`: recurse into subdirectories; defaults to `false`. +- `hidden`: include hidden entries; defaults to `true`. +- `follow`: recurse into symlinked directories; defaults to `false`. +- `type`: filter by entry type, such as `"file"` or `"directory"`; defaults to + `nil`. +- `names`: return basenames; defaults to `false`. + +**Parameters**: + +- `path` (`string`): Input path. +- `opts?` + (`{hidden?:boolean, recursive?:boolean, follow?:boolean, type?:modsFsEntryType, names?:boolean}`): + Optional traversal options. + +**Return**: + +- `paths` (`mods.List?`): Direct child paths, or basenames when + `opts.names` is `true`. +- `err` (`string?`): Error message when traversal setup fails. + +**Example**: + +```lua +fs.listdir("src") +fs.listdir("src", { names = true }) +``` + + + +#### `read_bytes(path)` + +Read full file in binary mode. + +**Parameters**: + +- `path` (`string`): Input path. + +**Return**: + +- `body` (`string?`): File contents read in binary mode, or `nil` on failure. +- `errmsg` (`string?`): Error message when the check fails. +- `errcode` (`integer?`): OS error code when available. + +**Example**: + +```lua +fs.read_bytes("README.md") +``` + + + +#### `read_text(path)` + +Read full file in text mode. + +**Parameters**: + +- `path` (`string`): Input path. + +**Return**: + +- `body` (`string?`): File contents read in text mode, or `nil` on failure. +- `errmsg` (`string?`): Error message when the check fails. +- `errcode` (`integer?`): OS error code when available. + +**Example**: + +```lua +fs.read_text("README.md") +``` diff --git a/docs/api/glob.md b/docs/api/glob.md new file mode 100644 index 0000000..045a163 --- /dev/null +++ b/docs/api/glob.md @@ -0,0 +1,267 @@ +--- +description: "Glob-style matching and filesystem expansion helpers." +--- + +# `glob` + +Glob-style matching and filesystem expansion helpers. + +## Usage + +```lua +glob = require "mods.glob" + +print(glob.match("src/mods/fs.lua", "**/*.lua")) --> true +print(glob.match("DATA.TXT", "*.txt", true)) --> true +print(glob.filter({ "a.lua", "b.txt" }, "*.lua")[1]) --> "a.lua" +print(glob.glob("src", "*.lua")[1]) +``` + +## Supported wildcards + +- `*`: match zero or more characters within one path segment. + + ```lua + match("main.lua", "*.lua") + ``` + +- `?`: match exactly one character within one path segment. + + ```lua + match("a1.lua", "a?.lua") + ``` + +- `[]`: match one character from a bracket class like `[a-z]`. + + ```lua + match("file7.lua", "file[0-9].lua") + ``` + +- `[!]`: negate a bracket class, like `[!0-9]`. + + ```lua + match("filex.lua", "file[!0-9].lua") + ``` + +- `{a,b}`: match one of several brace alternatives. + + ```lua + match("init.lua", "init.{lua,luac}") + ``` + +- `**`: match across path segments recursively. + + ```lua + match("src/mods/fs.lua", "**/*.lua") + ``` + +## Functions + +**Glob Operations**: + +| Function | Description | +| --------------------------------------------------- | ----------------------------------------------------------- | +| [`escape(s)`](#fn-escape) | Escape glob metacharacters in a literal string. | +| [`filter(names, pattern, ignorecase?)`](#fn-filter) | Return the values from `names` that match the glob pattern. | +| [`glob(path, pattern?, opts?)`](#fn-glob) | Return glob matches under `path`. | +| [`has_magic(s)`](#fn-has-magic) | Return whether a pattern contains glob metacharacters. | +| [`iglob(path, pattern?, opts?)`](#fn-iglob) | Iterator over glob matches under `path`. | +| [`match(path, pattern, ignorecase?)`](#fn-match) | Match a path against a glob pattern. | +| [`translate(pattern)`](#fn-translate) | Translate one glob segment into an equivalent Lua pattern. | + +### Glob Operations + + + +#### `escape(s)` + +Escape glob metacharacters in a literal string. + +**Parameters**: + +- `s` (`string`): Input literal string. + +**Return**: + +- `pattern` (`string`): Escaped glob pattern. + +**Example**: + +```lua +glob.escape("a*b") --> "a\\*b" +``` + + + +#### `filter(names, pattern, ignorecase?)` + +Return the values from `names` that match the glob pattern. + +**Parameters**: + +- `names` (`string[]`): Input names. +- `pattern` (`string`): Input glob pattern. +- `ignorecase?` (`boolean`): Override platform-default case matching. + +**Return**: + +- `matches` (`mods.List`): Matching values from `names`. + +**Example**: + +```lua +glob.filter({ "a.lua", "b.txt", "c.lua" }, "*.lua") --> { "a.lua", "c.lua" } +``` + + + +#### `glob(path, pattern?, opts?)` + +Return glob matches under `path`. + +**Options**: + +- `hidden`: include hidden paths; defaults to `true`. +- `recursive`: recurse into subdirectories; defaults to `false`. +- `follow`: recurse into symlinked directories; defaults to `false`. +- `ignorecase`: use case-insensitive matching; defaults to platform semantics. + +**Parameters**: + +- `path` (`string`): Input path. +- `pattern?` (`string`): Optional pattern to match. +- `opts?` + (`{hidden?:boolean, recursive?:boolean, follow?:boolean, ignorecase?:boolean}`): + Optional glob options. + +**Return**: + +- `paths` (`mods.List`): Matching paths under `path`. + +**Example**: + +```lua +glob.glob("src", "*.lua") +glob.glob("src", "*.lua", { recursive = true }) +``` + + + +#### `has_magic(s)` + +Return whether a pattern contains glob metacharacters. + +**Parameters**: + +- `s` (`string`): Input string. + +**Return**: + +- `has_magic` (`boolean`): True when the string contains glob syntax. + +**Example**: + +```lua +glob.has_magic("foo.txt") --> false +glob.has_magic("*.txt") --> true +``` + + + +#### `iglob(path, pattern?, opts?)` + +Iterator over glob matches under `path`. + +**Options**: + +- `hidden`: include hidden paths; defaults to `true`. +- `recursive`: recurse into subdirectories; defaults to `false`. +- `follow`: recurse into symlinked directories; defaults to `false`. +- `ignorecase`: use case-insensitive matching; defaults to platform semantics. + +**Parameters**: + +- `path` (`string`): Input path. +- `pattern?` (`string`): Optional pattern to match. +- `opts?` + (`{hidden?:boolean, recursive?:boolean, follow?:boolean, ignorecase?:boolean}`): + Optional glob options. + +**Return**: + +- `iterator` (`(fun(state:table, prev?:string): (path:string?))`): Iterator + function. +- `state` (`table`): Iterator state table. +- `initial` (`nil`): Initial iterator value. + +**Example**: + +```lua +for path in glob.iglob("src", "*.lua") do + print(path) +end +``` + + + +#### `match(path, pattern, ignorecase?)` + +Match a path against a glob pattern. + +**Parameters**: + +- `path` (`string`): Input path. +- `pattern` (`string`): Input glob pattern. +- `ignorecase?` (`boolean`): Override platform-default case matching. + +**Return**: + +- `matches` (`boolean`): True when the path matches the pattern. + +**Example**: + +```lua +glob.match("src/mods/fs.lua", "**/*.lua") --> true +``` + + + +#### `translate(pattern)` + +Translate one glob segment into an equivalent Lua pattern. + +**Parameters**: + +- `pattern` (`string`): Input glob segment. + +**Return**: + +- `lua_pattern` (`string`): Lua pattern string. + +**Example**: + +```lua +local s = "init.lua" +local pattern = "*.lua" +local matches = glob.match(s, pattern) +local translated_matches = s:match(glob.translate(pattern)) ~= nil +print(matches == translated_matches) --> true +``` + +> [!NOTE] +> +> - `*` and `?` stay within a single path segment. +> +> ```lua +> local pattern = "*.txt" +> print(glob.translate(pattern)) --> "^[^/]*%.txt$" +> print(glob.match("foo/bar.txt", pattern)) --> false +> ``` +> +> - `**` and `{a,b}` need higher-level matching logic. +> +> ```lua +> pattern = "src/{x,y}.lua" +> print(("src/x.lua"):match(glob.translate(pattern))) --> nil +> print(glob.match("src/x.lua", pattern)) --> true +> ``` diff --git a/docs/api/is.md b/docs/api/is.md new file mode 100644 index 0000000..0505cbd --- /dev/null +++ b/docs/api/is.md @@ -0,0 +1,558 @@ +--- +description: "Type predicates for Lua values and filesystem path types." +--- + +# `is` + +Type predicates for Lua values and filesystem path types. + +## Usage + +```lua +is = require "mods.is" + +ok = is.number(3.14) --> true +ok = is("hello", "string") --> true +ok = is.table({}) --> true +``` + +> [!NOTE] +> +> Function names are case-insensitive. +> +> ```lua +> is.table({}) --> true +> is.Table({}) --> true +> is.tAbLe({}) --> true +> ``` + + + +> [!IMPORTANT] +> +> Path checks require **LuaFileSystem** +> ([`lfs`](https://github.com/lunarmodules/luafilesystem)) and raise an error if +> it is not installed. + + + +## `is()` + +`is` is also callable as `is(value, type)` to check if a value is of a given +type. + +```lua +is("hello", "string") --> true +is("hello", "String") --> true +is("hello", "STRING") --> true +``` + +## Functions + +**Path Checks**: + +| Function | Description | +| ------------------------- | ------------------------------------------------------------ | +| [`block(v)`](#fn-block) | Returns `true` when `v` is a block device path. | +| [`char(v)`](#fn-char) | Returns `true` when `v` is a character device path. | +| [`device(v)`](#fn-device) | Returns `true` when `v` is a block or character device path. | +| [`dir(v)`](#fn-dir) | Returns `true` when `v` is a directory path. | +| [`fifo(v)`](#fn-fifo) | Returns `true` when `v` is a FIFO path. | +| [`file(v)`](#fn-file) | Returns `true` when `v` is a file path. | +| [`link(v)`](#fn-link) | Returns `true` when `v` is a symlink path. | +| [`path(v)`](#fn-path) | Returns `true` when `v` is a valid filesystem path. | +| [`socket(v)`](#fn-socket) | Returns `true` when `v` is a socket path. | + +**Type Checks**: + +| Function | Description | +| ----------------------------- | -------------------------------------- | +| [`boolean(v)`](#fn-boolean) | Returns `true` when `v` is a boolean. | +| [`function(v)`](#fn-function) | Returns `true` when `v` is a function. | +| [`nil(v)`](#fn-nil) | Returns `true` when `v` is `nil`. | +| [`number(v)`](#fn-number) | Returns `true` when `v` is a number. | +| [`string(v)`](#fn-string) | Returns `true` when `v` is a string. | +| [`table(v)`](#fn-table) | Returns `true` when `v` is a table. | +| [`thread(v)`](#fn-thread) | Returns `true` when `v` is a thread. | +| [`userdata(v)`](#fn-userdata) | Returns `true` when `v` is userdata. | + +**Value Checks**: + +| Function | Description | +| ----------------------------- | ------------------------------------------- | +| [`callable(v)`](#fn-callable) | Returns `true` when `v` is callable. | +| [`false(v)`](#fn-false) | Returns `true` when `v` is exactly `false`. | +| [`falsy(v)`](#fn-falsy) | Returns `true` when `v` is falsy. | +| [`integer(v)`](#fn-integer) | Returns `true` when `v` is an integer. | +| [`true(v)`](#fn-true) | Returns `true` when `v` is exactly `true`. | +| [`truthy(v)`](#fn-truthy) | Returns `true` when `v` is truthy. | + +### Path Checks + + + +#### `block(v)` + +Returns `true` when `v` is a block device path. + +**Parameters**: + +- `v` (`any`): Value to validate. + +**Return**: + +- `isBlock` (`boolean`): Whether the check succeeds. + +**Example**: + +```lua +is.block("/dev/sda") +``` + + + +#### `char(v)` + +Returns `true` when `v` is a character device path. + +**Parameters**: + +- `v` (`any`): Value to validate. + +**Return**: + +- `isChar` (`boolean`): Whether the check succeeds. + +**Example**: + +```lua +is.char("/dev/null") +``` + + + +#### `device(v)` + +Returns `true` when `v` is a block or character device path. + +**Parameters**: + +- `v` (`any`): Value to validate. + +**Return**: + +- `isDevice` (`boolean`): Whether the check succeeds. + +**Example**: + +```lua +is.device("/dev/null") +``` + + + +#### `dir(v)` + +Returns `true` when `v` is a directory path. + +**Parameters**: + +- `v` (`any`): Value to validate. + +**Return**: + +- `isDir` (`boolean`): Whether the check succeeds. + +**Example**: + +```lua +is.dir("/tmp") +``` + + + +#### `fifo(v)` + +Returns `true` when `v` is a FIFO path. + +**Parameters**: + +- `v` (`any`): Value to validate. + +**Return**: + +- `isFifo` (`boolean`): Whether the check succeeds. + +**Example**: + +```lua +is.fifo("/path/to/fifo") +``` + + + +#### `file(v)` + +Returns `true` when `v` is a file path. + +**Parameters**: + +- `v` (`any`): Value to validate. + +**Return**: + +- `isFile` (`boolean`): Whether the check succeeds. + +**Example**: + +```lua +is.file("README.md") +``` + + + +#### `link(v)` + +Returns `true` when `v` is a symlink path. + +**Parameters**: + +- `v` (`any`): Value to validate. + +**Return**: + +- `isLink` (`boolean`): Whether the check succeeds. + +**Example**: + +```lua +is.link("/path/to/link") +``` + + + +#### `path(v)` + +Returns `true` when `v` is a valid filesystem path. + +**Parameters**: + +- `v` (`any`): Value to validate. + +**Return**: + +- `isPath` (`boolean`): Whether the check succeeds. + +**Example**: + +```lua +is.path("README.md") +``` + +> [!NOTE] +> +> Returns `true` for broken symlinks. + + + +#### `socket(v)` + +Returns `true` when `v` is a socket path. + +**Parameters**: + +- `v` (`any`): Value to validate. + +**Return**: + +- `isSocket` (`boolean`): Whether the check succeeds. + +**Example**: + +```lua +is.socket("/path/to/socket") +``` + +### Type Checks + + + +#### `boolean(v)` + +Returns `true` when `v` is a boolean. + +**Parameters**: + +- `v` (`any`): Value to validate. + +**Return**: + +- `isBoolean` (`boolean`): Whether the check succeeds. + +**Example**: + +```lua +is.boolean(true) +``` + + + +#### `function(v)` + +Returns `true` when `v` is a function. + +**Parameters**: + +- `v` (`any`): Value to validate. + +**Return**: + +- `isFunction` (`boolean`): Whether the check succeeds. + +**Example**: + +```lua +is.Function(function() end) +``` + + + +#### `nil(v)` + +Returns `true` when `v` is `nil`. + +**Parameters**: + +- `v` (`any`): Value to validate. + +**Return**: + +- `isNil` (`boolean`): Whether the check succeeds. + +**Example**: + +```lua +is.Nil(nil) +``` + + + +#### `number(v)` + +Returns `true` when `v` is a number. + +**Parameters**: + +- `v` (`any`): Value to validate. + +**Return**: + +- `isNumber` (`boolean`): Whether the check succeeds. + +**Example**: + +```lua +is.number(3.14) +``` + + + +#### `string(v)` + +Returns `true` when `v` is a string. + +**Parameters**: + +- `v` (`any`): Value to validate. + +**Return**: + +- `isString` (`boolean`): Whether the check succeeds. + +**Example**: + +```lua +is.string("hello") +``` + + + +#### `table(v)` + +Returns `true` when `v` is a table. + +**Parameters**: + +- `v` (`any`): Value to validate. + +**Return**: + +- `isTable` (`boolean`): Whether the check succeeds. + +**Example**: + +```lua +is.table({}) +``` + + + +#### `thread(v)` + +Returns `true` when `v` is a thread. + +**Parameters**: + +- `v` (`any`): Value to validate. + +**Return**: + +- `isThread` (`boolean`): Whether the check succeeds. + +**Example**: + +```lua +is.thread(coroutine.create(function() end)) +``` + + + +#### `userdata(v)` + +Returns `true` when `v` is userdata. + +**Parameters**: + +- `v` (`any`): Value to validate. + +**Return**: + +- `isUserdata` (`boolean`): Whether the check succeeds. + +**Example**: + +```lua +is.userdata(io.stdout) +``` + +### Value Checks + + + +#### `callable(v)` + +Returns `true` when `v` is callable. + +**Parameters**: + +- `v` (`any`): Value to validate. + +**Return**: + +- `isCallable` (`boolean`): Whether the check succeeds. + +**Example**: + +```lua +is.callable(function() end) +``` + + + +#### `false(v)` + +Returns `true` when `v` is exactly `false`. + +**Parameters**: + +- `v` (`any`): Value to validate. + +**Return**: + +- `isFalse` (`boolean`): Whether the check succeeds. + +**Example**: + +```lua +is.False(false) +``` + + + +#### `falsy(v)` + +Returns `true` when `v` is falsy. + +**Parameters**: + +- `v` (`any`): Value to validate. + +**Return**: + +- `isFalsy` (`boolean`): Whether the check succeeds. + +**Example**: + +```lua +is.falsy(false) +``` + + + +#### `integer(v)` + +Returns `true` when `v` is an integer. + +**Parameters**: + +- `v` (`any`): Value to validate. + +**Return**: + +- `isInteger` (`boolean`): Whether the check succeeds. + +**Example**: + +```lua +is.integer(42) +``` + + + +#### `true(v)` + +Returns `true` when `v` is exactly `true`. + +**Parameters**: + +- `v` (`any`): Value to validate. + +**Return**: + +- `isTrue` (`boolean`): Whether the check succeeds. + +**Example**: + +```lua +is.True(true) +``` + + + +#### `truthy(v)` + +Returns `true` when `v` is truthy. + +**Parameters**: + +- `v` (`any`): Value to validate. + +**Return**: + +- `isTruthy` (`boolean`): Whether the check succeeds. + +**Example**: + +```lua +is.truthy("non-empty") +``` diff --git a/docs/api/json.md b/docs/api/json.md new file mode 100644 index 0000000..f123b42 --- /dev/null +++ b/docs/api/json.md @@ -0,0 +1,165 @@ +--- +description: "JSON encoding and decoding helpers." +--- + +# `json` + +JSON encoding and decoding helpers. + +> [!NOTE] +> +> This module aims to implement strict +> [RFC 8259](https://www.rfc-editor.org/rfc/rfc8259) JSON behavior. + +## Usage + +```lua +local json = require "mods.json" + +local encoded = json.encode({ ok = true, value = 42 }) +local decoded = json.decode(encoded) + +print(encoded) --> {"ok":true,"value":42} +print(decoded.ok) --> true +print(decoded.value) --> 42 +``` + +## Behavior + +- booleans, strings, and finite numbers map directly to JSON values + +```lua +print(json.encode(true)) --> true +print(json.encode("hi")) --> "hi" +print(json.encode(12.5)) --> 12.5 +``` + +- `json.null` encodes to `null`, and decoded `null` becomes `json.null` + +```lua +local value = json.decode('{"a":null}') +print(value.a == json.null) --> true +print(json.encode(json.null)) --> null +``` + +- tables encode as arrays or objects based on their keys + +```lua +print(json.encode({})) --> [] +print(json.encode({ "a", "b" })) --> ["a","b"] +print(json.encode({ a = 1 })) --> {"a":1} +``` + +- standalone JSON values like booleans, strings, and numbers are valid + +```lua +print(json.decode("true")) --> true +print(json.decode('"text"')) --> text +print(json.decode("42")) --> 42 +``` + +- encoding rejects unsupported Lua types, cyclic tables, mixed tables, sparse + arrays, `NaN`, and infinities + +```lua +local t = {} +t.self = t + +assert(json.encode(function() end)) +--> unsupported type: function + +assert(json.encode(t)) +--> cannot encode cyclic table + +assert(json.encode({ [1] = "a", b = true })) +--> cannot encode mixed table + +assert(json.encode({ [1] = "a", [3] = "c" })) +--> cannot encode sparse array + +assert(json.encode(0 / 0)) +--> cannot encode NaN + +assert(json.encode(math.huge)) +--> cannot encode infinity +``` + +- decoding rejects comments, trailing commas, single-quoted strings, and invalid + escapes + +```lua +assert(json.decode('{"a":1,}')) +--> expected string key at line 1, column 8 + +assert(json.decode('{"a":1}// comment')) +--> unexpected trailing content at line 1, column 8 + +assert(json.decode("{'a':1}")) +--> expected string key at line 1, column 2 + +assert(json.decode('["\\x"]')) +--> invalid escape sequence at line 1, column 3 +``` + +## Functions + + + +### `decode(s)` + +Decode a JSON string into Lua values. + +**Parameters**: + +- `s` (`string`): JSON string. + +**Return**: + +- `value` (`any`): Decoded Lua value. +- `err` (`string?`): Error message when decoding fails. + +**Example**: + +```lua +local value = json.decode('{"user":"Ada","active":true,"note":null}') +print(value.user) --> Ada +print(value.active) --> true +print(value.note == json.null) --> true +``` + + + +### `encode(value, opts?)` + +Encode a Lua value as JSON. + +**Parameters**: + +- `value` (`any`): Lua value to encode. +- `opts?` (`{sort_keys?:boolean, indent?:string}`): Encoding options. + +**Return**: + +- `json` (`string?`): JSON string. +- `err` (`string?`): Error message when encoding fails. + +**Example**: + +```lua +local s = json.encode({ + ok = true, + items = { 1, 2, 3 }, +}, { + indent = " ", +}) + +print(s) +-- { +-- "ok": true, +-- "items": [ +-- 1, +-- 2, +-- 3 +-- ] +-- } +``` diff --git a/docs/api/keyword.md b/docs/api/keyword.md new file mode 100644 index 0000000..738d8c4 --- /dev/null +++ b/docs/api/keyword.md @@ -0,0 +1,140 @@ +--- +description: "Helpers for Lua keywords and identifiers." +--- + +# `keyword` + +Helpers for Lua keywords and identifiers. + +## Usage + +```lua +kw = require "mods.keyword" + +kw.iskeyword("local")) --> true +kw.isidentifier("hello_world") --> true +``` + +## Functions + +**Collections**: + +| Function | Description | +| ------------------------ | ------------------------------------- | +| [`kwlist()`](#fn-kwlist) | Return Lua keywords as a `mods.List`. | +| [`kwset()`](#fn-kwset) | Return Lua keywords as a `mods.Set`. | + +**Normalization**: + +| Function | Description | +| ----------------------------------------------------- | ---------------------------------------------- | +| [`normalize_identifier(s)`](#fn-normalize-identifier) | Normalize an input into a safe Lua identifier. | + +**Predicates**: + +| Function | Description | +| ------------------------------------- | ------------------------------------------------------------- | +| [`isidentifier(v)`](#fn-isidentifier) | Return `true` when `v` is a valid non-keyword Lua identifier. | +| [`iskeyword(v)`](#fn-iskeyword) | Return `true` when `v` is a reserved Lua keyword. | + +### Collections + + + +#### `kwlist()` + +Return Lua keywords as a `mods.List`. + +**Return**: + +- `words` (`mods.List`): List of Lua keywords. + +**Example**: + +```lua +kw.kwlist():contains("and") --> true +kw.kwlist():contains("global") --> true -- Lua 5.5+ +``` + + + +#### `kwset()` + +Return Lua keywords as a `mods.Set`. + +**Return**: + +- `words` (`mods.Set`): Set of Lua keywords. + +**Example**: + +```lua +kw.kwlset():contains("and") --> true +kw.kwlset():contains("global") --> true -- Lua 5.5+ +``` + +### Normalization + + + +#### `normalize_identifier(s)` + +Normalize an input into a safe Lua identifier. + +**Parameters**: + +- `s` (`string`): Input string. + +**Return**: + +- `identifier` (`string`): Normalized Lua identifier. + +**Example**: + +```lua +kw.normalize_identifier(" 2 bad-name ") --> "_2_bad_name" +``` + +### Predicates + + + +#### `isidentifier(v)` + +Return `true` when `v` is a valid non-keyword Lua identifier. + +**Parameters**: + +- `v` (`any`): Value to validate. + +**Return**: + +- `isIdentifier` (`boolean`): Whether the check succeeds. + +**Example**: + +```lua +kw.isidentifier("hello_world") --> true +kw.isidentifier("local") --> false +``` + + + +#### `iskeyword(v)` + +Return `true` when `v` is a reserved Lua keyword. + +**Parameters**: + +- `v` (`any`): Value to validate. + +**Return**: + +- `isKeyword` (`boolean`): Whether the check succeeds. + +**Example**: + +```lua +kw.iskeyword("function") --> true +kw.iskeyword("hello") --> false +``` diff --git a/docs/api/list.md b/docs/api/list.md new file mode 100644 index 0000000..7a6bba1 --- /dev/null +++ b/docs/api/list.md @@ -0,0 +1,1343 @@ +--- +description: + "A list class for creating, transforming, and querying sequences of values." +--- + +# `List` + +A list class for creating, transforming, and querying sequences of values. + +## Usage + +```lua +List = require "mods.List" + +ls = List({ "a" }):append("b") +print(ls:contains("b")) --> true +print(ls:index("b")) --> 2 +``` + +> [!NOTE] +> +> `List(t)` wraps `t` with the `List` metatable in place. It does not copy or +> filter table values. `List(t):copy()` or `List.copy(t)` both copy only `1..#t` +> and wrap `t` as a List. + +## Functions + +**Access**: + +| Function | Description | +| ---------------------- | ------------------------------------------- | +| [`first()`](#fn-first) | Return the first element or `nil` if empty. | +| [`last()`](#fn-last) | Return the last element or `nil` if empty. | + +**Copies**: + +| Function | Description | +| -------------------- | ---------------------------------- | +| [`copy()`](#fn-copy) | Return a shallow copy of the list. | + +**Mutation**: + +| Function | Description | +| ------------------------------ | -------------------------------------------------------------------- | +| [`append()`](#fn-append) | Append a value to the end of the list. | +| [`clear()`](#fn-clear) | Remove all elements from the list. | +| [`extend(t)`](#fn-extend) | Extend the list with another list or set. | +| [`extract(pred)`](#fn-extract) | Extract values matching the predicate and remove them from the list. | +| [`insert(pos, v)`](#fn-insert) | Insert a value at the given position. | +| [`insert(v)`](#fn-insert) | Append a value to the end of the list. | +| [`pop()`](#fn-pop) | Remove and return the last element. | +| [`pop(pos)`](#fn-pop) | Remove and return the element at the given position. | +| [`prepend(v)`](#fn-prepend) | Insert a value at the start of the list. | +| [`remove(v)`](#fn-remove) | Remove the first matching value. | +| [`shuffle(rng?)`](#fn-shuffle) | Shuffle the list in place. | +| [`sort(comp?)`](#fn-sort) | Sort the list in place. | + +**Predicates**: + +| Function | Description | +| -------------------------- | ------------------------------------------------- | +| [`all(pred)`](#fn-all) | Return `true` if all values match the predicate. | +| [`any(pred)`](#fn-any) | Return `true` if any value matches the predicate. | +| [`equals(ls)`](#fn-equals) | Compare two lists using shallow element equality. | +| [`le(ls)`](#fn-le) | Compare two lists lexicographically. | +| [`lt(ls)`](#fn-lt) | Compare two lists lexicographically. | + +**Queries**: + +| Function | Description | +| -------------------------------- | ----------------------------------------------------------- | +| [`contains(v)`](#fn-contains) | Return `true` if the list contains the value. | +| [`count(v)`](#fn-count) | Count how many times a value appears. | +| [`index(v)`](#fn-index) | Return the index of the first matching value. | +| [`index_if(pred)`](#fn-index-if) | Return the index of the first value matching the predicate. | +| [`isempty()`](#fn-isempty) | Return whether the list has no elements. | +| [`len()`](#fn-len) | Return the number of elements in the list. | + +**Transforms**: + +| Function | Description | +| ------------------------------------- | -------------------------------------------------------------------- | +| [`concat(sep?, i?, j?)`](#fn-concat) | Concatenate list values using Lua's native `table.concat` behavior. | +| [`difference(t)`](#fn-difference) | Return a new list with values not in the given list or set. | +| [`drop(n)`](#fn-drop) | Return a new list without the first n elements. | +| [`filter(pred)`](#fn-filter) | Return a new list with values matching the predicate. | +| [`flatten()`](#fn-flatten) | Flatten one level of nested lists. | +| [`foreach(fn)`](#fn-foreach) | Apply a function to each element (for side effects). | +| [`group_by(fn)`](#fn-group-by) | Group list values by a computed key. | +| [`intersection(t)`](#fn-intersection) | Return values that are also present in the given list or set. | +| [`invert()`](#fn-invert) | Invert values to indices in a new table. | +| [`join(sep?, quoted?)`](#fn-join) | Join list values into a string. | +| [`keypath()`](#fn-keypath) | Render list items as a table-access key path. | +| [`map(fn)`](#fn-map) | Return a new list by mapping each value. | +| [`mirror()`](#fn-mirror) | Mirror values into a new table as both keys and values. | +| [`mul(n)`](#fn-mul) | Return a new list repeated `n` times (list multiplication behavior). | +| [`reduce(fn, init?)`](#fn-reduce) | Reduce the list to a single value using an accumulator. | +| [`reverse()`](#fn-reverse) | Reverse the list in place. | +| [`slice(i?, j?)`](#fn-slice) | Return a new list containing items from i to j (inclusive). | +| [`take(n)`](#fn-take) | Return the first n elements as a new list. | +| [`toset()`](#fn-toset) | Convert the list to a set. | +| [`tostring()`](#fn-tostring) | Render the list to a string via the regular method form. | +| [`uniq()`](#fn-uniq) | Return a new list with duplicates removed (first occurrence kept). | +| [`zip(t)`](#fn-zip) | Zip two collections into a list of 2-element tables. | + +**Metamethods**: + +| Function | Description | +| ------------------------------ | --------------------------------------------------------------------------------------------------------------- | +| [`__add(ls)`](#fn-add) | Extend the left-hand list in place with right-hand values, then return the same left-hand list reference (`+`). | +| [`__eq(ls)`](#fn-eq) | Compare two lists using shallow element equality (`==`). | +| [`__le(ls)`](#fn-le) | Compare two lists lexicographically (`<=`). | +| [`__lt(ls)`](#fn-lt) | Compare two lists lexicographically (`<`). | +| [`__mul(n)`](#fn-mul) | Repeat a list `n` times (`*`). | +| [`__sub(ls)`](#fn-sub) | Return values from the left list that are not present in the right list (`-`). | +| [`__tostring()`](#fn-tostring) | Render the list to a string like `{ "a", "b", 1 }`. | + +### Access + + + +#### `first()` + +Return the first element or `nil` if empty. + +**Return**: + +- `firstValue` (`any`): First value, or `nil` if empty. + +**Example**: + +```lua +v = List({ "a", "b" }):first() --> "a" +``` + + + +#### `last()` + +Return the last element or `nil` if empty. + +**Return**: + +- `lastValue` (`any`): Last value, or `nil` if empty. + +**Example**: + +```lua +v = List({ "a", "b" }):last() --> "b" +``` + +### Copies + + + +#### `copy()` + +Return a shallow copy of the list. + +**Return**: + +- `ls` (`mods.List`): New list. + +**Example**: + +```lua +c = List({ "a", "b" }):copy() --> { "a", "b" } +``` + +### Mutation + + + +#### `append()` + +Append a value to the end of the list. + +**Return**: + +- `self` (`T`): Current list. + +**Example**: + +```lua +ls = List({ "a" }):append("b") --> { "a", "b" } +``` + + + +#### `clear()` + +Remove all elements from the list. + +**Return**: + +- `self` (`T`): Current list. + +**Example**: + +```lua +ls = List({ "a", "b" }):clear() --> { } +``` + + + +#### `extend(t)` + +Extend the list with another list or set. + +**Parameters**: + +- `t` (`mods.List|mods.Set|any[]`): Values to append. + +**Return**: + +- `self` (`T`): Current list. + +**Example**: + +```lua +ls = List({ "a" }):extend({ "b", "c" }) --> { "a", "b", "c" } +ls = List({ "a" }):extend(Set({ "b", "c" })) --> { "a", "b", "c" } +``` + +> [!NOTE] +> +> `extend` is also available through the `+` operator. + + + +#### `extract(pred)` + +Extract values matching the predicate and remove them from the list. + +**Parameters**: + +- `pred` (`fun(v:any):boolean`): Predicate function. + +**Return**: + +- `ls` (`mods.List`): Extracted values. + +**Example**: + +```lua +ls = List({ "a", "bb", "c" }) +is_len_1 = function(v) return #v == 1 end +ex = ls:extract(is_len_1) --> ex = { "a", "c" }, ls = { "bb" } +``` + + + +#### `insert(pos, v)` + +Insert a value at the given position. + +**Parameters**: + +- `pos` (`integer`): Insert position. +- `v` (`any`): Value to insert. + +**Return**: + +- `self` (`T`): Current list. + +**Example**: + +```lua +ls = List({ "a", "c" }):insert(2, "b") --> { "a", "b", "c" } +``` + + + +#### `insert(v)` + +Append a value to the end of the list. + +**Parameters**: + +- `v` (`any`): Value to append. + +**Return**: + +- `self` (`T`): Current list. + +**Example**: + +```lua +ls = List({ "a", "b" }):insert("c") --> { "a", "b", "c" } +``` + + + +#### `pop()` + +Remove and return the last element. + +**Return**: + +- `removedValue` (`any`): Removed value. + +**Example**: + +```lua +ls = List({ "a", "b" }) +v = ls:pop() --> v == "b"; ls is { "a" } +``` + + + +#### `pop(pos)` + +Remove and return the element at the given position. + +**Parameters**: + +- `pos` (`integer`): Numeric value. + +**Return**: + +- `removedValue` (`any`): Removed value. + +**Example**: + +```lua +ls = List({ "a", "b", "c" }) +v = ls:pop(2) --> v == "b"; ls is { "a", "c" } +``` + + + +#### `prepend(v)` + +Insert a value at the start of the list. + +**Parameters**: + +- `v` (`any`): Value to validate. + +**Return**: + +- `self` (`T`): Current list. + +**Example**: + +```lua +ls = List({ "b", "c" }) +ls:prepend("a") --> { "a", "b", "c" } +``` + + + +#### `remove(v)` + +Remove the first matching value. + +**Parameters**: + +- `v` (`any`): Value to validate. + +**Return**: + +- `self` (`T`): Current list. + +**Example**: + +```lua +ls = List({ "a", "b", "b" }) +ls:remove("b") --> { "a", "b" } +``` + + + +#### `shuffle(rng?)` + +Shuffle the list in place. + +**Parameters**: + +- `rng?` (`fun(lo:integer, hi:integer):integer`): Optional random index picker; + defaults to `math.random`. + +**Return**: + +- `self` (`T`): Current list. + +**Example**: + +```lua +ls = List({ "a", "b", "c" }):shuffle() --> { "b", "c", "a" } -- order varies +``` + + + +#### `sort(comp?)` + +Sort the list in place. + +**Parameters**: + +- `comp?` (`fun(a:any, b:any):boolean`): Optional comparison function (defaults + to `nil`). + +**Return**: + +- `self` (`T`): Current list. + +**Example**: + +```lua +ls = List({ 3, 1, 2 }) +ls:sort() --> { 1, 2, 3 } + +words = List({ "ccc", "a", "bb" }) +words:sort(function(a, b) + return #a < #b +end) --> { "a", "bb", "ccc" } +``` + +### Predicates + + + +#### `all(pred)` + +Return `true` if all values match the predicate. + +**Parameters**: + +- `pred` (`fun(v:any):boolean`): Predicate function. + +**Return**: + +- `allMatch` (`boolean`): Whether the condition is met. + +**Example**: + +```lua +is_even = function(v) return v % 2 == 0 end +ok = List({ 2, 4 }):all(is_even) --> true +``` + +> [!NOTE] +> +> Empty lists return `true`. + + + +#### `any(pred)` + +Return `true` if any value matches the predicate. + +**Parameters**: + +- `pred` (`fun(v:any):boolean`): Predicate function. + +**Return**: + +- `anyMatch` (`boolean`): Whether the condition is met. + +**Example**: + +```lua +has_len_2 = function(v) return #v == 2 end +ok = List({ "a", "bb" }):any(has_len_2) --> true +``` + + + +#### `equals(ls)` + +Compare two lists using shallow element equality. + +**Parameters**: + +- `ls` (`mods.List|any[]`): Other list value. + +**Return**: + +- `isEqual` (`boolean`): Whether the condition is met. + +**Example**: + +```lua +a = List({ "x", "y" }) +b = List({ "x", "y" }) +ok = a:equals(b) --> true +``` + +> [!NOTE] +> +> - `equals` is also available through the `==` operator when both operands are +> `List`. +> +> ```lua +> a = List({ "a", 1 }) +> b = List({ "a", 1 }) +> ok = (a == b) --> true +> ``` +> +> - Unlike `==`, this method also works when `ls` is a plain array table. +> +> ```lua +> a = List({ "a", 1 }) +> b = { "a", 1 } +> ok = a:equals(b) --> true +> ``` +> +> - `equals` checks only array positions (`1..#list`), so extra non-array keys +> are ignored: +> +> ```lua +> t = {} +> a = List({ "a", t }) +> b = { "a", t, a = 1 } +> ok = a:equals(b) --> true +> ``` + + + +#### `le(ls)` + +Compare two lists lexicographically. + +**Parameters**: + +- `ls` (`mods.List|any[]`): Other list value. + +**Return**: + +- `isLessOrEqual` (`boolean`): Whether the condition is met. + +**Example**: + +```lua +ok = List({ 1, 2 }):le({ 1, 2 }) --> true +ok = List({ 1, 2 }):le({ 1, 1 }) --> false +``` + +> [!NOTE] +> +> `le` is also available through the `<=` operator. + + + +#### `lt(ls)` + +Compare two lists lexicographically. + +**Parameters**: + +- `ls` (`mods.List|any[]`): Other list value. + +**Return**: + +- `isLess` (`boolean`): Whether the condition is met. + +**Example**: + +```lua +ok = List({ 1, 2 }):lt({ 1, 3 }) --> true +ok = List({ 1, 2 }):lt({ 1, 2, 0 }) --> true +``` + +> [!NOTE] +> +> `lt` is also available through the `<` operator. + +### Queries + + + +#### `contains(v)` + +Return `true` if the list contains the value. + +**Parameters**: + +- `v` (`any`): Value to validate. + +**Return**: + +- `isPresent` (`boolean`): True when `v` is present in the list. + +**Example**: + +```lua +ok = List({ "a", "b" }):contains("b") --> true +``` + + + +#### `count(v)` + +Count how many times a value appears. + +**Parameters**: + +- `v` (`any`): Value to validate. + +**Return**: + +- `res` (`integer`): Result count. + +**Example**: + +```lua +n = List({ "a", "b", "b" }):count("b") --> 2 +``` + + + +#### `index(v)` + +Return the index of the first matching value. + +**Parameters**: + +- `v` (`any`): Value to validate. + +**Return**: + +- `index` (`integer?`): Result index, or nil when not found. + +**Example**: + +```lua +i = List({ "a", "b", "c", "b" }):index("b") --> 2 +``` + + + +#### `index_if(pred)` + +Return the index of the first value matching the predicate. + +**Parameters**: + +- `pred` (`fun(v:any):boolean`): Predicate function. + +**Return**: + +- `index` (`integer?`): Result index, or nil when no value matches. + +**Example**: + +```lua +gt_1 = function(x) return x > 1 end +i = List({ 1, 2, 3 }):index_if(gt_1) --> 2 +``` + + + +#### `isempty()` + +Return whether the list has no elements. + +**Return**: + +- `empty` (`boolean`): `true` when the list has no elements. + +**Example**: + +```lua +ok = List():isempty() --> true +``` + + + +#### `len()` + +Return the number of elements in the list. + +**Return**: + +- `count` (`integer`): Element count. + +**Example**: + +```lua +n = List({ "a", "b", "c" }):len() --> 3 +``` + +> [!NOTE] +> +> Uses Lua's `#` operator. + +### Transforms + + + +#### `concat(sep?, i?, j?)` + +Concatenate list values using Lua's native `table.concat` behavior. + +**Parameters**: + +- `sep?` (`string`): Optional separator value (defaults to `""`). +- `i?` (`integer`): Optional start index (defaults to `1`). +- `j?` (`integer`): Optional end index (defaults to `#self`). + +**Return**: + +- `concatenated` (`string`): Concatenated string. + +**Example**: + +```lua +s = List({ "a", "b", "c" }):concat(",") --> "a,b,c" +``` + +> [!NOTE] +> +> This method forwards to `table.concat` directly and keeps its strict element +> rules. + + + +#### `difference(t)` + +Return a new list with values not in the given list or set. + +**Parameters**: + +- `t` (`mods.List|mods.Set|any[]`): Values to remove. + +**Return**: + +- `ls` (`T`): New list. + +**Example**: + +```lua +d = List({ "a", "b", "c" }):difference({ "b" }) --> { "a", "c" } +``` + +> [!NOTE] +> +> `difference` is also available through the `-` operator. + + + +#### `drop(n)` + +Return a new list without the first n elements. + +**Parameters**: + +- `n` (`integer`): Numeric value. + +**Return**: + +- `ls` (`mods.List`): New list. + +**Example**: + +```lua +t = List({ "a", "b", "c" }):drop(1) --> { "b", "c" } +``` + + + +#### `filter(pred)` + +Return a new list with values matching the predicate. + +**Parameters**: + +- `pred` (`fun(v:any):boolean`): Predicate function. + +**Return**: + +- `ls` (`mods.List`): New list. + +**Example**: + +```lua +is_len_1 = function(v) return #v == 1 end +f = List({ "a", "bb", "c" }):filter(is_len_1) --> { "a", "c" } +``` + + + +#### `flatten()` + +Flatten one level of nested lists. + +**Return**: + +- `ls` (`mods.List`): New list. + +**Example**: + +```lua +f = List({ { "a", "b" }, { "c" } }):flatten() --> { "a", "b", "c" } +``` + + + +#### `foreach(fn)` + +Apply a function to each element (for side effects). + +**Parameters**: + +- `fn` (`fun(v:any)`): Callback function. + +**Return**: + +- `none` (`nil`) + +**Example**: + +```lua +List({ "a", "b" }):foreach(print) +--> prints -> a +--> prints -> b +``` + + + +#### `group_by(fn)` + +Group list values by a computed key. + +**Parameters**: + +- `fn` (`fun(v:any):any`): Callback function. + +**Return**: + +- `groups` (`table`): Groups keyed by the callback result. + +**Example**: + +```lua +words = { "aa", "b", "ccc", "dd" } +g = List(words):group_by(string.len) --> { {"b"}, { "aa", "dd" }, { "ccc" } } +``` + + + +#### `intersection(t)` + +Return values that are also present in the given list or set. + +**Parameters**: + +- `t` (`mods.List|mods.Set|any[]`): Other list/set. + +**Return**: + +- `ls` (`mods.List`): New list. + +**Example**: + +```lua +i = List({ "a", "b", "a", "c" }):intersection({ "a", "c" }) +--> { "a", "a", "c" } +``` + +> [!NOTE] +> +> Order is preserved from the original list. + + + +#### `invert()` + +Invert values to indices in a new table. + +**Return**: + +- `idxByValue` (`table`): Table mapping each value to its last index. + +**Example**: + +```lua +t = List({ "a", "b", "c" }):invert() --> { a = 1, b = 2, c = 3 } +``` + + + +#### `join(sep?, quoted?)` + +Join list values into a string. + +**Parameters**: + +- `sep?` (`string`): Optional separator value (defaults to `""`). +- `quoted?` (`boolean`): Optional boolean flag (defaults to `false`). + +**Return**: + +- `joined` (`string`): Joined string. + +**Example**: + +```lua +s = List({ "a", "b", "c" }):join(",") --> "a,b,c" +s = List({ "a", "b", "c" }):join(", ", true) --> '"a", "b", "c"' +``` + +> [!NOTE] +> +> Values are converted with `tostring` before joining. Set `quoted = true` to +> quote string values. + + + +#### `keypath()` + +Render list items as a table-access key path. + +**Return**: + +- `keyPath` (`string`): Key-path string. + +**Example**: + +```lua +p = List({ "ctx", "users", 1, "name" }):keypath() --> "ctx.users[1].name" +``` + + + +#### `map(fn)` + +Return a new list by mapping each value. + +**Parameters**: + +- `fn` (`fun(value:T):any`): Callback function. + +**Return**: + +- `ls` (`mods.List`): New list. + +**Example**: + +```lua +to_upper = function(v) return v:upper() end +m = List({ "a", "b" }):map(to_upper) --> { "A", "B" } +``` + + + +#### `mirror()` + +Mirror values into a new table as both keys and values. + +**Return**: + +- `mirroredValues` (`table`): Table mapping each value to itself. + +**Example**: + +```lua +t = List({ "a", "b", "c" }):mirror() --> { a = "a", b = "b", c = "c" } +``` + + + +#### `mul(n)` + +Return a new list repeated `n` times (list multiplication behavior). + +**Parameters**: + +- `n` (`integer`): Numeric value. + +**Return**: + +- `ls` (`mods.List`): New list. + +**Example**: + +```lua +ls = List({ "a", "b" }):mul(3) --> { "a", "b", "a", "b", "a", "b" } +``` + +> [!NOTE] +> +> `mul` is also available through the `*` operator. + + + +#### `reduce(fn, init?)` + +Reduce the list to a single value using an accumulator. + +**Parameters**: + +- `fn` (`fun(acc:any, v:any):any`): Reducer function. +- `init?` (`any`): Optional initial accumulator; for non-empty lists, `nil` or + omitted uses the first item. + +**Return**: + +- `reducedValue` (`any`): Reduced value. + +**Example**: + +```lua +add = function(acc, v) return acc + v end +sum = List({ 1, 2, 3 }):reduce(add, 0) --> 6 +sum = List({ 1, 2, 3 }):reduce(add, 10) --> 16 +``` + +> [!NOTE] +> +> For empty lists, returns `init` unchanged (or `nil` when omitted). + + + +#### `reverse()` + +Reverse the list in place. + +**Return**: + +- `ls` (`mods.List`): Same list, reversed in place. + +**Example**: + +```lua +r = List({ "a", "b", "c" }):reverse() --> { "c", "b", "a" } +``` + + + +#### `slice(i?, j?)` + +Return a new list containing items from i to j (inclusive). + +**Parameters**: + +- `i?` (`integer`): Optional start index (defaults to `1`). +- `j?` (`integer`): Optional end index (defaults to `#self`). + +**Return**: + +- `ls` (`mods.List`): New list. + +**Example**: + +```lua +t = List({ "a", "b", "c", "d" }):slice(2, 3) --> { "b", "c" } +``` + +> [!NOTE] +> +> Supports negative indices (-1 is last element). + + + +#### `take(n)` + +Return the first n elements as a new list. + +**Parameters**: + +- `n` (`integer`): Numeric value. + +**Return**: + +- `ls` (`mods.List`): New list. + +**Example**: + +```lua +t = List({ "a", "b", "c" }):take(2) --> { "a", "b" } +``` + + + +#### `toset()` + +Convert the list to a set. + +**Return**: + +- `set` (`mods.Set`): New set. + +**Example**: + +```lua +s = List({ "a", "b", "a" }):toset() --> { a = true, b = true } +``` + +> [!NOTE] +> +> Order is preserved from the original list. + + + +#### `tostring()` + +Render the list to a string via the regular method form. + +**Return**: + +- `renderedList` (`string`): Rendered list string. + +**Example**: + +```lua +s = List({ "a", "b", 1 }):tostring() --> '{ "a", "b", 1 }' +``` + + + +#### `uniq()` + +Return a new list with duplicates removed (first occurrence kept). + +**Return**: + +- `ls` (`mods.List`): New list. + +**Example**: + +```lua +u = List({ "a", "b", "a", "c" }):uniq() --> { "a", "b", "c" } +``` + + + +#### `zip(t)` + +Zip two collections into a list of 2-element tables. + +**Parameters**: + +- `t` (`mods.List|mods.Set|any[]`): Values to pair with. + +**Return**: + +- `ls` (`mods.List`): New list. + +**Example**: + +```lua +z = List({ "a", "b" }):zip({ 1, 2 }) --> { {"a",1}, {"b",2} } +z = List({ "a", "b" }):zip(Set({ 1, 2 })) --> { {"a",1}, {"b",2} } +``` + +> [!NOTE] +> +> Length is the minimum of both tables' lengths. + +### Metamethods + + + +#### `__add(ls)` + +Extend the left-hand list in place with right-hand values, then return the same +left-hand list reference (`+`). + +**Parameters**: + +- `ls` (`mods.List|any[]`): Other list value. + +**Return**: + +- `self` (`mods.List|any[]`): Current list. + +**Example**: + +```lua +a = List({ "a", "b" }) +b = { "c", "d" } +c = a + b --> c and a are the same reference: { "a", "b", "c", "d" } +``` + +> [!NOTE] +> +> `+` operator is equivalent to `:extend(ls)`. + + + +#### `__eq(ls)` + +Compare two lists using shallow element equality (`==`). + +**Parameters**: + +- `ls` (`mods.List|any[]`): Other list value. + +**Return**: + +- `isEqual` (`boolean`): Whether the condition is met. + +**Example**: + +```lua +a = List({ "a", { 1 } }) +b = List({ "a", { 1 } }) +ok = a == b --> false (different nested table references) + +t = { 1 } +a = List({ "a", t }) +b = List({ "a", t }) +ok = a == b --> true (same nested table reference) +``` + +> [!NOTE] +> +> - `==` returns `false` for `List` vs plain-table comparisons. Use +> `:equals(ls)` for `List` vs plain-table comparisons. +> +> ```lua +> t = { "a", 1 } +> a = List(t) +> b = { "a", 1 } +> ok = (a == b) --> false +> ok = a:equals(b) --> true +> ``` +> +> - Like `:equals(ls)`, `==` compares only array positions (`1..#list`), so +> extra non-array keys are ignored when both operands are `List`. +> +> ```lua +> a = List({ "a", t }) +> b = List({ "a", t, extra = 1 }) +> ok = (a == b) --> true +> ``` + + + +#### `__le(ls)` + +Compare two lists lexicographically (`<=`). + +**Parameters**: + +- `ls` (`mods.List|any[]`): Other list value. + +**Return**: + +- `isLessOrEqual` (`boolean`): Whether the condition is met. + +**Example**: + +```lua +ok = List({ 1, 2 }) <= List({ 1, 2 }) --> true +``` + +> [!NOTE] +> +> `<=` is equivalent to `:le(ls)`. + + + +#### `__lt(ls)` + +Compare two lists lexicographically (`<`). + +**Parameters**: + +- `ls` (`mods.List|any[]`): Other list value. + +**Return**: + +- `isLess` (`boolean`): Whether the condition is met. + +**Example**: + +```lua +ok = List({ 1, 2 }) < List({ 1, 3 }) --> true +``` + +> [!NOTE] +> +> `<` is equivalent to `:lt(ls)`. + + + +#### `__mul(n)` + +Repeat a list `n` times (`*`). + +**Parameters**: + +- `n` (`integer|mods.List`): Right operand. + +**Return**: + +- `ls` (`mods.List`): New list. + +**Example**: + +```lua +l1 = List({ "a", "b" }) * 3 --> { "a", "b", "a", "b", "a", "b" } +l2 = 3 * List({ "a", "b" }) --> { "a", "b", "a", "b", "a", "b" } +``` + +> [!NOTE] +> +> `*` is equivalent to `:mul(n)`. + + + +#### `__sub(ls)` + +Return values from the left list that are not present in the right list (`-`). + +**Parameters**: + +- `ls` (`mods.List|any[]`): Other list value. + +**Return**: + +- `ls` (`mods.List`): New list. + +**Example**: + +```lua +a = List({ "a", "b", "c" }) +b = { "b" } +d = a - b --> { "a", "c" } +``` + +> [!NOTE] +> +> `-` operator is equivalent to `:difference(ls)`. + + + +#### `__tostring()` + +Render the list to a string like `{ "a", "b", 1 }`. + +**Return**: + +- `renderedList` (`string`): Rendered list string. + +**Example**: + +```lua +s = tostring(List({ "a", "b", 1 })) --> '{ "a", "b", 1 }' +``` diff --git a/docs/api/log.md b/docs/api/log.md new file mode 100644 index 0000000..749b4f0 --- /dev/null +++ b/docs/api/log.md @@ -0,0 +1,94 @@ +--- +description: + "Logger factory that emits normalized records through an optional custom + handler." +--- + +# `log` + +Logger factory that emits normalized records through an optional custom handler. +When `opts.handler` is omitted, records are written to `io.stderr`. + +## Usage + +```lua +log = require "mods.log" + +local logger = log.new() +logger:warn("config missing") --> writes: [WARN]: config missing +``` + +## Functions + +**Factory**: + +| Function | Description | +| ----------------------- | -------------------- | +| [`new(opts?)`](#fn-new) | Create a new logger. | + +**Logger Methods**: + +| Function | Description | +| -------------------------------- | ----------------------------------------------------------- | +| [`debug(...)`](#fn-debug) | Emit a `debug` record. | +| [`error(...)`](#fn-error) | Emit an `error` record. | +| [`info(...)`](#fn-info) | Emit an `info` record. | +| [`log(levelname, ...)`](#fn-log) | Emit a record for `level` when it passes the logger filter. | +| [`warn(...)`](#fn-warn) | Emit a `warn` record. | + +### Factory + + + +#### `new(opts?)` + +Create a new logger. **Parameters**: + +- `opts?` (`mods.log.new.opts`): Logger configuration. + +**Return**: + +- `logger` (`mods.log.logger`): Logger instance. + +### Logger Methods + + + +#### `debug(...)` + +Emit a `debug` record. **Parameters**: + +- `...` (`any`): Additional values joined with spaces. + + + +#### `error(...)` + +Emit an `error` record. **Parameters**: + +- `...` (`any`): Additional values joined with spaces. + + + +#### `info(...)` + +Emit an `info` record. **Parameters**: + +- `...` (`any`): Additional values joined with spaces. + + + +#### `log(levelname, ...)` + +Emit a record for `level` when it passes the logger filter. **Parameters**: + +- `levelname` (`string|"debug"|"info"|"warn"|"error"|"off"`): Log level to emit. +- `...` (`any`): Additional values joined with spaces. + + + +#### `warn(...)` + +Emit a `warn` record. **Parameters**: + +- `...` (`any`): Additional values joined with spaces. diff --git a/docs/api/ntpath.md b/docs/api/ntpath.md new file mode 100644 index 0000000..cdb218c --- /dev/null +++ b/docs/api/ntpath.md @@ -0,0 +1,76 @@ +--- +description: "Windows/NT-style path operations." +--- + +# `ntpath` + +Windows/NT-style path operations. + +> 💡Python `ntpath`-style behavior, ported to Lua. + +## Usage + +```lua +ntpath = require "mods.ntpath" + +print(ntpath.join([[C:\]], "Users", "me")) --> "C:\Users\me" +print(ntpath.normcase([[A/B\C]])) --> [[a\b\c]] +print(ntpath.splitdrive([[C:\Users\me]])) --> "C:", [[\Users\me]] +print(ntpath.isreserved([[C:\Temp\CON.txt]])) --> true +``` + +> ✨ Same API as `mods.path`, but with Windows/NT path semantics. + +## Functions + + + +### `_expand_percent_vars(p)` + +Expand percent-style variables in a string. **Parameters**: + +- `p` (`string`) + +**Return**: + +- `expanded` (`string`) + + + +### `ismount(path)` + +Return `true` when `path` points to a mount root. + +**Parameters**: + +- `path` (`string`): Path to inspect. + +**Return**: + +- `isMount` (`boolean`): `true` if the path resolves to a mount root. + +**Example**: + +```lua +ntpath.ismount([[C:\]]) --> true +``` + + + +### `isreserved(path)` + +Return `true` when `path` contains a reserved NT filename. + +**Parameters**: + +- `path` (`string`): Path to inspect. + +**Return**: + +- `isReserved` (`boolean`): `true` if any component is NT-reserved. + +**Example**: + +```lua +ntpath.isreserved([[a\CON.txt]]) --> true +``` diff --git a/docs/api/operator.md b/docs/api/operator.md new file mode 100644 index 0000000..31670a5 --- /dev/null +++ b/docs/api/operator.md @@ -0,0 +1,534 @@ +--- +description: "Lua operators exposed as functions." +--- + +# `operator` + +Lua operators exposed as functions. + +## Usage + +```lua +operator = require "mods.operator" + +print(operator.add(1, 2)) --> 3 +``` + +## Functions + +**Arithmetic**: + +| Function | Description | +| ------------------------ | --------------------------------------------------------- | +| [`add(a, b)`](#fn-add) | Add two numbers. | +| [`div(a, b)`](#fn-div) | Divide `a` by `b` using Lua's floating-point division. | +| [`idiv(a, b)`](#fn-idiv) | Divide `a` by `b` and return the floor-division quotient. | +| [`mod(a, b)`](#fn-mod) | Return the modulo remainder of `a` divided by `b`. | +| [`mul(a, b)`](#fn-mul) | Multiply two numbers. | +| [`pow(a, b)`](#fn-pow) | Raise `a` to the power of `b`. | +| [`sub(a, b)`](#fn-sub) | Subtract `b` from `a`. | +| [`unm(a)`](#fn-unm) | Negate a number. | + +**Comparison**: + +| Function | Description | +| ---------------------- | -------------------------------------------------- | +| [`eq(a, b)`](#fn-eq) | Check whether two values are equal. | +| [`ge(a, b)`](#fn-ge) | Check whether `a` is greater than or equal to `b`. | +| [`gt(a, b)`](#fn-gt) | Check whether `a` is strictly greater than `b`. | +| [`le(a, b)`](#fn-le) | Check whether `a` is less than or equal to `b`. | +| [`lt(a, b)`](#fn-lt) | Check whether `a` is strictly less than `b`. | +| [`neq(a, b)`](#fn-neq) | Check whether two values are not equal. | + +**Logical**: + +| Function | Description | +| ------------------------ | ---------------------------------------------------- | +| [`land(a, b)`](#fn-land) | Evaluate `a and b` with Lua short-circuit semantics. | +| [`lnot(a)`](#fn-lnot) | Return the boolean negation of `a`. | +| [`lor(a, b)`](#fn-lor) | Evaluate `a or b` with Lua short-circuit semantics. | + +**String & Length**: + +| Function | Description | +| ---------------------------- | ---------------------------------------------------------------- | +| [`concat(a, b)`](#fn-concat) | Concatenate two strings. | +| [`len(a)`](#fn-len) | Return the length of a string or table using Lua's `#` operator. | + +**Tables & Calls**: + +| Function | Description | +| ----------------------------------- | -------------------------------------------------------------- | +| [`call(f, ...)`](#fn-call) | Call a function with variadic arguments and return its result. | +| [`index(t, k)`](#fn-index) | Return the value at key/index `k` in table `t`. | +| [`setindex(t, k, v)`](#fn-setindex) | Set `t[k] = v` and return the assigned value. | + +### Arithmetic + + + +#### `add(a, b)` + +Add two numbers. + +**Parameters**: + +- `a` (`number`): Left numeric value. +- `b` (`number`): Right numeric value. + +**Return**: + +- `sum` (`number`): Sum of `a` and `b`. + +**Example**: + +```lua +add(1, 2) --> 3 +``` + + + +#### `div(a, b)` + +Divide `a` by `b` using Lua's floating-point division. + +**Parameters**: + +- `a` (`number`): Dividend value. +- `b` (`number`): Divisor value. + +**Return**: + +- `quotient` (`number`): Quotient `a / b`. + +**Example**: + +```lua +div(10, 4) --> 2.5 +``` + + + +#### `idiv(a, b)` + +Divide `a` by `b` and return the floor-division quotient. + +**Parameters**: + +- `a` (`number`): Dividend value. +- `b` (`number`): Divisor value. + +**Return**: + +- `quotient` (`integer`): Floor-division result. + +**Example**: + +```lua +idiv(5, 2) --> 2 +``` + + + +#### `mod(a, b)` + +Return the modulo remainder of `a` divided by `b`. + +**Parameters**: + +- `a` (`number`): Dividend value. +- `b` (`number`): Divisor value. + +**Return**: + +- `remainder` (`number`): Remainder of `a % b`. + +**Example**: + +```lua +mod(5, 2) --> 1 +``` + + + +#### `mul(a, b)` + +Multiply two numbers. + +**Parameters**: + +- `a` (`number`): Left numeric value. +- `b` (`number`): Right numeric value. + +**Return**: + +- `product` (`number`): Product `a * b`. + +**Example**: + +```lua +mul(3, 4) --> 12 +``` + + + +#### `pow(a, b)` + +Raise `a` to the power of `b`. + +**Parameters**: + +- `a` (`number`): Base value. +- `b` (`number`): Exponent value. + +**Return**: + +- `power` (`number`): Result of `a ^ b`. + +**Example**: + +```lua +pow(2, 4) --> 16 +``` + + + +#### `sub(a, b)` + +Subtract `b` from `a`. + +**Parameters**: + +- `a` (`number`): Left numeric value. +- `b` (`number`): Right numeric value. + +**Return**: + +- `difference` (`number`): Difference `a - b`. + +**Example**: + +```lua +sub(5, 3) --> 2 +``` + + + +#### `unm(a)` + +Negate a number. + +**Parameters**: + +- `a` (`number`): Input numeric value. + +**Return**: + +- `negated` (`number`): Result of `-a`. + +**Example**: + +```lua +unm(3) --> -3 +``` + +### Comparison + + + +#### `eq(a, b)` + +Check whether two values are equal. + +**Parameters**: + +- `a` (`any`): Left value. +- `b` (`any`): Right value. + +**Return**: + +- `isEqual` (`boolean`): True when `a == b`. + +**Example**: + +```lua +eq(1, 1) --> true +``` + + + +#### `ge(a, b)` + +Check whether `a` is greater than or equal to `b`. + +**Parameters**: + +- `a` (`number`): Left numeric value. +- `b` (`number`): Right numeric value. + +**Return**: + +- `isGreaterOrEqual` (`boolean`): True when `a >= b`. + +**Example**: + +```lua +ge(2, 2) --> true +``` + + + +#### `gt(a, b)` + +Check whether `a` is strictly greater than `b`. + +**Parameters**: + +- `a` (`number`): Left numeric value. +- `b` (`number`): Right numeric value. + +**Return**: + +- `isGreater` (`boolean`): True when `a > b`. + +**Example**: + +```lua +gt(3, 2) --> true +``` + + + +#### `le(a, b)` + +Check whether `a` is less than or equal to `b`. + +**Parameters**: + +- `a` (`number`): Left numeric value. +- `b` (`number`): Right numeric value. + +**Return**: + +- `isLessOrEqual` (`boolean`): True when `a <= b`. + +**Example**: + +```lua +le(2, 2) --> true +``` + + + +#### `lt(a, b)` + +Check whether `a` is strictly less than `b`. + +**Parameters**: + +- `a` (`number`): Left numeric value. +- `b` (`number`): Right numeric value. + +**Return**: + +- `isLess` (`boolean`): True when `a < b`. + +**Example**: + +```lua +lt(1, 2) --> true +``` + + + +#### `neq(a, b)` + +Check whether two values are not equal. + +**Parameters**: + +- `a` (`any`): Left value. +- `b` (`any`): Right value. + +**Return**: + +- `isNotEqual` (`boolean`): True when `a ~= b`. + +**Example**: + +```lua +neq(1, 2) --> true +``` + +### Logical + + + +#### `land(a, b)` + +Evaluate `a and b` with Lua short-circuit semantics. + +**Parameters**: + +- `a` (`T1`): First operand. +- `b` (`T2`): Second operand. + +**Return**: + +- `andValue` (`T1|T2`): Result of `a and b`. + +**Example**: + +```lua +land(true, false) --> false +``` + + + +#### `lnot(a)` + +Return the boolean negation of `a`. + +**Parameters**: + +- `a` (`any`): Input value. + +**Return**: + +- `isNot` (`boolean`): Result of `not a`. + +**Example**: + +```lua +lnot(true) --> false +``` + + + +#### `lor(a, b)` + +Evaluate `a or b` with Lua short-circuit semantics. + +**Parameters**: + +- `a` (`T1`): First operand. +- `b` (`T2`): Second operand. + +**Return**: + +- `orValue` (`T1|T2`): Result of `a or b`. + +**Example**: + +```lua +lor(false, true) --> true +``` + +### String & Length + + + +#### `concat(a, b)` + +Concatenate two strings. + +**Parameters**: + +- `a` (`string`): Left string. +- `b` (`string`): Right string. + +**Return**: + +- `concatenated` (`string`): Concatenated result `a .. b`. + +**Example**: + +```lua +concat("a", "b") --> "ab" +``` + + + +#### `len(a)` + +Return the length of a string or table using Lua's `#` operator. + +**Parameters**: + +- `a` (`string|table`): Value supporting Lua's `#` operator. + +**Return**: + +- `length` (`integer`): Length computed by `#a`. + +**Example**: + +```lua +len("abc") --> 3 +``` + +### Tables & Calls + + + +#### `call(f, ...)` + +Call a function with variadic arguments and return its result. + +**Parameters**: + +- `f` (`fun(...:T1):T2`): Function to call. +- `...` (`T1`): Additional arguments. + +**Return**: + +- `callResult` (`T2`): Return value(s) from `f(...)`. + +**Example**: + +```lua +call(math.max, 1, 2) --> 2 +``` + + + +#### `index(t, k)` + +Return the value at key/index `k` in table `t`. + +**Parameters**: + +- `t` (`table`): Source table. +- `k` (`T`): Key/index value. + +**Return**: + +- `indexedValue` (`T`): Value stored at `t[k]`. + +**Example**: + +```lua +index({ a = 1 }, "a") --> 1 +``` + + + +#### `setindex(t, k, v)` + +Set `t[k] = v` and return the assigned value. + +**Parameters**: + +- `t` (`table`): Target table. +- `k` (`any`): Key/index value. +- `v` (`T`): Value to set. + +**Return**: + +- `assignedValue` (`T`): Assigned value `v`. + +**Example**: + +```lua +setindex({}, "a", 1) --> 1 +``` diff --git a/docs/api/path.md b/docs/api/path.md new file mode 100644 index 0000000..1e52564 --- /dev/null +++ b/docs/api/path.md @@ -0,0 +1,847 @@ +--- +description: "Cross-platform path operations with host-platform semantics." +--- + +# `path` + +Cross-platform path operations with host-platform semantics. + +## Usage + +```lua +path = require "mods.path" + +print(path.join("src", "mods", "path.lua")) --> "src/mods/path.lua" +print(path.normpath("a//b/./c")) --> "a/b/c" +print(path.splitext("archive.tar.gz")) --> "archive.tar", ".gz" +``` + +## Functions + +| Function | Description | +| ------------------------------------------------------- | ---------------------------- | +| [`_splitext(path, sep, altsep?, extsep)`](#fn-splitext) | Split extension from a path. | + +**Anchors**: + +| Function | Description | +| ---------------------------- | ------------------------------------------- | +| [`anchor(path)`](#fn-anchor) | Return drive and root combined. | +| [`drive(path)`](#fn-drive) | Return drive prefix when present. | +| [`root(path)`](#fn-root) | Return root separator segment when present. | + +**Components**: + +| Function | Description | +| -------------------------------- | ------------------------------------------------------------- | +| [`parents(path)`](#fn-parents) | Return logical parent paths from nearest to farthest. | +| [`parts(path)`](#fn-parts) | Split path into logical parts, including anchor when present. | +| [`stem(path)`](#fn-stem) | Return filename without its final suffix. | +| [`suffixes(path)`](#fn-suffixes) | Return all filename suffixes in order. | + +**Conversions**: + +| Function | Description | +| -------------------------------- | --------------------------------------------------- | +| [`as_posix(path)`](#fn-as-posix) | Convert backslashes (`\`) to forward slashes (`/`). | +| [`as_uri(path)`](#fn-as-uri) | Convert a local path to a `file://` URI. | +| [`from_uri(uri)`](#fn-from-uri) | Convert a `file://` URI to a local absolute path. | + +**Decomposition**: + +| Function | Description | +| ------------------------------------ | -------------------------------------------------- | +| [`basename(path)`](#fn-basename) | Return final path component. | +| [`dirname(path)`](#fn-dirname) | Return directory portion of a path. | +| [`split(path)`](#fn-split) | Split path into directory head and tail component. | +| [`splitdrive(path)`](#fn-splitdrive) | Split drive prefix from remainder. | +| [`splitext(path)`](#fn-splitext) | Split path into a root and extension. | +| [`splitroot(path)`](#fn-splitroot) | Split path into drive, root, and tail components. | + +**Derived**: + +| Function | Description | +| ----------------------------------------- | ------------------------------------------------ | +| [`abspath(path)`](#fn-abspath) | Return normalized absolute path. | +| [`commonpath(paths)`](#fn-commonpath) | Return longest common sub-path from a path list. | +| [`commonprefix(paths)`](#fn-commonprefix) | Return longest common leading string prefix. | +| [`relpath(path, start?)`](#fn-relpath) | Return `path` relative to optional `start` path. | + +**Environment**: + +| Function | Description | +| ------------------------------------ | ----------------------------------------------------------------------- | +| `cwd` | Return the current working directory path. | +| [`expanduser(path)`](#fn-expanduser) | Expand `~` home segment when available. | +| [`expandvars(path)`](#fn-expandvars) | Expand vars in a path (`$VAR`/`${VAR}` everywhere, `%VAR%` on Windows). | +| [`home()`](#fn-home) | Return the current user's home directory path. | + +**Normalization**: + +| Function | Description | +| -------------------------------- | ---------------------------------------------------- | +| [`isabs(path)`](#fn-isabs) | Return `true` when `path` is absolute. | +| [`join(path, ...)`](#fn-join) | Join path components. | +| [`normcase(s)`](#fn-normcase) | Normalize path case using the active path semantics. | +| [`normpath(path)`](#fn-normpath) | Normalize separators and dot segments. | + +**Relations**: + +| Function | Description | +| ------------------------------------------------------- | --------------------------------------------------------------------------------------- | +| [`is_relative_to(path, other)`](#fn-is-relative-to) | Return `true` when `path` is under `other`. | +| [`relative_to(path, other, walk_up?)`](#fn-relative-to) | Return `path` relative to `other`, or `nil` with an error when it is not under `other`. | +| [`with_name(path, name)`](#fn-with-name) | Return a path with the final filename replaced. | +| [`with_stem(path, stem)`](#fn-with-stem) | Return a path with the final filename stem replaced. | +| [`with_suffix(path, suffix)`](#fn-with-suffix) | Return a path with the final filename suffix replaced. | + + + +### `_splitext(path, sep, altsep?, extsep)` + +Split extension from a path. **Parameters**: + +- `path` (`string`) +- `sep` (`string`) +- `altsep?` (`string`) +- `extsep` (`string`) + +**Return**: + +- `root` (`string`) +- `ext` (`string`) + +### Anchors + + + +#### `anchor(path)` + +Return drive and root combined. + +**Parameters**: + +- `path` (`string`): Input path. + +**Return**: + +- `anchor` (`string`): Drive and root anchor. + +**Example**: + +```lua +path.anchor("c:\\") --> "c:\\" +``` + + + +#### `drive(path)` + +Return drive prefix when present. + +**Parameters**: + +- `path` (`string`): Input path. + +**Return**: + +- `drivePrefix` (`string`): Drive prefix. + +**Example**: + +```lua +path.drive("c:a/b") --> "c:" +path.drive("a/b") --> "" +``` + + + +#### `root(path)` + +Return root separator segment when present. + +**Parameters**: + +- `path` (`string`): Input path. + +**Return**: + +- `rootSeparator` (`string`): Root separator segment. + +**Example**: + +```lua +path.root("/tmp/a.txt") --> "/" +path.root("c:/") --> "\\" +path.root("a/b") --> "" +``` + +### Components + + + +#### `parents(path)` + +Return logical parent paths from nearest to farthest. + +**Parameters**: + +- `path` (`string`): Input path. + +**Return**: + +- `parents` (`mods.List`): Ancestor paths from nearest to farthest. + +**Example**: + +```lua +path.parents("a/b/c") --> {"a/b", "a", "."} +path.parents("c:a/b") --> {"c:a", "c:"} +``` + + + +#### `parts(path)` + +Split path into logical parts, including anchor when present. + +**Parameters**: + +- `path` (`string`): Input path. + +**Return**: + +- `paths` (`mods.List`): Path parts including anchor when present. + +**Example**: + +```lua +path.parts("a/b.txt") --> {"a", "b.txt"} +path.parts("/a/b") --> {"/", "a", "b"} +path.parts("c:a\\b") --> {"c:", "a", "b"} +``` + + + +#### `stem(path)` + +Return filename without its final suffix. + +**Parameters**: + +- `path` (`string`): Input path. + +**Return**: + +- `stem` (`string`): Filename stem. + +**Example**: + +```lua +path.stem("archive.tar.gz") --> "archive.tar" +path.stem("c:a/b") --> "b" +``` + + + +#### `suffixes(path)` + +Return all filename suffixes in order. + +**Parameters**: + +- `path` (`string`): Input path. + +**Return**: + +- `suffixes` (`mods.List`): Filename suffixes. + +**Example**: + +```lua +path.suffixes("archive.tar.gz") --> {".tar", ".gz"} +path.suffixes("a/b") --> {} +``` + +### Conversions + + + +#### `as_posix(path)` + +Convert backslashes (`\`) to forward slashes (`/`). + +**Parameters**: + +- `path` (`string`): Input path. + +**Return**: + +- `posixPath` (`string`): POSIX-style path. + +**Example**: + +```lua +path.as_posix("a\\b\\c") --> "a/b/c" +``` + + + +#### `as_uri(path)` + +Convert a local path to a `file://` URI. + +**Parameters**: + +- `path` (`string`): Input path. + +**Return**: + +- `fileUri` (`string?`): File URI. +- `err` (`string?`): Error message when conversion fails. + +**Example**: + +```lua +path.as_uri("/home/user/report.txt") --> "file:///home/user/report.txt" +path.as_uri("c:/a/b.c") --> "file:///c:/a/b.c" +path.as_uri("/a/b%#c") --> "file:///a/b%25%23c" +``` + + + +#### `from_uri(uri)` + +Convert a `file://` URI to a local absolute path. + +**Parameters**: + +- `uri` (`string`): URI value. + +**Return**: + +- `path` (`string?`): Resolved absolute path. +- `err` (`string?`): Error message when conversion fails. + +**Example**: + +```lua +path.from_uri("file://localhost/tmp/a.txt") --> "/tmp/a.txt" +``` + +### Decomposition + + + +#### `basename(path)` + +Return final path component. + +**Parameters**: + +- `path` (`string`): Path to inspect. + +**Return**: + +- `basename` (`string`): Final path component. + +**Example**: + +```lua +path.basename("/a/b.txt") --> "b.txt" +path.basename([[C:\a\b.txt]]) --> "b.txt" +``` + + + +#### `dirname(path)` + +Return directory portion of a path. + +**Parameters**: + +- `path` (`string`): Path to inspect. + +**Return**: + +- `dirname` (`string`): Parent directory path. + +**Example**: + +```lua +path.dirname("/a/b.txt") --> "/a" +path.dirname([[C:\a\b.txt]]) --> [[C:\a]] +``` + + + +#### `split(path)` + +Split path into directory head and tail component. + +**Parameters**: + +- `path` (`string`): Input path. + +**Return**: + +- `head` (`string`): Directory portion. +- `tail` (`string`): Final path component. + +**Example**: + +```lua +path.split("/a/b.txt") --> "/a", "b.txt" +``` + + + +#### `splitdrive(path)` + +Split drive prefix from remainder. + +**Parameters**: + +- `path` (`string`): Input path. + +**Return**: + +- `drive` (`string`): Drive or share prefix when present. +- `rest` (`string`): Path remainder. + +**Example**: + +```lua +path.splitdrive("/a/b") --> "", "/a/b" +``` + +> [!NOTE] +> +> On POSIX semantics the drive portion is always empty. + + + +#### `splitext(path)` + +Split path into a root and extension. + +**Parameters**: + +- `path` (`string`): Input path. + +**Return**: + +- `root` (`string`): Path without the final extension. +- `ext` (`string`): Final extension including leading dot. + +**Example**: + +```lua +path.splitext("archive.tar.gz") --> "archive.tar", ".gz" +``` + + + +#### `splitroot(path)` + +Split path into drive, root, and tail components. + +**Parameters**: + +- `path` (`string`): Path to split. + +**Return**: + +- `drive` (`string`): Drive or share prefix (empty on POSIX). +- `root` (`string`): Root separator segment. +- `tail` (`string`): Remaining path without leading root separator. + +**Example**: + +```lua +path.splitroot("/a/b") --> "", "/", "a/b" +path.splitroot([[C:\a\b]]) --> "C:", [[\]], "a\\b" +``` + +### Derived + + + +#### `abspath(path)` + +Return normalized absolute path. + +**Parameters**: + +- `path` (`string`): Path to absolutize. + +**Return**: + +- `absolutePath` (`string`): Absolute normalized path. + +**Example**: + +```lua +path.abspath("/a/./b") --> "/a/b" +path.abspath([[C:\a\..\b]]) --> [[C:\b]] +``` + + + +#### `commonpath(paths)` + +Return longest common sub-path from a path list. + +**Parameters**: + +- `paths` (`string[]`): List of paths. + +**Return**: + +- `commonPath` (`string?`): Longest common sub-path. +- `err` (`string?`): Error message when inputs are incompatible. + +**Example**: + +```lua +path.commonpath({ "/a/b/c", "/a/b/d" }) --> "/a/b" +path.commonpath({ [[C:\a\b\c]], [[c:/a/b/d]] }) --> [[C:\a\b]] +``` + + + +#### `commonprefix(paths)` + +Return longest common leading string prefix. + +**Parameters**: + +- `paths` (`string[]`): List of paths. + +**Return**: + +- `commonPrefix` (`string`): Longest common string prefix. + +**Example**: + +```lua +path.commonprefix({"abc", "abd"}) --> "ab" +path.commonprefix({"/home/swen/spam", "/home/swen/eggs"}) --> "/home/swen/" +path.commonprefix({"abc", "xyz"}) --> "" +``` + + + +#### `relpath(path, start?)` + +Return `path` relative to optional `start` path. + +**Parameters**: + +- `path` (`string`): Input path. +- `start?` (`string`): Optional base path. + +**Return**: + +- `relativePath` (`string?`): Relative path from `start` to `path`. +- `err` (`string?`): Error message when the path cannot be made relative. + +**Example**: + +```lua +path.relpath("/a/b/c", "/a") --> "b/c" +path.relpath([[C:\a\b\c]], [[C:\a]]) --> [[b\c]] +``` + +### Environment + + + +#### `cwd` + +Return the current working directory path. + +#### `expanduser(path)` + +Expand `~` home segment when available. + +**Parameters**: + +- `path` (`string`): Path that may begin with `~`. + +**Return**: + +- `expandedPath` (`string?`): Path with the home segment expanded when + available. +- `err` (`string?`): Error message when `~` expansion cannot be resolved. + +**Example**: + +```lua +path.expanduser("~/tmp") --> "/tmp" (when HOME is set) +path.expanduser([[x\y]]) --> [[x\y]] +``` + + + +#### `expandvars(path)` + +Expand vars in a path (`$VAR`/`${VAR}` everywhere, `%VAR%` on Windows). + +**Parameters**: + +- `path` (`string`): Path containing variable placeholders. + +**Return**: + +- `expandedPath` (`string`): Path with variable values substituted. + +**Example**: + +```lua +path.expandvars("$HOME/bin") --> "/home/me/bin" +path.expandvars("${XDG_CONFIG_HOME}/nvim") --> "/home/me/.config/nvim" +path.expandvars("%USERPROFILE%\\bin") --> "C:\\Users\\me\\bin" +path.expandvars("$UNKNOWN/bin") --> "$UNKNOWN/bin" +``` + + + +#### `home()` + +Return the current user's home directory path. + +**Return**: + +- `homePath` (`string?`): Home directory path when available. +- `err` (`string?`): Error message when the home directory cannot be resolved. + +**Example**: + +```lua +path.home() +``` + +### Normalization + + + +#### `isabs(path)` + +Return `true` when `path` is absolute. + +**Parameters**: + +- `path` (`string`): Input path. + +**Return**: + +- `isAbsolute` (`boolean`): True when `path` is absolute. + +**Example**: + +```lua +path.isabs("/a/b") --> true +``` + + + +#### `join(path, ...)` + +Join path components. + +**Parameters**: + +- `path` (`string`): Base path component. +- `...` (`string`): Additional path components. + +**Return**: + +- `joinedPath` (`string`): Joined path. + +**Example**: + +```lua +path.join("/usr", "bin") --> "/usr/bin" +path.join([[C:/a]], [[b]]) --> [[C:/a\b]] +``` + +> [!NOTE] +> +> Single input is returned as-is. + + + +#### `normcase(s)` + +Normalize path case using the active path semantics. + +**Parameters**: + +- `s` (`string`): Input path value. + +**Return**: + +- `normalizedPath` (`string`): Path after case normalization. + +**Example**: + +```lua +path.normcase("ABC") --> "abc" +path.normcase("/A/B") --> "\\a\\b" +``` + +> [!NOTE] +> +> On POSIX semantics this returns the input unchanged. Use `mods.ntpath` to +> force Windows-style case folding and separator normalization. + + + +#### `normpath(path)` + +Normalize separators and dot segments. + +**Parameters**: + +- `path` (`string`): Path to normalize. + +**Return**: + +- `normalizedPath` (`string`): Normalized path. + +**Example**: + +```lua +path.normpath("/a//./b/..") --> "/a" +path.normpath([[A/foo/../B]]) --> [[A\B]] +``` + +### Relations + + + +#### `is_relative_to(path, other)` + +Return `true` when `path` is under `other`. + +**Parameters**: + +- `path` (`string`): Input path. +- `other` (`string`): Reference path. + +**Return**: + +- `isRelative` (`boolean`): True when `path` is under `other`. + +**Example**: + +```lua +path.is_relative_to("a/b/c", "a/b") --> true +path.is_relative_to("C:A/B", "c:a") --> true +path.is_relative_to("a/b", "a/b/c") --> false +``` + + + +#### `relative_to(path, other, walk_up?)` + +Return `path` relative to `other`, or `nil` with an error when it is not under +`other`. + +When `walk_up` is `true`, allow `..` segments to walk up to a shared prefix. + +**Parameters**: + +- `path` (`string`): Input path. +- `other` (`string`): Reference path. +- `walk_up?` (`boolean`): Allow walking up to a shared prefix. + +**Return**: + +- `relativePath` (`string?`): Path relative to `other`, or `nil` on error. +- `err` (`string?`): Error message when the path cannot be made relative. + +**Example**: + +```lua +path.relative_to("/a/b/c.txt", "/a") --> "b/c.txt" +path.relative_to("/a/b", "/a/c", true) --> "../b" +path.relative_to("/a/b", "/a/x") --> nil, "'/a/b' is not in the subpath of '/a/x'" +``` + + + +#### `with_name(path, name)` + +Return a path with the final filename replaced. + +**Parameters**: + +- `path` (`string`): Input path. +- `name` (`string`): Replacement filename. + +**Return**: + +- `updatedPath` (`string?`): Path with replaced filename, or `nil` on error. +- `err` (`string?`): Error message when replacement fails. + +**Example**: + +```lua +path.with_name("a/b", "c.txt") --> "a/c.txt" +path.with_name("a/b.txt", "c.lua") --> "a/c.lua" +path.with_name("a/b", "c/d") --> nil, "invalid name 'c/d'" +path.with_name("/", "d.xml") --> nil, "'/' has an empty name" +``` + + + +#### `with_stem(path, stem)` + +Return a path with the final filename stem replaced. + +**Parameters**: + +- `path` (`string`): Input path. +- `stem` (`string`): Replacement filename stem. + +**Return**: + +- `updatedPath` (`string?`): Path with replaced filename stem, or `nil` on + error. +- `err` (`string?`): Error message when replacement fails. + +**Example**: + +```lua +path.with_stem("a/b", "d") --> "/a/d" +path.with_stem("a/b.lua", "d") --> "/a/d.lua" +path.with_stem("/", "d") --> "'/' has an empty name" +path.with_stem("a/b", "d") --> "invalid name ''." +``` + + + +#### `with_suffix(path, suffix)` + +Return a path with the final filename suffix replaced. + +**Parameters**: + +- `path` (`string`): Input path. +- `suffix` (`string`): Replacement suffix. + +**Return**: + +- `updatedPath` (`string?`): Path with replaced suffix, or `nil` on error. +- `err` (`string?`): Error message when replacement fails. + +**Example**: + +```lua +path.with_suffix("a/b", ".gz") --> "a/b/.gz" +path.with_suffix("a/b.gz", ".lua") --> "a/b/.lua" +path.with_suffix("a/b", "gz") --> nil, "invalid suffix 'gz'" +path.with_suffix("//a/b", "gz") --> nil, "'//a/b' has an empty name" +``` diff --git a/docs/api/posixpath.md b/docs/api/posixpath.md new file mode 100644 index 0000000..31d90cc --- /dev/null +++ b/docs/api/posixpath.md @@ -0,0 +1,22 @@ +--- +description: "POSIX-style path operations." +--- + +# `posixpath` + +POSIX-style path operations. + +> 💡 Python `posixpath`-style behavior, ported to Lua. + +## Usage + +```lua +posixpath = require "mods.posixpath" + +print(posixpath.join("/usr", "bin")) --> "/usr/bin" +print(posixpath.normpath("/a//./b/..")) --> "/a" +print(posixpath.splitext("archive.tar.gz")) --> "archive.tar", ".gz" +print(posixpath.relpath("/usr/local/bin", "/usr")) --> "local/bin" +``` + +> ✨ Same API as `mods.path`, but with POSIX path semantics. diff --git a/docs/api/runtime.md b/docs/api/runtime.md new file mode 100644 index 0000000..805d576 --- /dev/null +++ b/docs/api/runtime.md @@ -0,0 +1,131 @@ +--- +description: "Lua runtime metadata and version compatibility flags." +--- + +# `runtime` + +Lua runtime metadata and version compatibility flags. + +## Usage + +```lua +runtime = require "mods.runtime" + +print(runtime.version) --> 501 | 502 | 503 | 504 | 505 +print(runtime.is_lua55) --> true | false +``` + +## Fields + +| Field | Description | +| --------------------------- | ------------------------------------------------- | +| [`is_lua51`](#is-lua51) | True only on Lua 5.1 runtimes. | +| [`is_lua52`](#is-lua52) | True only on Lua 5.2 runtimes. | +| [`is_lua53`](#is-lua53) | True only on Lua 5.3 runtimes. | +| [`is_lua54`](#is-lua54) | True only on Lua 5.4 runtimes. | +| [`is_lua55`](#is-lua55) | True only on Lua 5.5 runtimes. | +| [`is_luajit`](#is-luajit) | True when running under LuaJIT. | +| [`is_windows`](#is-windows) | True when running on a Windows host. | +| [`major`](#major) | Major version number parsed from `version`. | +| [`minor`](#minor) | Minor version number parsed from `version`. | +| [`version`](#version) | Numeric version encoded as `major * 100 + minor`. | + + + +### `is_lua51` (`boolean`) + +True only on Lua 5.1 runtimes. + +```lua +print(runtime.is_lua51) --> true | false +``` + + + +### `is_lua52` (`boolean`) + +True only on Lua 5.2 runtimes. + +```lua +print(runtime.is_lua52) --> true | false +``` + + + +### `is_lua53` (`boolean`) + +True only on Lua 5.3 runtimes. + +```lua +print(runtime.is_lua53) --> true | false +``` + + + +### `is_lua54` (`boolean`) + +True only on Lua 5.4 runtimes. + +```lua +print(runtime.is_lua54) --> true | false +``` + + + +### `is_lua55` (`boolean`) + +True only on Lua 5.5 runtimes. + +```lua +print(runtime.is_lua55) --> true | false +``` + + + +### `is_luajit` (`boolean`) + +True when running under LuaJIT. + +```lua +print(runtime.is_luajit) --> true | false +``` + + + +### `is_windows` (`boolean`) + +True when running on a Windows host. + +```lua +print(runtime.is_windows) --> true | false +``` + + + +### `major` (`5`) + +Major version number parsed from `version`. + +```lua +print(runtime.major) --> 5 +``` + + + +### `minor` (`1|2|3|4|5`) + +Minor version number parsed from `version`. + +```lua +print(runtime.minor) --> 1 | 2 | 3 | 4 | 5 +``` + + + +### `version` (`501|502|503|504|505`) + +Numeric version encoded as `major * 100 + minor`. + +```lua +print(runtime.version) --> 501 | 502 | 503 | 504 | 505 +``` diff --git a/docs/api/set.md b/docs/api/set.md new file mode 100644 index 0000000..84124f5 --- /dev/null +++ b/docs/api/set.md @@ -0,0 +1,843 @@ +--- +description: "A set class for creating, combining, and querying unique values." +--- + +# `set` + +A set class for creating, combining, and querying unique values. + +## Usage + +```lua +Set = require "mods.Set" + +s = Set({ "a" }) +print(s:contains("a")) --> true +``` + +## Functions + +**Mutation**: + +| Function | Description | +| --------------------------------------------------------------------- | ----------------------------------------------------------- | +| [`add(v)`](#fn-add) | Add an element to the set. | +| [`clear()`](#fn-clear) | Remove all elements from the set. | +| [`difference_update(set)`](#fn-difference-update) | Remove elements found in another set (in place). | +| [`intersection_update(set)`](#fn-intersection-update) | Keep only elements common to both sets (in place). | +| [`pop()`](#fn-pop) | Remove and return an arbitrary element. | +| [`symmetric_difference_update(set)`](#fn-symmetric-difference-update) | Update the set with elements not shared by both (in place). | +| [`update(set)`](#fn-update) | Add all elements from another set (in place). | + +**Predicates**: + +| Function | Description | +| ----------------------------------- | ---------------------------------------------------------------- | +| [`equals(t)`](#fn-equals) | Return true when both sets contain exactly the same members. | +| [`isdisjoint(set)`](#fn-isdisjoint) | Return true if sets have no elements in common. | +| [`isempty()`](#fn-isempty) | Return true if the set has no elements. | +| [`issubset(t)`](#fn-issubset) | Return true if all elements of this set are also in another set. | +| [`issuperset(t)`](#fn-issuperset) | Return true if this set contains all elements of another set. | + +**Queries**: + +| Function | Description | +| ----------------------------- | ----------------------------------------- | +| [`contains(v)`](#fn-contains) | Return true if the set contains `v`. | +| [`len()`](#fn-len) | Return the number of elements in the set. | + +**Set Operations**: + +| Function | Description | +| ----------------------------------------------------- | ---------------------------------------------------------------------------- | +| [`copy()`](#fn-copy) | Return a shallow copy of the set. | +| [`difference(t)`](#fn-difference) | Return elements in this set but not in another. | +| [`has(v)`](#fn-has) | Check whether a value is present in the set without following the metatable. | +| [`intersection(t)`](#fn-intersection) | Return elements common to both sets. | +| [`remove(v)`](#fn-remove) | Remove an element if present, do nothing otherwise. | +| [`symmetric_difference(t)`](#fn-symmetric-difference) | Return elements not shared by both sets. | +| [`union(t)`](#fn-union) | Return a new set with all elements from both. | + +**Transforms**: + +| Function | Description | +| --------------------------------- | ------------------------------------------------------- | +| [`join(sep?, quoted?)`](#fn-join) | Join set values into a string. | +| [`map(fn)`](#fn-map) | Return a new set by mapping each value. | +| [`mirror()`](#fn-mirror) | Mirror values into a new table as both keys and values. | +| [`tostring()`](#fn-tostring) | Render the set as a string. | +| [`values()`](#fn-values) | Return a list of all values in the set. | + +**Metamethods**: + +| Function | Description | +| ------------------------------ | -------------------------------------------------------------------------- | +| [`__add(t)`](#fn-add) | Return the union of two sets using `+`. | +| [`__band(t)`](#fn-band) | Return the intersection of two sets using `&`. | +| [`__bor(t)`](#fn-bor) | Return the union of two sets using `\|`. | +| [`__bxor(t)`](#fn-bxor) | Return elements present in exactly one set using `^`. | +| [`__eq(t)`](#fn-eq) | Return true if both sets contain exactly the same members using `==`. | +| [`__le(t)`](#fn-le) | Return true if the left set is a subset of the right set using `<=`. | +| [`__lt(set)`](#fn-lt) | Return true if the left set is a proper subset of the right set using `<`. | +| [`__sub(set)`](#fn-sub) | Return the difference of two sets using `-`. | +| [`__tostring()`](#fn-tostring) | Render the set via `tostring(set)`. | + +### Mutation + + + +#### `add(v)` + +Add an element to the set. + +**Parameters**: + +- `v` (`any`): Value to add. + +**Return**: + +- `self` (`T`): Current set. + +**Example**: + +```lua +s = Set({ "a" }):add("b") --> s contains "a", "b" +``` + + + +#### `clear()` + +Remove all elements from the set. + +**Return**: + +- `self` (`T`): Current set. + +**Example**: + +```lua +s = Set({ "a", "b" }):clear() --> s is empty +``` + + + +#### `difference_update(set)` + +Remove elements found in another set (in place). + +**Parameters**: + +- `set` (`T|mods.List`): Other set/list. + +**Return**: + +- `self` (`T`): Current set. + +**Example**: + +```lua +s = Set({ "a", "b" }):difference_update(Set({ "b" })) --> s contains "a" +``` + + + +#### `intersection_update(set)` + +Keep only elements common to both sets (in place). + +**Parameters**: + +- `set` (`T|mods.List`): Other set/list. + +**Return**: + +- `self` (`T`): Current set. + +**Example**: + +```lua +s = Set({ "a", "b" }):intersection_update(Set({ "b", "c" })) +--> s contains "b" +``` + + + +#### `pop()` + +Remove and return an arbitrary element. + +**Return**: + +- `removedValue` (`any`): Removed value, or `nil` when the set is empty. + +**Example**: + +```lua +v = Set({ "a", "b" }):pop() --> v is either "a" or "b" +``` + + + +#### `symmetric_difference_update(set)` + +Update the set with elements not shared by both (in place). + +**Parameters**: + +- `set` (`T|mods.List`): Other set/list. + +**Return**: + +- `self` (`T`): Current set. + +**Example**: + +```lua +s = Set({ "a", "b" }):symmetric_difference_update(Set({ "b", "c" })) +--> s contains "a", "c" +``` + + + +#### `update(set)` + +Add all elements from another set (in place). + +**Parameters**: + +- `set` (`T|mods.List`): Other set/list. + +**Return**: + +- `self` (`T`): Current set. + +**Example**: + +```lua +s = Set({ "a" }):update(Set({ "b" })) --> s contains "a", "b" +``` + +### Predicates + + + +#### `equals(t)` + +Return true when both sets contain exactly the same members. + +**Parameters**: + +- `t` (`mods.Set|mods.List|table`): Other set/list. + +**Return**: + +- `isEqual` (`boolean`): True when both sets contain the same members. + +**Example**: + +```lua +a = Set({ "a", "b" }) +b = Set({ "b", "a" }) +ok = a:equals(b) --> true +``` + +> [!NOTE] +> +> `equals` is also available as the `__eq` (`==`) operator. `a:equals(b)` is +> equivalent to `a == b`. + + + +#### `isdisjoint(set)` + +Return true if sets have no elements in common. + +**Parameters**: + +- `set` (`T|mods.List`): Other set/list. + +**Return**: + +- `isDisjoint` (`boolean`): True when sets have no elements in common. + +**Example**: + +```lua +ok = Set({ "a" }):isdisjoint(Set({ "b" })) --> true +``` + + + +#### `isempty()` + +Return true if the set has no elements. + +**Return**: + +- `isEmpty` (`boolean`): True when the set has no elements. + +**Example**: + +```lua +empty = Set({}):isempty() --> true +``` + + + +#### `issubset(t)` + +Return true if all elements of this set are also in another set. + +**Parameters**: + +- `t` (`mods.Set|mods.List|table`): Other set/list. + +**Return**: + +- `isSubset` (`boolean`): True when every element of `self` exists in `set`. + +**Example**: + +```lua +ok = Set({ "a" }):issubset(Set({ "a", "b" })) --> true +``` + +> [!NOTE] +> +> `issubset` is also available as the `__le` (`<=`) operator. `a:issubset(b)` is +> equivalent to `a <= b`. + + + +#### `issuperset(t)` + +Return true if this set contains all elements of another set. + +**Parameters**: + +- `t` (`mods.Set|mods.List|table`): Other set/list. + +**Return**: + +- `isSuperset` (`boolean`): True when `self` contains every element of `set`. + +**Example**: + +```lua +ok = Set({ "a", "b" }):issuperset(Set({ "a" })) --> true +``` + +### Queries + + + +#### `contains(v)` + +Return true if the set contains `v`. + +**Parameters**: + +- `v` (`any`): Value to check. + +**Return**: + +- `isPresent` (`boolean`): True when `v` is present in the set. + +**Example**: + +```lua +ok = Set({ "a", "b" }):contains("a") --> true +ok = Set({ "a", "b" }):contains("z") --> false +``` + + + +#### `len()` + +Return the number of elements in the set. + +**Return**: + +- `count` (`integer`): Element count. + +**Example**: + +```lua +n = Set({ "a", "b" }):len() --> 2 +``` + +### Set Operations + + + +#### `copy()` + +Return a shallow copy of the set. + +**Return**: + +- `set` (`mods.Set`): New set. + +**Example**: + +```lua +c = Set({ "a" }):copy() --> c is a new set with "a" +``` + + + +#### `difference(t)` + +Return elements in this set but not in another. + +**Parameters**: + +- `t` (`mods.Set|mods.List|table`): Other set/list. + +**Return**: + +- `set` (`mods.Set`): New set. + +**Example**: + +```lua +d = Set({ "a", "b" }):difference(Set({ "b" })) --> d contains "a" +``` + +> [!NOTE] +> +> `difference` is also available as the `__sub` (`-`) operator. +> `a:difference(b)` is equivalent to `a - b`. + + + +#### `has(v)` + +Check whether a value is present in the set without following the metatable. + +**Parameters**: + +- `v` (`any`): Value to look up. + +**Return**: + +- `present` (`boolean`): Whether the value is present. + +**Example**: + +```lua +s = Set({ "a", "b", "c" }) +print(s:has("a")) --> true +print(s:has("__index")) --> false +``` + + + +#### `intersection(t)` + +Return elements common to both sets. + +**Parameters**: + +- `t` (`mods.Set|mods.List|table`): Other set/list. + +**Return**: + +- `set` (`mods.Set`): New set. + +**Example**: + +```lua +i = Set({ "a", "b" }):intersection(Set({ "b", "c" })) --> i contains "b" +``` + +> [!NOTE] +> +> `intersection` is also available as `__band` (`&`) on Lua 5.3+. + + + +#### `remove(v)` + +Remove an element if present, do nothing otherwise. + +**Parameters**: + +- `v` (`any`): Value to remove. + +**Return**: + +- `self` (`T`): Current set. + +**Example**: + +```lua +s = Set({ "a", "b" }):remove("b") --> s contains "a" +``` + + + +#### `symmetric_difference(t)` + +Return elements not shared by both sets. + +**Parameters**: + +- `t` (`mods.Set|mods.List|table`): Other set/list. + +**Return**: + +- `set` (`mods.Set`): New set. + +**Example**: + +```lua +d = Set({ "a", "b" }):symmetric_difference(Set({ "b", "c" })) +--> d contains "a", "c" +``` + +> [!NOTE] +> +> `symmetric_difference` is also available as `__bxor` (`^`) on Lua 5.3+. + + + +#### `union(t)` + +Return a new set with all elements from both. + +**Parameters**: + +- `t` (`mods.Set|mods.List|table`): Other set/list. + +**Return**: + +- `set` (`mods.Set`): New set. + +**Example**: + +```lua +s = Set({ "a" }):union(Set({ "b" })) --> s contains "a", "b" +``` + +> [!NOTE] +> +> `union` is available as `__add` (`+`) and `__bor` (`|`) on Lua 5.3+. +> `a:union(b)` is equivalent to `a + b` and `a | b`. + +### Transforms + + + +#### `join(sep?, quoted?)` + +Join set values into a string. + +**Parameters**: + +- `sep?` (`string`): Optional separator value (defaults to `""`). +- `quoted?` (`boolean`): Optional boolean flag (defaults to `false`). + +**Return**: + +- `joined` (`string`): Joined string. + +**Example**: + +```lua +s = Set({ "b", "a" }):join(", ") --> "a, b" +s = Set({ "b", "a" }):join(", ", true) --> '"a", "b"' +``` + +> [!NOTE] +> +> Join order is not guaranteed. + + + +#### `map(fn)` + +Return a new set by mapping each value. + +**Parameters**: + +- `fn` (`fun(v:any):any`): Mapping function. + +**Return**: + +- `set` (`mods.Set`): New set. + +**Example**: + +```lua +s = Set({ 1, 2 }):map(function(v) return v * 10 end) --> s contains 10, 20 +``` + + + +#### `mirror()` + +Mirror values into a new table as both keys and values. + +**Return**: + +- `mirroredValues` (`table`): Table mapping each value to itself. + +**Example**: + +```lua +mirrored = Set({ "a", "b" }):mirror() --> { a = "a", b = "b" } +``` + + + +#### `tostring()` + +Render the set as a string. + +**Return**: + +- `renderedSet` (`string`): Rendered set string. + +**Example**: + +```lua +s = Set({ "b", "a", 1 }):tostring() --> '{ 1, "a", "b" }' +``` + + + +#### `values()` + +Return a list of all values in the set. + +**Return**: + +- `values` (`mods.List`): List of set values. + +**Example**: + +```lua +values = Set({ "a", "b" }):values() --> { "a", "b" } +``` + +### Metamethods + + + +#### `__add(t)` + +Return the union of two sets using `+`. + +**Parameters**: + +- `t` (`mods.Set|mods.List|table`): Other set/list. + +**Return**: + +- `set` (`mods.Set`): New set. + +**Example**: + +```lua +a = Set({ "a", "b" }) +b = Set({ "b", "c" }) +u = a + b --> { a = true, b = true, c = true } +``` + +> [!NOTE] +> +> `__add` is the operator form of `:union(set)`. + + + +#### `__band(t)` + +Return the intersection of two sets using `&`. + +**Parameters**: + +- `t` (`mods.Set|mods.List|table`): Other set/list. + +**Return**: + +- `set` (`mods.Set`): New set. + +**Example**: + +```lua +a = Set({ "a", "b" }) +b = Set({ "b", "c" }) +i = a & b --> { b = true } +``` + +> [!NOTE] +> +> `__band` is the operator form of `:intersection(set)` on Lua 5.3+. + + + +#### `__bor(t)` + +Return the union of two sets using `|`. + +**Parameters**: + +- `t` (`mods.Set|mods.List|table`): Other set/list. + +**Return**: + +- `set` (`mods.Set`): New set. + +**Example**: + +```lua +a = Set({ "a", "b" }) +b = Set({ "b", "c" }) +u = a | b --> { a = true, b = true, c = true } +``` + +> [!NOTE] +> +> `__bor` is the operator form of `:union(set)` on Lua 5.3+. + + + +#### `__bxor(t)` + +Return elements present in exactly one set using `^`. + +**Parameters**: + +- `t` (`mods.Set|mods.List|table`): Other set/list. + +**Return**: + +- `set` (`mods.Set`): New set. + +**Example**: + +```lua +a = Set({ "a", "b" }) +b = Set({ "b", "c" }) +d = a ^ b --> { a = true, c = true } +``` + +> [!NOTE] +> +> `__bxor` is the operator form of `:symmetric_difference(set)` on Lua 5.3+. + + + +#### `__eq(t)` + +Return true if both sets contain exactly the same members using `==`. + +**Parameters**: + +- `t` (`mods.Set|mods.List|table`): Other set/list. + +**Return**: + +- `isEqual` (`boolean`): True when both sets contain the same members. + +**Example**: + +```lua +ok = Set({ "a", "b" }) == Set({ "b", "a" }) --> true +``` + +> [!NOTE] +> +> `__eq` is the operator form of `:equals(set)`. + + + +#### `__le(t)` + +Return true if the left set is a subset of the right set using `<=`. + +**Parameters**: + +- `t` (`mods.Set|mods.List|table`): Other set/list. + +**Return**: + +- `isSubset` (`boolean`): True when `self` is a subset of `set`. + +**Example**: + +```lua +a = Set({ "a" }) +b = Set({ "a", "b" }) +ok = a <= b --> true +``` + +> [!NOTE] +> +> `__le` is the operator form of `:issubset(set)`. + + + +#### `__lt(set)` + +Return true if the left set is a proper subset of the right set using `<`. + +**Parameters**: + +- `set` (`mods.Set|table`): Other set. + +**Return**: + +- `isProperSubset` (`boolean`): True when `self` is a proper subset of `set`. + +**Example**: + +```lua +a = Set({ "a" }) +b = Set({ "a", "b" }) +ok = a < b --> true +``` + + + +#### `__sub(set)` + +Return the difference of two sets using `-`. + +**Parameters**: + +- `set` (`mods.Set|table`): Other set. + +**Return**: + +- `set` (`mods.Set`): New set. + +**Example**: + +```lua +a = Set({ "a", "b" }) +b = Set({ "b", "c" }) +d = a - b --> { a = true } +``` + +> [!NOTE] +> +> `__sub` is the operator form of `:difference(set)`. + + + +#### `__tostring()` + +Render the set via `tostring(set)`. + +**Return**: + +- `renderedSet` (`string`): Rendered set string. + +**Example**: + +```lua +s = tostring(Set({ "b", "a", 1 })) --> '{ 1, "a", "b" }' +``` diff --git a/docs/api/str.md b/docs/api/str.md new file mode 100644 index 0000000..7a5091e --- /dev/null +++ b/docs/api/str.md @@ -0,0 +1,968 @@ +--- +description: + "String operations for searching, splitting, trimming, and formatting text." +--- + +# `str` + +String operations for searching, splitting, trimming, and formatting text. + +## Usage + +```lua +str = require "mods.str" + +print(str.capitalize("hello world")) --> "Hello world" +``` + +## Functions + +**Casing & Transform**: + +| Function | Description | +| -------------------------------------------------------- | --------------------------------------------------------- | +| [`startswith(s, prefix, start?, stop?)`](#fn-startswith) | Return true if string starts with prefix. | +| [`swapcase(s)`](#fn-swapcase) | Return a copy with case of alphabetic characters swapped. | +| [`title(s)`](#fn-title) | Return titlecased copy. | +| [`translate(s, table_map)`](#fn-translate) | Translate characters using a mapping table. | +| [`upper(s)`](#fn-upper) | Return uppercased copy. | +| [`zfill(s, width)`](#fn-zfill) | Pad numeric string on the left with zeros. | + +**Formatting**: + +| Function | Description | +| ---------------------------------------------------- | --------------------------------------------------------------------- | +| [`capitalize(s)`](#fn-capitalize) | Return copy with first character capitalized and the rest lowercased. | +| [`center(s, width, fillchar?)`](#fn-center) | Center string within width, padded with fill characters. | +| [`count(s, sub, start?, stop?)`](#fn-count) | Count non-overlapping occurrences of a substring. | +| [`endswith(s, suffix, start?, stop?)`](#fn-endswith) | Return true if string ends with suffix. | +| [`expandtabs(s, tabsize?)`](#fn-expandtabs) | Expand tabs to spaces using given tabsize. | +| [`find(s, sub, start?, stop?)`](#fn-find) | Return lowest index of substring or nil if not found. | +| [`format_map(s, mapping)`](#fn-format-map) | Format string with mapping (key-based) replacement. | + +**Layout**: + +| Function | Description | +| ----------------------------------------- | ------------------------------------------------------------------- | +| [`join(sep, ls)`](#fn-join) | Join an array-like table of strings using this string as separator. | +| [`ljust(s, width, fillchar?)`](#fn-ljust) | Left-justify string in a field of given width. | +| [`lower(s)`](#fn-lower) | Return lowercased copy. | +| [`lstrip(s, chars?)`](#fn-lstrip) | Remove leading characters (default: whitespace). | +| [`rstrip(s, chars?)`](#fn-rstrip) | Remove trailing characters (default: whitespace). | +| [`strip(s, chars?)`](#fn-strip) | Remove leading and trailing characters (default: whitespace). | + +**Predicates**: + +| Function | Description | +| ------------------------------------- | -------------------------------------------------------------------------------------------- | +| [`isalnum(s)`](#fn-isalnum) | Return true if all characters are alphanumeric and string is non-empty. | +| [`isalpha(s)`](#fn-isalpha) | Return true if all characters are alphabetic and string is non-empty. | +| [`isascii(s)`](#fn-isascii) | Return true if all characters are ASCII. | +| [`isdecimal(s)`](#fn-isdecimal) | Return true if all characters are decimal characters and string is non-empty. | +| [`isidentifier(s)`](#fn-isidentifier) | Return true if string is a valid identifier and not a reserved keyword. | +| [`islower(s)`](#fn-islower) | Return true if all cased characters are lowercase and there is at least one cased character. | +| [`isprintable(s)`](#fn-isprintable) | Return true if all characters are printable. | +| [`isspace(s)`](#fn-isspace) | Return true if all characters are whitespace and string is non-empty. | +| [`istitle(s)`](#fn-istitle) | Return true if string is titlecased. | +| [`isupper(s)`](#fn-isupper) | Return true if all cased characters are uppercase and there is at least one cased character. | + +**Split & Replace**: + +| Function | Description | +| --------------------------------------------- | ------------------------------------------------------------------------- | +| [`partition(s, sep)`](#fn-partition) | Partition string into head, sep, tail from left. | +| [`removeprefix(s, prefix)`](#fn-removeprefix) | Remove prefix if present. | +| [`removesuffix(s, suffix)`](#fn-removesuffix) | Remove suffix if present. | +| [`replace(s, old, new, count?)`](#fn-replace) | Return a copy of the string with all occurrences of a substring replaced. | +| [`rfind(s, sub, start?, stop?)`](#fn-rfind) | Return highest index of substring or nil if not found. | +| [`rjust(s, width, fillchar?)`](#fn-rjust) | Right-justify string in a field of given width. | +| [`rpartition(s, sep)`](#fn-rpartition) | Partition string into head, sep, tail from right. | +| [`rsplit(s, sep?, maxsplit?)`](#fn-rsplit) | Split from the right by separator, up to maxsplit. | +| [`split(s, sep?, maxsplit?)`](#fn-split) | Split by separator (or whitespace) up to maxsplit. | +| [`splitlines(s, keepends?)`](#fn-splitlines) | Split on line boundaries. | + +### Casing & Transform + + + +#### `startswith(s, prefix, start?, stop?)` + +Return true if string starts with prefix. + +**Parameters**: + +- `s` (`string`): Input string. +- `prefix` (`string|string[]`): Prefix string. +- `start?` (`integer`): Optional start index (defaults to `1`). +- `stop?` (`integer`): Optional exclusive end index (defaults to `#s + 1`). + +**Return**: + +- `hasPrefix` (`boolean`): True when `s` starts with `prefix`. + +**Example**: + +```lua +ok = startswith("hello.lua", "he") --> true +``` + +> [!NOTE] +> +> If prefix is a list, returns `true` when any prefix matches. + + + +#### `swapcase(s)` + +Return a copy with case of alphabetic characters swapped. + +**Parameters**: + +- `s` (`string`): Input string. + +**Return**: + +- `swappedCase` (`string`): String with alphabetic case swapped. + +**Example**: + +```lua +s = swapcase("AbC") --> "aBc" +``` + + + +#### `title(s)` + +Return titlecased copy. + +**Parameters**: + +- `s` (`string`): Input string. + +**Return**: + +- `titlecased` (`string`): Titlecased string. + +**Example**: + +```lua +s = title("hello world") --> "Hello World" +``` + + + +#### `translate(s, table_map)` + +Translate characters using a mapping table. + +**Parameters**: + +- `s` (`string`): Input string. +- `table_map` (`table`): Character translation map. + +**Return**: + +- `translated` (`string`): Translated string. + +**Example**: + +```lua +map = { [string.byte("a")] = "b", ["c"] = false } +s = translate("abc", map) --> "bb" +``` + + + +#### `upper(s)` + +Return uppercased copy. + +**Parameters**: + +- `s` (`string`): Input string. + +**Return**: + +- `uppercased` (`string`): Uppercased string. + +**Example**: + +```lua +s = upper("Hello") --> "HELLO" +``` + + + +#### `zfill(s, width)` + +Pad numeric string on the left with zeros. + +**Parameters**: + +- `s` (`string`): Input string. +- `width` (`integer`): Target width. + +**Return**: + +- `zeroFilled` (`string`): Zero-padded string. + +**Example**: + +```lua +s = zfill("42", 5) --> "00042" +``` + +### Formatting + + + +#### `capitalize(s)` + +Return copy with first character capitalized and the rest lowercased. + +**Parameters**: + +- `s` (`string`): Input string. + +**Return**: + +- `capitalized` (`string`): Capitalized string. + +**Example**: + +```lua +s = capitalize("hello WORLD") --> "Hello world" +``` + + + +#### `center(s, width, fillchar?)` + +Center string within width, padded with fill characters. + +**Parameters**: + +- `s` (`string`): Input string. +- `width` (`integer`): Target width. +- `fillchar?` (`string`): Optional fill character. + +**Return**: + +- `centered` (`string`): Centered string. + +**Example**: + +```lua +s = center("hi", 6, "-") --> "--hi--" +``` + + + +#### `count(s, sub, start?, stop?)` + +Count non-overlapping occurrences of a substring. + +**Parameters**: + +- `s` (`string`): Input string. +- `sub` (`string`): Substring to search. +- `start?` (`integer`): Optional start index (defaults to `1`). +- `stop?` (`integer`): Optional exclusive end index (defaults to `#s + 1`). + +**Return**: + +- `count` (`integer`): Number of non-overlapping matches. + +**Example**: + +```lua +n = count("aaaa", "aa") --> 2 +n = count("aaaa", "a", 2, -1) --> 2 +n = count("abcd", "") --> 5 +``` + + + +#### `endswith(s, suffix, start?, stop?)` + +Return true if string ends with suffix. + +**Parameters**: + +- `s` (`string`): Input string. +- `suffix` (`string|string[]`): Suffix string. +- `start?` (`integer`): Optional start index (defaults to `1`). +- `stop?` (`integer`): Optional exclusive end index (defaults to `#s + 1`). + +**Return**: + +- `hasSuffix` (`boolean`): True when `s` ends with `suffix`. + +**Example**: + +```lua +ok = endswith("hello.lua", ".lua") --> true +``` + +> [!NOTE] +> +> If suffix is a list, returns `true` when any suffix matches. + + + +#### `expandtabs(s, tabsize?)` + +Expand tabs to spaces using given tabsize. + +**Parameters**: + +- `s` (`string`): Input string. +- `tabsize?` (`integer`): Optional tab width (defaults to `8`). + +**Return**: + +- `expanded` (`string`): String with tabs expanded. + +**Example**: + +```lua +s = expandtabs("a\tb", 4) --> "a b" +``` + + + +#### `find(s, sub, start?, stop?)` + +Return lowest index of substring or nil if not found. + +**Parameters**: + +- `s` (`string`): Input string. +- `sub` (`string`): Substring to search. +- `start?` (`integer`): Optional start index (defaults to `1`). +- `stop?` (`integer`): Optional exclusive end index (defaults to `#s + 1`). + +**Return**: + +- `index` (`integer?`): First match index, or `nil` when not found. + +**Example**: + +```lua +i = find("hello", "ll") --> 3 +``` + + + +#### `format_map(s, mapping)` + +Format string with mapping (key-based) replacement. + +**Parameters**: + +- `s` (`string`): Template string with `{key}` placeholders. +- `mapping` (`table`): Values used to replace placeholder keys. + +**Return**: + +- `formatted` (`string`): Formatted string with placeholders replaced. + +**Example**: + +```lua +s = format_map("hi {name}", { name = "bob" }) --> "hi bob" +``` + +> [!NOTE] +> +> `format_map` is a lightweight `{key}` replacement helper. For richer +> templating, use `mods.template`. + +### Layout + + + +#### `join(sep, ls)` + +Join an array-like table of strings using this string as separator. + +**Parameters**: + +- `sep` (`string`): Separator value. +- `ls` (`string[]`): Table value. + +**Return**: + +- `joined` (`string`): Joined string. + +**Example**: + +```lua +s = join(",", { "a", "b", "c" }) --> "a,b,c" +``` + + + +#### `ljust(s, width, fillchar?)` + +Left-justify string in a field of given width. + +**Parameters**: + +- `s` (`string`): Input string. +- `width` (`integer`): Target width. +- `fillchar?` (`string`): Optional fill character. + +**Return**: + +- `leftJustified` (`string`): Left-justified string. + +**Example**: + +```lua +s = ljust("hi", 5, ".") --> "hi..." +``` + + + +#### `lower(s)` + +Return lowercased copy. + +**Parameters**: + +- `s` (`string`): Input string. + +**Return**: + +- `lowercased` (`string`): Lowercased string. + +**Example**: + +```lua +s = lower("HeLLo") --> "hello" +``` + + + +#### `lstrip(s, chars?)` + +Remove leading characters (default: whitespace). + +**Parameters**: + +- `s` (`string`): Input string. +- `chars?` (`string`): Optional character set. + +**Return**: + +- `leadingStripped` (`string`): String with leading characters removed. + +**Example**: + +```lua +s = lstrip(" hello") --> "hello" +``` + + + +#### `rstrip(s, chars?)` + +Remove trailing characters (default: whitespace). + +**Parameters**: + +- `s` (`string`): Input string. +- `chars?` (`string`): Optional character set. + +**Return**: + +- `trailingStripped` (`string`): String with trailing characters removed. + +**Example**: + +```lua +s = rstrip("hello ") --> "hello" +``` + + + +#### `strip(s, chars?)` + +Remove leading and trailing characters (default: whitespace). + +**Parameters**: + +- `s` (`string`): Input string. +- `chars?` (`string`): Optional character set. + +**Return**: + +- `stripped` (`string`): String with leading and trailing characters removed. + +**Example**: + +```lua +s = strip(" hello ") --> "hello" +``` + +### Predicates + + + +#### `isalnum(s)` + +Return true if all characters are alphanumeric and string is non-empty. + +**Parameters**: + +- `s` (`string`): Input string. + +**Return**: + +- `isAlnum` (`boolean`): True when `s` is non-empty and all characters are + alphanumeric. + +**Example**: + +```lua +ok = isalnum("abc123") --> true +``` + +> [!NOTE] +> +> Lua letters are ASCII by default, so non-ASCII letters are not alphanumeric. +> +> ```lua +> isalnum("á1") --> false +> ``` + + + +#### `isalpha(s)` + +Return true if all characters are alphabetic and string is non-empty. + +**Parameters**: + +- `s` (`string`): Input string. + +**Return**: + +- `isAlpha` (`boolean`): True when `s` is non-empty and all characters are + alphabetic. + +**Example**: + +```lua +ok = isalpha("abc") --> true +``` + +> [!NOTE] +> +> Lua letters are ASCII by default, so non-ASCII letters are not alphabetic. +> +> ```lua +> isalpha("á") --> false +> ``` + + + +#### `isascii(s)` + +Return true if all characters are ASCII. + +**Parameters**: + +- `s` (`string`): Input string. + +**Return**: + +- `isAscii` (`boolean`): True when all bytes in `s` are ASCII. + +**Example**: + +```lua +ok = isascii("hello") --> true +``` + +> [!NOTE] +> +> The empty string returns `true`. + + + +#### `isdecimal(s)` + +Return true if all characters are decimal characters and string is non-empty. + +**Parameters**: + +- `s` (`string`): Input string. + +**Return**: + +- `isDecimal` (`boolean`): True when `s` is non-empty and all characters are + decimal digits. + +**Example**: + +```lua +ok = isdecimal("123") --> true +``` + + + +#### `isidentifier(s)` + +Return true if string is a valid identifier and not a reserved keyword. + +**Parameters**: + +- `s` (`string`): Input string. + +**Return**: + +- `isIdentifier` (`boolean`): True when `s` is a valid identifier and not a + keyword. + +**Example**: + +```lua +ok = isidentifier("foo_bar") --> true +ok = isidentifier("2var") --> false +ok = isidentifier("end") --> false (keyword) +``` + + + +#### `islower(s)` + +Return true if all cased characters are lowercase and there is at least one +cased character. + +**Parameters**: + +- `s` (`string`): Input string. + +**Return**: + +- `isLower` (`boolean`): True when `s` has at least one cased character and all + are lowercase. + +**Example**: + +```lua +ok = islower("hello") --> true +``` + + + +#### `isprintable(s)` + +Return true if all characters are printable. + +**Parameters**: + +- `s` (`string`): Input string. + +**Return**: + +- `isPrintable` (`boolean`): True when all bytes in `s` are printable ASCII. + +**Example**: + +```lua +ok = isprintable("abc!") --> true +``` + +> [!NOTE] +> +> The empty string returns `true`. + + + +#### `isspace(s)` + +Return true if all characters are whitespace and string is non-empty. + +**Parameters**: + +- `s` (`string`): Input string. + +**Return**: + +- `isSpace` (`boolean`): True when `s` is non-empty and all characters are + whitespace. + +**Example**: + +```lua +ok = isspace(" \t") --> true +``` + + + +#### `istitle(s)` + +Return true if string is titlecased. + +**Parameters**: + +- `s` (`string`): Input string. + +**Return**: + +- `isTitle` (`boolean`): True when `s` is titlecased. + +**Example**: + +```lua +ok = istitle("Hello World") --> true +``` + + + +#### `isupper(s)` + +Return true if all cased characters are uppercase and there is at least one +cased character. + +**Parameters**: + +- `s` (`string`): Input string. + +**Return**: + +- `isUpper` (`boolean`): True when `s` has at least one cased character and all + are uppercase. + +**Example**: + +```lua +ok = isupper("HELLO") --> true +``` + +### Split & Replace + + + +#### `partition(s, sep)` + +Partition string into head, sep, tail from left. + +**Parameters**: + +- `s` (`string`): Input string. +- `sep` (`string`): Separator value. + +**Return**: + +- `head` (`string`): Part before the separator. +- `separator` (`string`): Matched separator, or empty string when not found. +- `tail` (`string`): Part after the separator. + +**Example**: + +```lua +a, b, c = partition("a-b-c", "-") --> "a", "-", "b-c" +``` + + + +#### `removeprefix(s, prefix)` + +Remove prefix if present. + +**Parameters**: + +- `s` (`string`): Input string. +- `prefix` (`string`): Prefix string. + +**Return**: + +- `prefixRemoved` (`string`): String with prefix removed when present. + +**Example**: + +```lua +s = removeprefix("foobar", "foo") --> "bar" +``` + + + +#### `removesuffix(s, suffix)` + +Remove suffix if present. + +**Parameters**: + +- `s` (`string`): Input string. +- `suffix` (`string`): Suffix string. + +**Return**: + +- `suffixRemoved` (`string`): String with suffix removed when present. + +**Example**: + +```lua +s = removesuffix("foobar", "bar") --> "foo" +``` + + + +#### `replace(s, old, new, count?)` + +Return a copy of the string with all occurrences of a substring replaced. + +**Parameters**: + +- `s` (`string`): Input string. +- `old` (`string`): Substring to replace. +- `new` (`string`): Replacement string. +- `count?` (`integer`): Optional maximum replacement count. + +**Return**: + +- `replaced` (`string`): String with replacements applied. + +**Example**: + +```lua +s = replace("a-b-c", "-", "_", 1) --> "a_b-c" +``` + + + +#### `rfind(s, sub, start?, stop?)` + +Return highest index of substring or nil if not found. + +**Parameters**: + +- `s` (`string`): Input string. +- `sub` (`string`): Substring to search. +- `start?` (`integer`): Optional start index (defaults to `1`). +- `stop?` (`integer`): Optional inclusive end index (defaults to `#s`). + +**Return**: + +- `index` (`integer?`): Last match index, or `nil` when not found. + +**Example**: + +```lua +i = rfind("ababa", "ba") --> 4 +``` + + + +#### `rjust(s, width, fillchar?)` + +Right-justify string in a field of given width. + +**Parameters**: + +- `s` (`string`): Input string. +- `width` (`integer`): Target width. +- `fillchar?` (`string`): Optional fill character. + +**Return**: + +- `rightJustified` (`string`): Right-justified string. + +**Example**: + +```lua +s = rjust("hi", 5, ".") --> "...hi" +``` + + + +#### `rpartition(s, sep)` + +Partition string into head, sep, tail from right. + +**Parameters**: + +- `s` (`string`): Input string. +- `sep` (`string`): Separator value. + +**Return**: + +- `head` (`string`): Part before the separator. +- `separator` (`string`): Matched separator, or empty string when not found. +- `tail` (`string`): Part after the separator. + +**Example**: + +```lua +a, b, c = rpartition("a-b-c", "-") --> "a-b", "-", "c" +``` + + + +#### `rsplit(s, sep?, maxsplit?)` + +Split from the right by separator, up to maxsplit. + +**Parameters**: + +- `s` (`string`): Input string. +- `sep?` (`string`): Optional separator value. +- `maxsplit?` (`integer`): Optional maximum number of splits. + +**Return**: + +- `parts` (`mods.List`): Split parts. + +**Example**: + +```lua +parts = rsplit("a,b,c", ",", 1) --> { "a,b", "c" } +``` + + + +#### `split(s, sep?, maxsplit?)` + +Split by separator (or whitespace) up to maxsplit. + +**Parameters**: + +- `s` (`string`): Input string. +- `sep?` (`string`): Optional separator value. +- `maxsplit?` (`integer`): Optional maximum number of splits. + +**Return**: + +- `parts` (`mods.List`): Split parts. + +**Example**: + +```lua +parts = split("a,b,c", ",") --> { "a", "b", "c" } +``` + + + +#### `splitlines(s, keepends?)` + +Split on line boundaries. + +**Parameters**: + +- `s` (`string`): Input string. +- `keepends?` (`boolean`): Optional whether to keep line endings. + +**Return**: + +- `lines` (`mods.List`): Split lines. + +**Example**: + +```lua +lines = splitlines("a\nb\r\nc") --> { "a", "b", "c" } +``` diff --git a/docs/api/stringcase.md b/docs/api/stringcase.md new file mode 100644 index 0000000..977d9c8 --- /dev/null +++ b/docs/api/stringcase.md @@ -0,0 +1,391 @@ +--- +description: "String case conversion and word splitting." +--- + +# `stringcase` + +String case conversion and word splitting. + +## Usage + +```lua +stringcase = require "mods.stringcase" + +print(stringcase.snake("FooBar")) --> "foo_bar" +``` + +## Functions + +**Basic**: + +| Function | Description | +| ----------------------- | -------------------------------- | +| [`lower(s)`](#fn-lower) | Convert string to all lowercase. | +| [`upper(s)`](#fn-upper) | Convert string to all uppercase. | + +**Letter Case**: + +| Function | Description | +| --------------------------------- | ------------------------------------------------------------------------- | +| [`capitalize(s)`](#fn-capitalize) | Capitalize the first letter and lowercase the rest. | +| [`sentence(s)`](#fn-sentence) | Convert string to sentence case (first letter uppercase, rest unchanged). | +| [`swapcase(s)`](#fn-swapcase) | Swap case of each letter. | + +**Word Case**: + +| Function | Description | +| --------------------------------- | --------------------------------------------------------------------- | +| [`acronym(s)`](#fn-acronym) | Get acronym of words in string (first letters only). | +| [`camel(s)`](#fn-camel) | Convert string to camelCase. | +| [`constant(s)`](#fn-constant) | Convert string to CONSTANT_CASE (uppercase snake_case). | +| [`delimit(s, sep?)`](#fn-delimit) | Normalize to snake_case, then delimit words with a separator. | +| [`dot(s)`](#fn-dot) | Convert string to dot.case. | +| [`kebab(s)`](#fn-kebab) | Convert string to kebab-case. | +| [`pascal(s)`](#fn-pascal) | Convert string to PascalCase. | +| [`path(s)`](#fn-path) | Convert string to path/case (slashes between words). | +| [`snake(s)`](#fn-snake) | Convert string to snake_case. | +| [`space(s)`](#fn-space) | Convert string to space case (spaces between words). | +| [`title(s)`](#fn-title) | Convert string to Title Case (first letter of each word capitalized). | + +### Basic + + + +#### `lower(s)` + +Convert string to all lowercase. + +**Parameters**: + +- `s` (`string`): Input string. + +**Return**: + +- `lowercased` (`string`): Lowercased string. + +**Example**: + +```lua +lower("foo_bar-baz") --> "foo_bar-baz" +lower("FooBar baz") --> "foobar baz" +``` + + + +#### `upper(s)` + +Convert string to all uppercase. + +**Parameters**: + +- `s` (`string`): Input string. + +**Return**: + +- `uppercased` (`string`): Uppercased string. + +**Example**: + +```lua +upper("foo_bar-baz") --> "FOO_BAR-BAZ" +upper("FooBar baz") --> "FOOBAR BAZ" +``` + +### Letter Case + + + +#### `capitalize(s)` + +Capitalize the first letter and lowercase the rest. + +**Parameters**: + +- `s` (`string`): Input string. + +**Return**: + +- `capitalized` (`string`): Capitalized string. + +**Example**: + +```lua +capitalize("foo_bar-baz") --> "Foo_bar-baz" +capitalize("FooBar baz") --> "Foobar baz" +``` + + + +#### `sentence(s)` + +Convert string to sentence case (first letter uppercase, rest unchanged). + +**Parameters**: + +- `s` (`string`): Input string. + +**Return**: + +- `sentenceCased` (`string`): Sentence-cased string. + +**Example**: + +```lua +sentence("foo_bar-baz") --> "Foo_bar-baz" +sentence("FooBar baz") --> "FooBar baz" +``` + + + +#### `swapcase(s)` + +Swap case of each letter. + +**Parameters**: + +- `s` (`string`): Input string. + +**Return**: + +- `swapCased` (`string`): Swap-cased string. + +**Example**: + +```lua +swapcase("foo_bar-baz") --> "FOO_BAR-BAZ" +swapcase("FooBar baz") --> "fOObAR BAZ" +``` + +### Word Case + + + +#### `acronym(s)` + +Get acronym of words in string (first letters only). + +**Parameters**: + +- `s` (`string`): Input string. + +**Return**: + +- `acronym` (`string`): Acronym string. + +**Example**: + +```lua +acronym("foo_bar-baz") --> "FBB" +acronym("FooBar baz") --> "FBB" +``` + + + +#### `camel(s)` + +Convert string to camelCase. + +**Parameters**: + +- `s` (`string`): Input string. + +**Return**: + +- `camelCased` (`string`): Camel-cased string. + +**Example**: + +```lua +camel("foo_bar-baz") --> "fooBarBaz" +camel("FooBar baz") --> "fooBarBaz" +``` + + + +#### `constant(s)` + +Convert string to CONSTANT_CASE (uppercase snake_case). + +**Parameters**: + +- `s` (`string`): Input string. + +**Return**: + +- `constantCased` (`string`): Constant-cased string. + +**Example**: + +```lua +constant("foo_bar-baz") --> "FOO_BAR_BAZ" +constant("FooBar baz") --> "FOO_BAR_BAZ" +``` + + + +#### `delimit(s, sep?)` + +Normalize to snake_case, then delimit words with a separator. + +**Parameters**: + +- `s` (`string`): Input string. +- `sep?` (`string`): Optional separator value (defaults to `""`). + +**Return**: + +- `delimited` (`string`): String with normalized words separated by `sep`. + +**Example**: + +```lua +delimit("foo_bar-baz", "-") --> "foo-bar-baz" +delimit("FooBar baz", "-") --> "foo-bar-baz" +``` + + + +#### `dot(s)` + +Convert string to dot.case. + +**Parameters**: + +- `s` (`string`): Input string. + +**Return**: + +- `dotCased` (`string`): Dot-cased string. + +**Example**: + +```lua +dot("foo_bar-baz") --> "foo.bar.baz" +dot("FooBar baz") --> "foo.bar.baz" +``` + + + +#### `kebab(s)` + +Convert string to kebab-case. + +**Parameters**: + +- `s` (`string`): Input string. + +**Return**: + +- `kebabCased` (`string`): Kebab-cased string. + +**Example**: + +```lua +kebab("foo_bar-baz") --> "foo-bar-baz" +kebab("FooBar baz") --> "foo-bar-baz" +``` + + + +#### `pascal(s)` + +Convert string to PascalCase. + +**Parameters**: + +- `s` (`string`): Input string. + +**Return**: + +- `pascalCased` (`string`): Pascal-cased string. + +**Example**: + +```lua +pascal("foo_bar-baz") --> "FooBarBaz" +pascal("FooBar baz") --> "FooBarBaz" +``` + + + +#### `path(s)` + +Convert string to path/case (slashes between words). + +**Parameters**: + +- `s` (`string`): Input string. + +**Return**: + +- `pathCased` (`string`): Path-cased string. + +**Example**: + +```lua +path("foo_bar-baz") --> "foo/bar/baz" +path("FooBar baz") --> "foo/bar/baz" +``` + + + +#### `snake(s)` + +Convert string to snake_case. + +**Parameters**: + +- `s` (`string`): Input string. + +**Return**: + +- `snakeCased` (`string`): Snake-cased string. + +**Example**: + +```lua +snake("foo_bar-baz") --> "foo_bar_baz" +snake("FooBar baz") --> "foo_bar_baz" +``` + + + +#### `space(s)` + +Convert string to space case (spaces between words). + +**Parameters**: + +- `s` (`string`): Input string. + +**Return**: + +- `spaceCased` (`string`): Space-cased string. + +**Example**: + +```lua +space("foo_bar-baz") --> "foo bar baz" +space("FooBar baz") --> "foo bar baz" +``` + + + +#### `title(s)` + +Convert string to Title Case (first letter of each word capitalized). + +**Parameters**: + +- `s` (`string`): Input string. + +**Return**: + +- `titleCased` (`string`): Title-cased string. + +**Example**: + +```lua +title("foo_bar-baz") --> "Foo Bar Baz" +title("FooBar baz") --> "Foo Bar Baz" +``` diff --git a/docs/api/stringify.md b/docs/api/stringify.md new file mode 100644 index 0000000..7d6a3b2 --- /dev/null +++ b/docs/api/stringify.md @@ -0,0 +1,144 @@ +--- +description: "Render Lua values as readable source-like text." +--- + +# `stringify` + +Render Lua values as readable source-like text. + +## Usage + +```lua +local stringify = require("mods").stringify + +local t = { "first", name = "Ada"} + +print(stringify(t)) +--> { +--> "first", +--> name = "Ada" +--> } +``` + + + +## `stringify(value, opts?)` + +Render Lua values as readable source-like text. + +**Parameters**: + +- `value` (`any`): Lua value to render. +- `opts?` + (`{omit_array_keys?:boolean, indent?:string, newline?:string, replacer?:fun(k,v):(result:any)}`): + Rendering options. + +**Return**: + +- `out` (`string`): Readable string representation. + +**Example**: + +```lua +local stringify = require("mods").stringify + +local t = { + "first", + "second", + name = "Ada", + ok = true, + nested = { value = 42 }, +} + +print(stringify(t)) +--> { +--> "first", +--> "second", +--> name = "Ada", +--> nested = { +--> value = 42 +--> }, +--> ok = true +--> } +``` + +### Key formatting + +Strings are quoted and reserved keys use bracket notation. + +```lua +print(stringify({ name = "Ada", ["with space"] = true })) +--> { +--> ["with space"] = true, +--> name = "Ada" +--> } +``` + +### Cycle rendering + +Circular references render as ``. + +```lua +local t = {} +t.self = t + +print(stringify(t)) +--> { +--> self = +--> } +``` + +## Options + +### `omit_array_keys` + +Contiguous positive integer keys render as implicit array entries by default. +Set `omit_array_keys = false` to keep them explicit. + +```lua +print(stringify({ "first", nil, "third" }, { omit_array_keys = false })) +--> { +--> [1] = "first", +--> [3] = "third" +--> } +``` + +### `indent` + +`indent` controls the repeated indentation prefix. It defaults to two spaces. + +```lua +print(stringify({ a = { b = true } }, { indent = ".." })) +--> { +--> a = { +--> b = true +--> } +--> } +``` + +### `newline` + +`newline` controls the line separator. Use `newline = ""` for inline output. + +```lua +print(stringify({ a = 1, b = 2 }, { newline = "" })) +--> {a=1,b=2} +``` + +### `replacer` + +`replacer` can transform or remove values before they are rendered. + +```lua +local function replacer(k, v) + if k == "secret" then + return nil + end + return v +end + +print(stringify({ name = "Ada", secret = "hidden" }, { replacer = replacer })) +--> { +--> name = "Ada" +--> } +``` diff --git a/docs/api/tbl.md b/docs/api/tbl.md new file mode 100644 index 0000000..28c8f22 --- /dev/null +++ b/docs/api/tbl.md @@ -0,0 +1,473 @@ +--- +description: + "Table operations for querying, copying, merging, and transforming tables." +--- + +# `tbl` + +Table operations for querying, copying, merging, and transforming tables. + +## Usage + +```lua +tbl = require "mods.tbl" + +print(tbl.count({ a = 1, b = 2 })) --> 2 +``` + +## Functions + +**Copies**: + +| Function | Description | +| ----------------------------- | ----------------------------------- | +| [`copy(t)`](#fn-copy) | Create a shallow copy of the table. | +| [`deepcopy(v)`](#fn-deepcopy) | Create a deep copy of a value. | + +**Core Utilities**: + +| Function | Description | +| ----------------------- | --------------------------------------- | +| [`clear(t)`](#fn-clear) | Remove all entries from the table. | +| [`count(t)`](#fn-count) | Return the number of keys in the table. | + +**Iterators**: + +| Function | Description | +| ------------------------------- | -------------------------------------------- | +| [`foreach(t, fn)`](#fn-foreach) | Call a function for each value in the table. | +| [`spairs(t)`](#fn-spairs) | Iterate key-value pairs in sorted key order. | + +**Queries**: + +| Function | Description | +| ------------------------------------ | ---------------------------------------------------------------- | +| [`deep_equal(a, b)`](#fn-deep-equal) | Return `true` if two tables are deeply equal. | +| [`filter(t, pred)`](#fn-filter) | Filter entries by a value predicate. | +| [`find(t, v)`](#fn-find) | Find the first key whose value equals the given value. | +| [`find_if(t, pred)`](#fn-find-if) | Find first value and key matching predicate. | +| [`get(t, ...)`](#fn-get) | Safely get nested value by keys. | +| [`is_same(a, b)`](#fn-is-same) | Return `true` if two tables have the same keys and equal values. | + +**Transforms**: + +| Function | Description | +| ------------------------------ | -------------------------------------------------- | +| [`invert(t)`](#fn-invert) | Invert keys/values into new table. | +| [`isempty(t)`](#fn-isempty) | Return true if table has no entries. | +| [`keys(t)`](#fn-keys) | Return a list of all keys in the table. | +| [`map(t, fn)`](#fn-map) | Return a new table by mapping each key-value pair. | +| [`update(t1, t2)`](#fn-update) | Merge entries from `t2` into `t1` and return `t1`. | +| [`values(t)`](#fn-values) | Return a list of all values in the table. | + +### Copies + + + +#### `copy(t)` + +Create a shallow copy of the table. + +**Parameters**: + +- `t` (`T`): Source table. + +**Return**: + +- `copy` (`T`): Shallow-copied table. + +**Example**: + +```lua +t = copy({ a = 1, b = 2 }) --> { a = 1, b = 2 } +``` + + + +#### `deepcopy(v)` + +Create a deep copy of a value. + +**Parameters**: + +- `v` (`T`): Input value. + +**Return**: + +- `copiedValue` (`T`): Deep-copied value. + +**Example**: + +```lua +t = deepcopy({ a = { b = 1 } }) --> { a = { b = 1 } } +n = deepcopy(42) --> 42 +``` + +> [!NOTE] +> +> If `v` is a table, all nested tables are copied recursively; other types are +> returned as-is. + +### Core Utilities + + + +#### `clear(t)` + +Remove all entries from the table. + +**Parameters**: + +- `t` (`table`): Target table. + +**Return**: + +- `none` (`nil`) + +**Example**: + +```lua +t = { a = 1, b = 2 } +clear(t) --> t = {} +``` + + + +#### `count(t)` + +Return the number of keys in the table. + +**Parameters**: + +- `t` (`table`): Input table. + +**Return**: + +- `count` (`integer`): Number of keys in `t`. + +**Example**: + +```lua +n = count({ a = 1, b = 2 }) --> 2 +``` + +### Iterators + + + +#### `foreach(t, fn)` + +Call a function for each value in the table. + +**Parameters**: + +- `t` (`table`): Input table. +- `fn` (`fun(value:V, key:K)`): Function invoked for each entry. + +**Return**: + +- `none` (`nil`) + +**Example**: + +```lua +foreach({ a = 1, b = 2 }, function(v, k) + print(k, v) +end) +``` + + + +#### `spairs(t)` + +Iterate key-value pairs in sorted key order. + +**Parameters**: + +- `t` (`T`): Input table. + +**Return**: + +- `iterator` (`fun(table: table, index?: K):(K, V)`): Sorted pairs + iterator. +- **value** (`T`) + +**Example**: + +```lua +for k, v in spairs({ b = 2, a = 1 }) do + print(k, v) +end +``` + +### Queries + + + +#### `deep_equal(a, b)` + +Return `true` if two tables are deeply equal. + +**Parameters**: + +- `a` (`table`): Left table. +- `b` (`table`): Right table. + +**Return**: + +- `isDeepEqual` (`boolean`): True when both tables are recursively equal. + +**Example**: + +```lua +ok = deep_equal({ a = { b = 1 } }, { a = { b = 1 } }) --> true +ok = deep_equal({ a = { b = 1 } }, { a = { b = 2 } }) --> false +``` + + + +#### `filter(t, pred)` + +Filter entries by a value predicate. + +**Parameters**: + +- `t` (`table`): Input table. +- `pred` (`fun(value:V):boolean`): Value predicate. + +**Return**: + +- `filtered` (`table`): Table containing entries where `pred(v)` is true. + +**Example**: + +```lua +even = filter({ a = 1, b = 2, c = 3 }, function(v) + return v % 2 == 0 +end) --> { b = 2 } +``` + + + +#### `find(t, v)` + +Find the first key whose value equals the given value. + +**Parameters**: + +- `t` (`table`): Input table. +- `v` (`V`): Value to find. + +**Return**: + +- `key` (`K?`): First matching key, or `nil` when not found. + +**Example**: + +```lua +key = find({ a = 1, b = 2, c = 2 }, 2) --> "b" or "c" +``` + + + +#### `find_if(t, pred)` + +Find first value and key matching predicate. + +**Parameters**: + +- `t` (`table`): Input table. +- `pred` (`fun(key:K,value:V):boolean`): Predicate function. + +**Return**: + +- `value` (`V?`): First matching value, or `nil` when not found. +- `key` (`K?`): Key for the first matching value, or `nil` when not found. + +**Example**: + +```lua +v, k = find_if({ a = 1, b = 2 }, function(v, k) + return k == "b" and v == 2 +end) --> 2, "b" +``` + + + +#### `get(t, ...)` + +Safely get nested value by keys. + +**Parameters**: + +- `t` (`table`): Root table. +- `...` (`any`): Additional arguments. + +**Return**: + +- `nestedValue` (`any`): Nested value, or `nil` when any key is missing. + +**Example**: + +```lua +t = { a = { b = { c = 1 } } } +v1 = get(t, "a", "b", "c") --> 1 +v2 = get(t) --> { a = { b = { c = 1 } } } +``` + +> [!NOTE] +> +> If no keys are provided, returns the input table. + + + +#### `is_same(a, b)` + +Return `true` if two tables have the same keys and equal values. + +**Parameters**: + +- `a` (`table`): Left table. +- `b` (`table`): Right table. + +**Return**: + +- `isSame` (`boolean`): True when both tables have the same keys and values. + +**Example**: + +```lua +ok = is_same({ a = 1, b = 2 }, { b = 2, a = 1 }) --> true +ok = is_same({ a = {} }, { a = {} }) --> false +``` + +### Transforms + + + +#### `invert(t)` + +Invert keys/values into new table. + +**Parameters**: + +- `t` (`table`): Input table. + +**Return**: + +- `inverted` (`table`): Inverted table (`value -> key`). + +**Example**: + +```lua +t = invert({ a = 1, b = 2 }) --> { [1] = "a", [2] = "b" } +``` + + + +#### `isempty(t)` + +Return true if table has no entries. + +**Parameters**: + +- `t` (`table`): Input table. + +**Return**: + +- `isEmpty` (`boolean`): True when `t` has no entries. + +**Example**: + +```lua +empty = isempty({}) --> true +``` + + + +#### `keys(t)` + +Return a list of all keys in the table. + +**Parameters**: + +- `t` (`table`): Input table. + +**Return**: + +- `keys` (`mods.List`): List of keys in `t`. + +**Example**: + +```lua +keys = keys({ a = 1, b = 2 }) --> { "a", "b" } +``` + + + +#### `map(t, fn)` + +Return a new table by mapping each key-value pair. + +**Parameters**: + +- `t` (`table`): Input table. +- `fn` (`fun(key:K, value:V):T`): Key-value mapping function. + +**Return**: + +- `mapped` (`table`): New table with mapped values. + +**Example**: + +```lua +t = map({ a = 1, b = 2 }, function(k, v) + return k .. v +end) --> { a = "a1", b = "b2" } +``` + +> [!NOTE] +> +> Output keeps original keys; only values are transformed by `fn`. + + + +#### `update(t1, t2)` + +Merge entries from `t2` into `t1` and return `t1`. + +**Parameters**: + +- `t1` (`T`): Target table. +- `t2` (`table`): Source table. + +**Return**: + +- `table` (`T`): Updated `t1` table. + +**Example**: + +```lua +t1 = { a = 1, b = 2 } +update(t1, { b = 3, c = 4 }) --> t1 is { a = 1, b = 3, c = 4 } +``` + + + +#### `values(t)` + +Return a list of all values in the table. + +**Parameters**: + +- `t` (`table`): Input table. + +**Return**: + +- `values` (`mods.List`): List of values in `t`. + +**Example**: + +```lua +vals = values({ a = 1, b = 2 }) --> { 1, 2 } +``` diff --git a/docs/api/template.md b/docs/api/template.md new file mode 100644 index 0000000..0d5bdc2 --- /dev/null +++ b/docs/api/template.md @@ -0,0 +1,123 @@ +--- +description: "String template rendering with {{." +--- + +# `template` + +String template rendering with {{...}} placeholders. + +## Usage + +```lua +template = require "mods.template" + +view = { + user = { name = "Ada" }, +} + +out = template("Hello {{user.name}}!", view) --> "Hello Ada!" +``` + + + +## `template(tmpl, view)` + +Render string templates with {{...}} placeholders. + +**Parameters**: + +- `tmpl` (`string`): Template string with placeholders. +- `view` (`table`): Input data used to resolve placeholders. + +**Return**: + +- `out` (`string`): Rendered output string. + +**Example**: + +```lua +view = { subject = "World" } +template("Hello {{subject}}", view) --> "Hello World" +``` + +> [!NOTE] +> +> Whitespace inside placeholders is ignored. +> +> ```lua +> template("Hi {{ name }}", { name = "Ada" }) --> "Hi Ada" +> ``` + +## Dot Paths + +Use dot notation to access nested values in `view`. + +```lua +view = { user = { meta = { role = "Engineer" } } } +template("Role: {{user.meta.role}}", view) --> "Role: Engineer" +``` + +> [!NOTE] +> +> {{.}} renders the entire root `view` table, not a nested +> field. +> +> ```lua +> template("View: {{.}}", { value = 123 }) +> --> View: { +> -- value = 123 +> -- } +> ``` + +## Function Values + +If a placeholder resolves to a function, that function is called and its result +is rendered. + +```lua +view = { name_func = function() return "Ada" end } +template("Hi {{name_func}}", view) --> "Hi Ada" +``` + +> [!NOTE] +> +> If the function returns `nil`, the placeholder renders as an empty string. + +## Table Values + +Table placeholders are rendered using `mods.stringify`. + +```lua +view = { data = { a = 1, b = true } } +template("Data: {{data}}", view) +--> Data: { +-- a = 1, +-- b = true +-- } +``` + +## Missing and Invalid Placeholders + +Missing keys render as an empty string. + +```lua +view = {} +template("Missing: {{unknown}}", view) --> "Missing: " +``` + +Invalid placeholder names render as an empty string (for example: +{{..}}, {{.name}}, +{{user.}}, {{user..name}}). + +```lua +view = { user = { name = "Ada" } } +template("Bad: {{user..name}}", view) --> "Bad: " +``` + +If a placeholder is not closed ({{unclosed), it is emitted +as-is. + +```lua +view = { name = "Ada" } +template("Hi {{name", view) --> "Hi {{name" +``` diff --git a/docs/api/utils.md b/docs/api/utils.md new file mode 100644 index 0000000..1309acd --- /dev/null +++ b/docs/api/utils.md @@ -0,0 +1,232 @@ +--- +description: "Shared utility helpers used across the Mods library." +--- + +# `utils` + +Shared utility helpers used across the Mods library. + +## Usage + +```lua +utils = require "mods.utils" + +print(utils.quote('hello "world"')) --> 'hello "world"' +``` + +## Functions + +**Formatting**: + +| Function | Description | +| ------------------------------- | -------------------------------------------------------------- | +| [`args_repr(v)`](#fn-args-repr) | Format a list-like table as a comma-separated argument string. | +| [`keypath(...)`](#fn-keypath) | Format a key chain as a Lua-like table access path. | +| [`quote(v)`](#fn-quote) | Smart-quote a string for readable Lua-like output. | + +**Lazy Loading**: + +| Function | Description | +| -------------------------------------------- | --------------------------------- | +| [`lazy_module(name, err?)`](#fn-lazy-module) | Return a lazy proxy for a module. | + +**Validation**: + +| Function | Description | +| ------------------------------------------------------------------- | ----------------------------------------------------------------------------- | +| [`assert_arg(argn, v, validator?, optional?, lv?)`](#fn-assert-arg) | Assert argument value using `mods.validate` and raise a Lua error on failure. | +| [`validate(name, v, validator?, optional?, msg?)`](#fn-validate) | Validate a value using `mods.validate` and raise a Lua error on failure. | +| [`validate(path, v, validator?, optional?, msg?)`](#fn-validate) | Validate a value using `mods.validate` and raise a Lua error on failure. | + +### Formatting + + + +#### `args_repr(v)` + +Format a list-like table as a comma-separated argument string. + +**Parameters**: + +- `v` (`any`): Value to format. `nil` returns an empty string. + +**Return**: + +- `out` (`string`): Argument list string. + +**Example**: + +```lua +utils.args_repr({ "a", 1, true }) --> '"a", 1, true' +``` + + + +#### `keypath(...)` + +Format a key chain as a Lua-like table access path. + +**Parameters**: + +- `...` (`any`): Additional arguments. + +**Return**: + +- `path` (`string`): Rendered key path. + +**Example**: + +```lua +p1 = utils.keypath("t", "a", "b", "c") --> "t.a.b.c" +p2 = utils.keypath("ctx", "users", 1, "name") --> "ctx.users[1].name" +p3 = utils.keypath("ctx", "invalid-key") --> 'ctx["invalid-key"]' +p4 = utils.keypath() --> "" +``` + + + +#### `quote(v)` + +Smart-quote a string for readable Lua-like output. + +**Parameters**: + +- `v` (`string`): String to quote. + +**Return**: + +- `out` (`string`): Quoted string. + +**Example**: + +```lua +print(utils.quote('He said "hi"')) -- 'He said "hi"' +print(utils.quote('say "hi" and \\'bye\\'')) -- "say \"hi\" and 'bye'" +``` + +### Lazy Loading + + + +#### `lazy_module(name, err?)` + +Return a lazy proxy for a module. + +The proxy rewrites its metamethods after first access while keeping the proxy +table itself free of cached fields. + +**Parameters**: + +- `name` (`string`): Module name passed to `require`. +- `err?` (`string`): Optional error message raised when loading fails. + +**Return**: + +- `module` (`{}`): Lazy proxy for the loaded module. + +**Example**: + +```lua +local fs = utils.lazy_module("mods.fs") +print(fs.exists("README.md")) + +local stringify = utils.lazy_module("mods.stringify") +print(stringify({ a = 1 })) +``` + +> [!NOTE] +> +> Supports both table-returning modules and function-returning modules. + +### Validation + + + +#### `assert_arg(argn, v, validator?, optional?, lv?)` + +Assert argument value using `mods.validate` and raise a Lua error on failure. + +**Parameters**: + +- `argn` (`integer`): Argument index for error context. +- `v` (`T`): Value to check. +- `validator?` (`modsValidatorName`): Validator name (defaults to `"truthy"`). +- `optional?` (`boolean`): Skip errors when `v` is `nil` (defaults to `false`). +- `lv?` (`integer`): Error level passed to `error` (defaults to `3`). + +**Return**: + +- `validatedValue` (`T`): Same input value on success, or `nil` when optional. + +**Example**: + +```lua +utils.assert_arg(1, "ok", "string") --> "ok" +utils.assert_arg(2, nil, "string", true) --> nil +utils.assert_arg(2, 123, "string") +--> raises: bad argument #2 (expected string, got number) +utils.assert_arg(3, "x", "number", false, "need {{expected}}, got {{got}}") +--> raises: bad argument #3 (need number, got string) +``` + +> [!NOTE] +> +> When the caller function name is available, error text includes +> `to ''` (Lua-style bad argument context). + + + +#### `validate(name, v, validator?, optional?, msg?)` + +Validate a value using `mods.validate` and raise a Lua error on failure. + +**Parameters**: + +- `name` (`string`): Name for the error prefix. +- `v` (`any`): Value to validate. +- `validator?` (`modsValidatorName`): Validator name (defaults to `"truthy"`). +- `optional?` (`boolean`): Skip errors when `v` is `nil` (defaults to `false`). +- `msg?` (`string`): Optional override template passed to `mods.validate`. + +**Return**: + +- `none` (`nil`) + +**Example**: + +```lua +utils.validate("path", "ok", "string") +utils.validate("name", nil, "string", true) +utils.validate("count", "x", "number") +--> raises: count: expected number, got string +``` + + + +#### `validate(path, v, validator?, optional?, msg?)` + +Validate a value using `mods.validate` and raise a Lua error on failure. + +**Parameters**: + +- `path` (`table`): Path parts for the error name. +- `v` (`any`): Value to validate. +- `validator?` (`modsValidatorName`): Validator name (defaults to `"truthy"`). +- `optional?` (`boolean`): Skip errors when `v` is `nil` (defaults to `false`). +- `msg?` (`string`): Optional override template passed to `mods.validate`. + +**Return**: + +- `none` (`nil`) + +**Example**: + +```lua +utils.validate({ "ctx", "users", 1, "name" }, nil, "string", true) +utils.validate({ "ctx", "users", 1, "name" }, 123, "string") +--> raises: ctx.users[1].name: expected string, got number +``` + +> [!NOTE] +> +> On failure, `path` is rendered with `mods.utils.keypath`. diff --git a/docs/api/validate.md b/docs/api/validate.md new file mode 100644 index 0000000..196af83 --- /dev/null +++ b/docs/api/validate.md @@ -0,0 +1,745 @@ +--- +description: "Validation helpers for Lua values and filesystem path types." +--- + +# `validate` + +Validation helpers for Lua values and filesystem path types. + +## Usage + +```lua +local validate = require "mods.validate" + +ok, err = validate.number("nope") --> false, "number expected, got string" +ok, err = validate(123, "number") --> true, nil +``` + +## `validate()` + +`validate(v, validator)` dispatches to the registered validator. If `validator` +is omitted, it defaults to `"truthy"`. + +```lua +validate() --> false, "truthy value expected, got no value" +validate(1) --> true, nil +validate(1, "nil") --> false, "nil expected, got number" +``` + +> [!IMPORTANT] +> +> Path checks require **LuaFileSystem** +> ([`lfs`](https://github.com/lunarmodules/luafilesystem)) and raise an error if +> it is not installed. + +## Validator Names + +Validator names are case-insensitive for field access. + +```lua +validate.number(1) --> true, nil +validate.NumBer(1) --> true, nil +``` + +`validator` in `validate(v, validator)` is matched as-is (case-sensitive): + +```lua +validate(1, "number") --> true, nil +validate(1, "NuMbEr") --> false, "NuMbEr expected, got number" +``` + +## Custom Messages + +Validator functions accept an optional template override as the second argument: +validate.number(v, "need {{expected}}, got {{got}}")`. + +You can also set `validate.messages.` to define default templates per +validator. + +```lua +validate.string(123, "want {{expected}}, got {{got}}") +--> false, "want string, got number" +``` + +## Functions + +**Path Checks**: + +| Function | Description | +| ------------------------------- | ------------------------------------------------------------------------------------------------------- | +| [`block(v, msg?)`](#fn-block) | Returns `true` when `v` is a block device path. Otherwise returns `false` and an error message. | +| [`char(v, msg?)`](#fn-char) | Returns `true` when `v` is a char device path. Otherwise returns `false` and an error message. | +| [`device(v, msg?)`](#fn-device) | Returns `true` when `v` is a block or char device path. Otherwise returns `false` and an error message. | +| [`dir(v, msg?)`](#fn-dir) | Returns `true` when `v` is a directory path. Otherwise returns `false` and an error message. | +| [`fifo(v, msg?)`](#fn-fifo) | Returns `true` when `v` is a FIFO path. Otherwise returns `false` and an error message. | +| [`file(v, msg?)`](#fn-file) | Returns `true` when `v` is a file path. Otherwise returns `false` and an error message. | +| [`link(v, msg?)`](#fn-link) | Returns `true` when `v` is a symlink path. Otherwise returns `false` and an error message. | +| [`path(v, msg?)`](#fn-path) | Returns `true` when `v` is a valid filesystem path. Otherwise returns `false` and an error message. | +| [`socket(v, msg?)`](#fn-socket) | Returns `true` when `v` is a socket path. Otherwise returns `false` and an error message. | + +**Registration**: + +| Function | Description | +| ------------------------------------------------------ | -------------------------------------------------- | +| [`register(name, validator, template?)`](#fn-register) | Register or override a validator function by name. | + +**Type Checks**: + +| Function | Description | +| ----------------------------------- | -------------------------------------------------------------------------------------------- | +| [`boolean(v, msg?)`](#fn-boolean) | Returns `true` when `v` is a boolean. Otherwise returns `false` and an error message. | +| [`function(v, msg?)`](#fn-function) | Returns `true` when `v` is a function. Otherwise returns `false` and an error message. | +| [`nil(v, msg?)`](#fn-nil) | Returns `true` when `v` is `nil`. Otherwise returns `false` and an error message. | +| [`number(v, msg?)`](#fn-number) | Returns `true` when `v` is a number. Otherwise returns `false` and an error message. | +| [`string(v, msg?)`](#fn-string) | Returns `true` when `v` is a string. Otherwise returns `false` and an error message. | +| [`table(v, msg?)`](#fn-table) | Returns `true` when `v` is a table. Otherwise returns `false` and an error message. | +| [`thread(v, msg?)`](#fn-thread) | Returns `true` when `v` is a thread. Otherwise returns `false` and an error message. | +| [`userdata(v, msg?)`](#fn-userdata) | Returns `true` when `v` is a userdata value. Otherwise returns `false` and an error message. | + +**Value Checks**: + +| Function | Description | +| ----------------------------------- | ------------------------------------------------------------------------------------------- | +| [`callable(v, msg?)`](#fn-callable) | Returns `true` when `v` is callable. Otherwise returns `false` and an error message. | +| [`false(v, msg?)`](#fn-false) | Returns `true` when `v` is exactly `false`. Otherwise returns `false` and an error message. | +| [`falsy(v, msg?)`](#fn-falsy) | Returns `true` when `v` is falsy. Otherwise returns `false` and an error message. | +| [`integer(v, msg?)`](#fn-integer) | Returns `true` when `v` is an integer. Otherwise returns `false` and an error message. | +| [`true(v, msg?)`](#fn-true) | Returns `true` when `v` is exactly `true`. Otherwise returns `false` and an error message. | +| [`truthy(v, msg?)`](#fn-truthy) | Returns `true` when `v` is truthy. Otherwise returns `false` and an error message. | + +### Path Checks + + + +#### `block(v, msg?)` + +Returns `true` when `v` is a block device path. Otherwise returns `false` and an +error message. + +**Parameters**: + +- `v` (`any`): Value to validate. +- `msg?` (`string`): Optional override template. + +**Return**: + +- `isValid` (`boolean`): Whether the check succeeds. +- `err` (`string?`): Error message when the check fails. + +**Example**: + +```lua +ok, err = validate.block(".") +``` + + + +#### `char(v, msg?)` + +Returns `true` when `v` is a char device path. Otherwise returns `false` and an +error message. + +**Parameters**: + +- `v` (`any`): Value to validate. +- `msg?` (`string`): Optional override template. + +**Return**: + +- `isValid` (`boolean`): Whether the check succeeds. +- `err` (`string?`): Error message when the check fails. + +**Example**: + +```lua +ok, err = validate.char(".") +``` + + + +#### `device(v, msg?)` + +Returns `true` when `v` is a block or char device path. Otherwise returns +`false` and an error message. + +**Parameters**: + +- `v` (`any`): Value to validate. +- `msg?` (`string`): Optional override template. + +**Return**: + +- `isValid` (`boolean`): Whether the check succeeds. +- `err` (`string?`): Error message when the check fails. + +**Example**: + +```lua +ok, err = validate.device(".") +``` + + + +#### `dir(v, msg?)` + +Returns `true` when `v` is a directory path. Otherwise returns `false` and an +error message. + +**Parameters**: + +- `v` (`any`): Value to validate. +- `msg?` (`string`): Optional override template. + +**Return**: + +- `isValid` (`boolean`): Whether the check succeeds. +- `err` (`string?`): Error message when the check fails. + +**Example**: + +```lua +ok, err = validate.dir(".") +``` + + + +#### `fifo(v, msg?)` + +Returns `true` when `v` is a FIFO path. Otherwise returns `false` and an error +message. + +**Parameters**: + +- `v` (`any`): Value to validate. +- `msg?` (`string`): Optional override template. + +**Return**: + +- `isValid` (`boolean`): Whether the check succeeds. +- `err` (`string?`): Error message when the check fails. + +**Example**: + +```lua +ok, err = validate.fifo(".") +``` + + + +#### `file(v, msg?)` + +Returns `true` when `v` is a file path. Otherwise returns `false` and an error +message. + +**Parameters**: + +- `v` (`any`): Value to validate. +- `msg?` (`string`): Optional override template. + +**Return**: + +- `isValid` (`boolean`): Whether the check succeeds. +- `err` (`string?`): Error message when the check fails. + +**Example**: + +```lua +ok, err = validate.file(".") +``` + + + +#### `link(v, msg?)` + +Returns `true` when `v` is a symlink path. Otherwise returns `false` and an +error message. + +**Parameters**: + +- `v` (`any`): Value to validate. +- `msg?` (`string`): Optional override template. + +**Return**: + +- `isValid` (`boolean`): Whether the check succeeds. +- `err` (`string?`): Error message when the check fails. + +**Example**: + +```lua +ok, err = validate.link(".") +``` + + + +#### `path(v, msg?)` + +Returns `true` when `v` is a valid filesystem path. Otherwise returns `false` +and an error message. + +**Parameters**: + +- `v` (`any`): Value to validate. +- `msg?` (`string`): Optional override template. + +**Return**: + +- `isValid` (`boolean`): Whether the check succeeds. +- `err` (`string?`): Error message when the check fails. + +**Example**: + +```lua +ok, err = validate.path("README.md") +``` + + + +#### `socket(v, msg?)` + +Returns `true` when `v` is a socket path. Otherwise returns `false` and an error +message. + +**Parameters**: + +- `v` (`any`): Value to validate. +- `msg?` (`string`): Optional override template. + +**Return**: + +- `isValid` (`boolean`): Whether the check succeeds. +- `err` (`string?`): Error message when the check fails. + +**Example**: + +```lua +ok, err = validate.socket(".") +``` + +### Registration + + + +#### `register(name, validator, template?)` + +Register or override a validator function by name. + +**Parameters**: + +- `name` (`string`): Validator name. +- `validator` (`fun(v:any):(ok:boolean)`): Validator function. +- `template?` (`string`): Optional default message template. + +**Return**: + +- `none` (`nil`) + +**Example**: + +```lua +validate.register("odd", function(v) + return type(v) == "number" and v % 2 == 1 +end, "{{value}} does not satisfy {{expected}}") + +ok, err = validate.odd(3) --> true, nil +ok, err = validate.odd("x") --> false, '"x" does not satisfy odd' +ok, err = validate(2, "odd") --> false, "2 does not satisfy odd" +``` + +> [!NOTE] +> +> - If `template` is provided, it becomes the default message template for that +> validator. +> - If `template` is omitted, failures use: +> `{{expected}} expected, got {{got}}`. + +### Type Checks + + + +#### `boolean(v, msg?)` + +Returns `true` when `v` is a boolean. Otherwise returns `false` and an error +message. + +**Parameters**: + +- `v` (`any`): Value to validate. +- `msg?` (`string`): Optional override template. + +**Return**: + +- `isValid` (`boolean`): Whether the check succeeds. +- `err` (`string?`): Error message when the check fails. + +**Example**: + +```lua +ok, err = validate.boolean(true) --> true, nil +ok, err = validate.boolean(1) --> false, "boolean expected, got number" +``` + + + +#### `function(v, msg?)` + +Returns `true` when `v` is a function. Otherwise returns `false` and an error +message. + +**Parameters**: + +- `v` (`any`): Value to validate. +- `msg?` (`string`): Optional override template. + +**Return**: + +- `isValid` (`boolean`): Whether the check succeeds. +- `err` (`string?`): Error message when the check fails. + +**Example**: + +```lua +ok, err = validate.Function(function() end) --> true, nil +ok, err = validate.Function(1) +--> false, "function expected, got number" +``` + + + +#### `nil(v, msg?)` + +Returns `true` when `v` is `nil`. Otherwise returns `false` and an error +message. + +**Parameters**: + +- `v` (`any`): Value to validate. +- `msg?` (`string`): Optional override template. + +**Return**: + +- `isValid` (`boolean`): Whether the check succeeds. +- `err` (`string?`): Error message when the check fails. + +**Example**: + +```lua +ok, err = validate.Nil(nil) --> true, nil +ok, err = validate.Nil(0) --> false, "nil expected, got number" +``` + + + +#### `number(v, msg?)` + +Returns `true` when `v` is a number. Otherwise returns `false` and an error +message. + +**Parameters**: + +- `v` (`any`): Value to validate. +- `msg?` (`string`): Optional override template. + +**Return**: + +- `isValid` (`boolean`): Whether the check succeeds. +- `err` (`string?`): Error message when the check fails. + +**Example**: + +```lua +ok, err = validate.number(42) --> true, nil +ok, err = validate.number("x") --> false, "number expected, got string" +``` + + + +#### `string(v, msg?)` + +Returns `true` when `v` is a string. Otherwise returns `false` and an error +message. + +**Parameters**: + +- `v` (`any`): Value to validate. +- `msg?` (`string`): Optional override template. + +**Return**: + +- `isValid` (`boolean`): Whether the check succeeds. +- `err` (`string?`): Error message when the check fails. + +**Example**: + +```lua +ok, err = validate.string("hello") --> true, nil +ok, err = validate.string(1) --> false, "string expected, got number" +``` + + + +#### `table(v, msg?)` + +Returns `true` when `v` is a table. Otherwise returns `false` and an error +message. + +**Parameters**: + +- `v` (`any`): Value to validate. +- `msg?` (`string`): Optional override template. + +**Return**: + +- `isValid` (`boolean`): Whether the check succeeds. +- `err` (`string?`): Error message when the check fails. + +**Example**: + +```lua +ok, err = validate.table({}) --> true, nil +ok, err = validate.table(1) --> false, "table expected, got number" +``` + + + +#### `thread(v, msg?)` + +Returns `true` when `v` is a thread. Otherwise returns `false` and an error +message. + +**Parameters**: + +- `v` (`any`): Value to validate. +- `msg?` (`string`): Optional override template. + +**Return**: + +- `isValid` (`boolean`): Whether the check succeeds. +- `err` (`string?`): Error message when the check fails. + +**Example**: + +```lua +co = coroutine.create(function() end) +ok, err = validate.thread(co) --> true, nil +ok, err = validate.thread(1) --> false, "thread expected, got number" +``` + + + +#### `userdata(v, msg?)` + +Returns `true` when `v` is a userdata value. Otherwise returns `false` and an +error message. + +**Parameters**: + +- `v` (`any`): Value to validate. +- `msg?` (`string`): Optional override template. + +**Return**: + +- `isValid` (`boolean`): Whether the check succeeds. +- `err` (`string?`): Error message when the check fails. + +**Example**: + +```lua +ok, err = validate.userdata(io.stdout) --> true, nil +ok, err = validate.userdata(1) --> false, "userdata expected, got number" +``` + +### Value Checks + + + +#### `callable(v, msg?)` + +Returns `true` when `v` is callable. Otherwise returns `false` and an error +message. + +**Parameters**: + +- `v` (`any`): Value to validate. +- `msg?` (`string`): Optional override template. + +**Return**: + +- `isValid` (`boolean`): Whether the check succeeds. +- `err` (`string?`): Error message when the check fails. + +**Example**: + +```lua +ok, err = validate.callable(type) --> true, nil +ok, err = validate.callable(1) --> false, "callable value expected, got 1" +``` + + + +#### `false(v, msg?)` + +Returns `true` when `v` is exactly `false`. Otherwise returns `false` and an +error message. + +**Parameters**: + +- `v` (`any`): Value to validate. +- `msg?` (`string`): Optional override template. + +**Return**: + +- `isValid` (`boolean`): Whether the check succeeds. +- `err` (`string?`): Error message when the check fails. + +**Example**: + +```lua +ok, err = validate.False(false) --> true, nil +ok, err = validate.False(true) --> false, "false value expected, got true" +``` + + + +#### `falsy(v, msg?)` + +Returns `true` when `v` is falsy. Otherwise returns `false` and an error +message. + +**Parameters**: + +- `v` (`any`): Value to validate. +- `msg?` (`string`): Optional override template. + +**Return**: + +- `isValid` (`boolean`): Whether the check succeeds. +- `err` (`string?`): Error message when the check fails. + +**Example**: + +```lua +ok, err = validate.falsy(false) --> true, nil +ok, err = validate.falsy(1) --> false, "falsy value expected, got 1" +``` + + + +#### `integer(v, msg?)` + +Returns `true` when `v` is an integer. Otherwise returns `false` and an error +message. + +**Parameters**: + +- `v` (`any`): Value to validate. +- `msg?` (`string`): Optional override template. + +**Return**: + +- `isValid` (`boolean`): Whether the check succeeds. +- `err` (`string?`): Error message when the check fails. + +**Example**: + +```lua +ok, err = validate.integer(1) --> true, nil +ok, err = validate.integer(1.5) --> false, "integer value expected, got 1.5" +``` + + + +#### `true(v, msg?)` + +Returns `true` when `v` is exactly `true`. Otherwise returns `false` and an +error message. + +**Parameters**: + +- `v` (`any`): Value to validate. +- `msg?` (`string`): Optional override template. + +**Return**: + +- `isValid` (`boolean`): Whether the check succeeds. +- `err` (`string?`): Error message when the check fails. + +**Example**: + +```lua +ok, err = validate.True(true) --> true, nil +ok, err = validate.True(false) --> false, "true value expected, got false" +``` + + + +#### `truthy(v, msg?)` + +Returns `true` when `v` is truthy. Otherwise returns `false` and an error +message. + +**Parameters**: + +- `v` (`any`): Value to validate. +- `msg?` (`string`): Optional override template. + +**Return**: + +- `isValid` (`boolean`): Whether the check succeeds. +- `err` (`string?`): Error message when the check fails. + +**Example**: + +```lua +ok, err = validate.truthy(1) --> true, nil +ok, err = validate.truthy(false) --> false, "truthy value expected, got false" +``` + +## Fields + + + +### `messages` (`modsValidatorMessages`) + +Custom error-message templates for validator failures. + +Set `validate.messages.`, where `` is a validator name (for example: +`number`, `truthy`, `file`). + +The error-message template is used only when validation fails and an error +message is returned. + +```lua +validate.messages.number = "need {{expected}}, got {{got}}" +ok, err = validate.number("x") --> false, "need number, got string" +``` + +**Placeholders**: + +- {{expected}}: The check target (for example `number`, + `string`, `truthy`). +- {{got}}: The detected failure kind (usually a Lua type; + path validators use `invalid path`). +- {{value}}: The passed value, formatted for display (strings + are quoted). + +> [!NOTE] +> +> When the passed value is `nil`, rendered value text uses `no value`. +> +> ```lua +> validate.messages.truthy = "{{expected}} value expected, got {{value}}" +> validate.truthy(nil) --> false, "truthy value expected, got no value" +> ``` + +**Default Messages**: + +- Type checks: {{expected}} expected, got {{got}} +- Value checks: {{expected}} value expected, got {{value}} +- Path checks: {{value}} is not a valid {{expected}} path + (for `path`: {{value}} is not a valid path) + +> [!NOTE] +> +> For path checks, if the value is not a `string`, the message falls back to +> `messages.string` (as if `validate.string` was called).