Skip to content
This repository has been archived by the owner on Apr 13, 2024. It is now read-only.

Commit

Permalink
Merge pull request #70 from AtkinsSJ/date-builtin
Browse files Browse the repository at this point in the history
Implement a basic `date` builtin
  • Loading branch information
KernelDeimos authored Mar 29, 2024
2 parents 3302c66 + 0bd881a commit 0b378bd
Show file tree
Hide file tree
Showing 2 changed files with 313 additions and 0 deletions.
2 changes: 2 additions & 0 deletions src/puter-shell/coreutils/__exports__.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import module_cd from './cd.js'
import module_changelog from './changelog.js'
import module_clear from './clear.js'
import module_cp from './cp.js'
import module_date from './date.js'
import module_dcall from './dcall.js'
import module_dirname from './dirname.js'
import module_echo from './echo.js'
Expand Down Expand Up @@ -65,6 +66,7 @@ export default {
"changelog": module_changelog,
"clear": module_clear,
"cp": module_cp,
"date": module_date,
"dcall": module_dcall,
"dirname": module_dirname,
"echo": module_echo,
Expand Down
311 changes: 311 additions & 0 deletions src/puter-shell/coreutils/date.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,311 @@
/*
* Copyright (C) 2024 Puter Technologies Inc.
*
* This file is part of Phoenix Shell.
*
* Phoenix Shell is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Exit } from './coreutil_lib/exit.js';

// "When no formatting operand is specified, the output in the POSIX locale shall be equivalent to specifying:"
const DEFAULT_FORMAT = '+%a %b %e %H:%M:%S %Z %Y';

function padStart(number, length, padChar) {
let string = number.toString();
if ( string.length >= length ) {
return string;
}

return padChar.repeat(length - string.length) + string;
}

function highlight(text) {
return `\x1B[92m${text}\x1B[0m`;
}

export default {
name: 'date',
usage: 'date [OPTIONS] [+FORMAT]',
description: 'Print the system date and time\n\n' +
'If FORMAT is provided, it controls the date format used.',
helpSections: {
'Format Sequences': 'The following format sequences are understood:\n\n' +
` ${highlight('%a')} Weekday name, abbreviated.\n` +
` ${highlight('%A')} Weekday name\n` +
` ${highlight('%b')} Month name, abbreviated\n` +
` ${highlight('%B')} Month name\n` +
` ${highlight('%c')} Default date and time representation\n` +
` ${highlight('%C')} Century, 2 digits padded with '0'\n` +
` ${highlight('%d')} Day of the month, 2 digits padded with '0'\n` +
` ${highlight('%D')} Date in the format mm/dd/yy\n` +
` ${highlight('%e')} Day of the month, 2 characters padded with leading spaces\n` +
` ${highlight('%h')} Same as ${highlight('%b')}\n` +
` ${highlight('%H')} Hour (24-hour clock), 2 digits padded with '0'\n` +
` ${highlight('%I')} Hour (12-hour clock), 2 digits padded with '0'\n` +
// ` ${highlight('%j')} TODO: Day of the year, 3 digits padded with '0'\n` +
` ${highlight('%m')} Month, 2 digits padded with '0', with January = 01\n` +
` ${highlight('%M')} Minutes, 2 digits padded with '0'\n` +
` ${highlight('%n')} A newline character\n` +
` ${highlight('%p')} AM or PM\n` +
` ${highlight('%r')} Time (12-hour clock) with AM/PM, as 'HH:MM:SS AM/PM'\n` +
` ${highlight('%S')} Seconds, 2 digits padded with '0'\n` +
` ${highlight('%t')} A tab character\n` +
` ${highlight('%T')} Time (24-hour clock), as 'HH:MM:SS'\n` +
` ${highlight('%u')} Weekday as a number, with Monday = 1 and Sunday = 7\n` +
// ` ${highlight('%U')} TODO: Week of the year (Sunday as the first day of the week) as a decimal number [00,53]. All days in a new year preceding the first Sunday shall be considered to be in week 0.\n` +
// ` ${highlight('%V')} TODO: Week of the year (Monday as the first day of the week) as a decimal number [01,53]. If the week containing January 1 has four or more days in the new year, then it shall be considered week 1; otherwise, it shall be the last week of the previous year, and the next week shall be week 1.\n` +
` ${highlight('%w')} Weekday as a number, with Sunday = 0\n` +
// ` ${highlight('%W')} TODO: Week of the year (Monday as the first day of the week) as a decimal number [00,53]. All days in a new year preceding the first Monday shall be considered to be in week 0.\n` +
` ${highlight('%x')} Default date representation\n` +
` ${highlight('%X')} Default time representation\n` +
` ${highlight('%y')} Year within century, 2 digits padded with '0'\n` +
` ${highlight('%Y')} Year\n` +
` ${highlight('%Z')} Timezone name, if it can be determined\n` +
` ${highlight('%%')} A percent sign\n`
},
args: {
$: 'simple-parser',
allowPositionals: true,
},
execute: async ctx => {
const { out, err } = ctx.externs;
const { positionals } = ctx.locals;

if ( positionals.length > 1 ) {
await err.write('date: Too many arguments\n');
throw new Exit(1);
}

let format = positionals.shift() ?? DEFAULT_FORMAT;

if ( ! format.startsWith('+') ) {
await err.write('date: Format does not begin with `+`\n');
throw new Exit(1);
}
format = format.substring(1);

// TODO: Should we use the server time instead? Maybe put that behind an option.
const date = new Date();
const locale = 'en-US'; // TODO: POSIX: Pull this from the user's settings.

let output = '';
for (let i = 0; i < format.length; i++) {
let char = format[i];
if ( char === '%' ) {
char = format[++i];
switch (char) {
// "Locale's abbreviated weekday name."
case 'a': {
output += date.toLocaleDateString(locale, { weekday: 'short' });
break;
}

// "Locale's full weekday name."
case 'A': {
output += date.toLocaleDateString(locale, { weekday: 'long' });
break;
}

// "Locale's abbreviated month name."
case 'b':
// "A synonym for %b."
case 'h': {
output += date.toLocaleDateString(locale, { month: 'short' });
break;
}

// "Locale's full month name."
case 'B': {
output += date.toLocaleDateString(locale, { month: 'long' });
break;
}

// "Locale's appropriate date and time representation."
case 'c': {
output += date.toLocaleString(locale);
break;
}

// "Century (a year divided by 100 and truncated to an integer) as a decimal number [00,99]."
case 'C': {
output += Math.trunc(date.getFullYear() / 100);
break;
}

// "Day of the month as a decimal number [01,31]."
case 'd': {
output += padStart(date.getDate(), 2, '0');
break;
}

// "Date in the format mm/dd/yy."
case 'D': {
const month = padStart(date.getMonth() + 1, 2, '0');
const day = padStart(date.getDate(), 2, '0');
const year = padStart(date.getFullYear() % 100, 2, '0');
output += `${month}/${day}/${year}`;
break;
}

// "Day of the month as a decimal number [1,31] in a two-digit field with leading <space>
// character fill."
case 'e': {
output += padStart(date.getDate(), 2, ' ');
break;
}

// "Hour (24-hour clock) as a decimal number [00,23]."
case 'H': {
output += padStart(date.getHours(), 2, '0');
break;
}

// "Hour (12-hour clock) as a decimal number [01,12]."
case 'I': {
output += padStart((date.getHours() % 12) || 12, 2, '0');
break;
}

// TODO: "Day of the year as a decimal number [001,366]."
case 'j': break;

// "Month as a decimal number [01,12]."
case 'm': {
// getMonth() starts at 0 for January
output += padStart(date.getMonth() + 1, 2, '0');
break;
}

// "Minute as a decimal number [00,59]."
case 'M': {
output += padStart(date.getMinutes(), 2, '0');
break;
}

// "A <newline>."
case 'n': output += '\n'; break;

// "Locale's equivalent of either AM or PM."
case 'p': {
// TODO: We should access this from the locale.
output += date.getHours() < 12 ? 'AM' : 'PM';
break;
}

// "12-hour clock time [01,12] using the AM/PM notation; in the POSIX locale, this shall be
// equivalent to %I : %M : %S %p."
case 'r': {
const rawHours = date.getHours();
const hours = padStart((rawHours % 12) || 12, 2, '0');
// TODO: We should access this from the locale.
const am_pm = rawHours < 12 ? 'AM' : 'PM';
const minutes = padStart(date.getMinutes(), 2, '0');
const seconds = padStart(date.getSeconds(), 2, '0');
output += `${hours}:${minutes}:${seconds} ${am_pm}`;
break;
}

// "Seconds as a decimal number [00,60]."
case 'S': {
output += padStart(date.getSeconds(), 2, '0');
break;
}

// "A <tab>."
case 't': output += '\t'; break;

// "24-hour clock time [00,23] in the format HH:MM:SS."
case 'T': {
const hours = padStart(date.getHours(), 2, '0');
const minutes = padStart(date.getMinutes(), 2, '0');
const seconds = padStart(date.getSeconds(), 2, '0');
output += `${hours}:${minutes}:${seconds}`;
break;
}

// "Weekday as a decimal number [1,7] (1=Monday)."
case 'u': {
// getDay() returns 0 for Sunday
output += date.getDay() || 7;
break;
}

// TODO: "Week of the year (Sunday as the first day of the week) as a decimal number [00,53].
// All days in a new year preceding the first Sunday shall be considered to be in week 0."
case 'U': break;

// TODO: "Week of the year (Monday as the first day of the week) as a decimal number [01,53].
// If the week containing January 1 has four or more days in the new year, then it shall be
// considered week 1; otherwise, it shall be the last week of the previous year, and the next
// week shall be week 1."
case 'V': break;

// "Weekday as a decimal number [0,6] (0=Sunday)."
case 'w': {
output += date.getDay();
break;
}

// TODO: "Week of the year (Monday as the first day of the week) as a decimal number [00,53].
// All days in a new year preceding the first Monday shall be considered to be in week 0."
case 'W': break;

// "Locale's appropriate date representation."
case 'x': {
output += date.toLocaleDateString(locale);
break;
}

// "Locale's appropriate time representation."
case 'X': {
output += date.toLocaleTimeString(locale);
break;
}

// "Year within century [00,99]."
case 'y': {
output += date.getFullYear() % 100;
break;
}

// "Year with century as a decimal number."
case 'Y': {
output += date.getFullYear();
break;
}

// "Timezone name, or no characters if no timezone is determinable."
case 'Z': {
output += date.toLocaleDateString(locale, { timeZoneName: 'short' });
break;
}

// "A <percent-sign> character."
case '%': output += '%'; break;

// We reached the end of the string, just output the %.
case undefined: output += '%'; break;

// If nothing matched, just output the input verbatim
default: output += '%' + char; break;
}
continue;
}
output += char;
}
output += '\n';

await out.write(output);
}
};

0 comments on commit 0b378bd

Please sign in to comment.