はじめに
先日、
という記事を書いたのですが、Windows には Linux の cal のような CLI カレンダーがないので、これを踏まえたカレンダーを作ってみたくなりました。
See also:
cal.exe
いきなりですが、ソースコードです。
program cal;
{$APPTYPE CONSOLE}
{$RTTI EXPLICIT METHODS([]) FIELDS([]) PROPERTIES([])}
{$WEAKLINKRTTI ON}
{$DEFINE USE_HIGHLIGHT}
uses
System.SysUtils, System.DateUtils;
const
CAL_WIDTH: array [Boolean] of Integer = (25, 32);
CAL_COLS: array [Boolean] of Integer = (3, 2);
CAL_DIGITS: array [Boolean] of Integer = (2, 3);
CAL_HEADERS: array [Boolean] of array [Boolean] of string =
(('Wk| Su Mo Tu We Th Fr Sa', 'Wk| Sun Mon Tue Wed Thu Fri Sat'),
('Wk| Mo Tu We Th Fr Sa Su', 'Wk| Mon Tue Wed Thu Fri Sat Sun'));
type
TCal = array [0..8] of string;
var
CalArr: array of TCal;
MonthArr: array of Boolean;
function WeekOf_US(const AValue: TDateTime): Word;
begin
var BaseDate: TDateTime := StartOfTheYear(AValue);
var TargetDate := BaseDate - (BaseDate.DayOfTheWeek mod 7);
Result := Succ(Trunc(AValue - TargetDate) div 7);
end; { WeekOf_US }
function GenerateCalendar(const ADate: TDateTime; const IsISO, IsSN, ShowToday: Boolean): TCal;
var
_WeekOf: function (const AValue: TDateTime): Word;
_DayOfWeek: function (const DateTime: TDateTime): Word;
begin
var dCal := Default(TCal);
var TargetDate := StartOfTheMonth(ADate);
var DIM := DaysInMonth(TargetDate);
var Line := 0;
dCal[Line] := StringOfChar(' ', (CAL_WIDTH[IsSN] - 7) div 2) +
FormatDateTime('YYYY/MM', TargetDate); Inc(Line);
if IsISO then
begin
_WeekOf := WeekOf;
_DayOfWeek := DayOfTheWeek;
end
else
begin
_WeekOf := WeekOf_US;
_DayOfWeek := DayOfWeek;
end;
dCal[Line] := CAL_HEADERS[IsISO][IsSN]; Inc(Line);
dCal[Line] := StringOfChar('-', CAL_WIDTH[IsSN]); Inc(Line);
var SN := 0;
if IsSN then
SN := Trunc(StartOfTheMonth(ADate) - StartOfTheYear(ADate));
var StrFmt := '%' + CAL_DIGITS[IsSN].ToString + 'd';
var Day := 1;
var StartDoW := _DayOfWeek(TargetDate);
var HasToday := ShowToday and (ADate.GetYear = Today.GetYear) and (ADate.GetMonth = Today.GetMonth);
var BoW := TargetDate;
repeat
var LineStr := Format('%2d|', [_WeekOf(BoW)]);
LineStr := LineStr + StringOfChar(' ', Pred(StartDoW) * Succ(CAL_DIGITS[IsSN]));
var Count := 0;
for var i:= StartDoW to 7 do
begin
var sDay := Format(StrFmt, [SN + Day + Count]);
if HasToday and (Day + Count = Today.GetDay) then
begin
sDay := '[' + sDay + ']'; // Today marker
HasToday := False;
end
else if LineStr[Length(LineStr)] <> ']'then
sDay := ' ' + sDay;
LineStr := LineStr + sDay;
Inc(Count);
if (Day + Count) > DIM then
Break;
end;
Inc(Day, Count);
BoW := BoW + Count;
StartDoW := 1;
dCal[Line] := LineStr; Inc(Line);
until Day > DIM;
Result := dCal;
end; { PrintCalendar }
begin
// コマンドラインの処理
if FindCmdLineSwitch('?') then
begin
Writeln('Usage: ',
ChangeFileExt(ExtractFileName(ParamStr(0)), ''),
' [-3] [-h] [-j] [-m] [-y] [year [month]]');
Exit;
end;
var PDate: TDateTime := Today;
var dYear := 0;
var dMonth := 0;
var UseISO := FindCmdLineSwitch('m');
var UseSN := FindCmdLineSwitch('j');
var ShowToday := not FindCmdLineSwitch('h');
var Months := 1;
if FindCmdLineSwitch('3') then
Months := 3;
if FindCmdLineSwitch('y') then
Months := 12;
for var i:=1 to ParamCount do
begin
var sw := ParamStr(i);
if CharInSet(sw[1], ['0'..'9']) then
begin
var v := StrToIntDef(sw, 0);
if v > 0 then
begin
if dYear = 0 then
dYear := v
else if v in [1..12] then
dMonth := v;
end;
if dMonth > 0 then
Break;
end;
end;
if dYear > 0 then
begin
if dMonth = 0 then
begin
Months := 12;
dMonth := 1;
end;
PDate := EncodeDate(dYear, dMonth, 1);
end;
// カレンダーの生成
SetLength(CalArr, Months);
SetLength(MonthArr, Months);
var TargetDate: TDateTime;
case Months of
12: TargetDate := StartOfTheYear(PDate);
3: TargetDate := StartOfTheMonth(PDate).IncMonth(-1);
else
TargetDate := StartOfTheMonth(PDate);
end;
for var i:=1 to Months do
begin
CalArr[Pred(i)] := GenerateCalendar(TargetDate, UseISO, UseSN, ShowToday);
MonthArr[Pred(i)] := TargetDate = StartOfTheMonth(Today);
TargetDate := IncMonth(TargetDate);
end;
// カレンダーの表示
var Cols := CAL_COLS[UseSN];
var ll := Pred(Months) div Cols;
var idx := 0;
var Width := CAL_WIDTH[UseSN];
for var l:=0 to ll do
begin
var vCal := Default(TCal);
var ii := Cols-1;
for var i:=0 to ii do
begin
if idx >= Months then
Break;
for var j:=Low(vCal) to High(vCal) do
begin
var s := CalArr[idx][j];
s := Copy(s + StringOfChar(' ', Width), 1, Width);
{$IFDEF USE_HIGHLIGHT}
if ShowToday and MonthArr[idx] and (j > 2) then
begin
if Pos('[', s) > 0 then
begin
s := StringReplace(s, '[', ' ' + #$1B'[7m', []);
s := StringReplace(s, ']', #$1B'[0m' + ' ', []);
end;
end;
{$ENDIF}
vCal[j] := vCal[j] + s;
if i < ii then
vCal[j] := vCal[j] + ' ';
end;
Inc(idx);
end;
for var i := Low(vCal) to High(vCal) do
Writeln(vCal[i]);
if l < ll then
Writeln;
end;
end.
Delphi 13 Florence で作成しましたが、Community Edition でもコンパイルできると思います。
使い方
普通に実行するとこうなります (2026/02/06 実行)。
今月のカレンダーが表示され、今日がハイライトされていますね。Linux のカレンダーと異なるのは週番号が表示される事です。
・ISO 8601 準拠
-m オプションを付けて実行すると、ISO 8601 準拠となり、週が月曜始まりとなります。週番号の付き方も異なります。
・3 ヵ月表示 (当年)
-3 を指定すると当月を中心に 3 ヵ月分表示されます。
・1 年表示 (当年)
-y を指定すると当年 12 ヵ月分が表示されます。
・1 年表示 (年指定)
表示年度を変えるには年を指定します。例えば 2028 年を指定するには
cal 2028
とします。他のスイッチも同時に使えますが、-3 スイッチは無視されます。
・年月指定
表示年月を変えるには年と月を指定します。例えば 2028 年 6 月を指定するには
cal 2028 6
とします。
Linux の cal の年月指定 (mm yyyy) とは順番が逆になっています。日本だとこちらの指定の方が自然だと思うので変更してみました。
MSX-DOS2 Tools 付属の cal.com も yyyy/mm 指定でした。
・年間通算日
-j を指定すると、日付ではなく、その年の 1/1 からの通算日表示になります。
表示幅が広くなるため -3 や -y では 2 列表示になります。
・今日の表示
-h を指定すると今日がハイライトされなくなります。
・スイッチ一覧
-? を指定するとスイッチ一覧が表示されます。
おわりに
当日のハイライト表示に ANSI エスケープコード を使っているので、Windows Terminal ではなく素のコマンドプロンプトを使うと表示が化けます。
ハイライト表示にコンソール API を使うよう改変するのはそんなに難しくないと思いますが、とりあえずハイライト表示をやめたいのなら {$DEFINE USE_HIGHLIGHT} をコメントアウトするか削除してください。
- {$DEFINE USE_HIGHLIGHT}
+ {.$DEFINE USE_HIGHLIGHT}
当日がハイライトされる代わりに日付が [ ] で括られて表示されます。
uses に System.SysUtils と System.DateUtils しかないので macOS や Linux 向けにもビルドできると思うのですが、これらには cal があるので、あまり意味はないですよね。
エスケープシーケンスで座標指定してカレンダーを書くようにすれば、処理が随分スッキリすると思います。むしろ Today の処理を抜くだけでかなりスッキリするのですが...。
See also:












