サンプル・プログラム
文字カウント・クラス
53: // 文字カウント・クラス ====================================================
54: class countKanji {
55:
56: /**
57: * コンストラクタ
58: * @param String text テキスト
59: * @return なし
60: */
61: constructor(text) {
62: //プロパティ
63: this.text = text; //テキスト
64: this.moji = 0; //文字数
65: this.kanji = 0; //漢字の数
66: this.kuten = 0; //句点の数
67: this.toten = 0; //読点の数
68:
69: //文字数をカウントする
70: for (let i = 0; i < this.text.length; i++) {
71: let ch = this.text.substr(i, 1);
72: if (ch.match(/[一-龠]+/g)) {
73: this.moji++;
74: this.kanji++;
75: } else if (ch.match(/[。。..]+/g)) {
76: this.kuten++;
77: } else if (ch.match(/[、、,,]+/g)) {
78: this.toten++;
79: } else if (! ch.match(/[\n\r]+/g)) {
80: this.moji++;
81: }
82: }
83: }
84: }
85: // End of Class ============================================================
constructorは、オブジェクトが生成されたときに一度だけ実行される特殊なメソッドで、そのオブジェクトの初期化などに用いる。メソッドの記述方法は、関数と同じである。引数を渡すことができる。constructor の中で this.名前 で示された変数はプロパティになる。
ここでは、constructor の中で、プロパティとして文字数、漢字の数、句点の数、読点の数を、それぞれ代入している。漢字、句点、読点文字の識別に正規表現を用いている。
countKanji クラスは、constructor 以外のメソッドは持っていない。
114: /**
115: * 文字数等をカウントして表示する.
116: * @param なし
117: * @return なし
118: */
119: function countCharacters() {
120: //文字数カウント
121: let ck = new countKanji(document.getElementById('sour').value)
122:
123: //結果を表示
124: document.getElementById('moji').innerHTML = ck.moji.toLocaleString() + '文字';
125: document.getElementById('kanji').innerHTML = (ck.kanji / ck.moji * 100).toLocaleString() + '%';
126: document.getElementById('sentenceLength').innerHTML = (ck.moji / ck.kuten).toLocaleString() + '文字';
127: document.getElementById('phraseLength').innerHTML = (ck.moji / (ck.kuten + ck.toten)).toLocaleString() + '文字';
128: }
プロパティの参照は、JavaScriptに備わっているオブジェクトと同様、オブジェクト名.プロパティ名という名前で呼び出す。
1ヵ月カレンダーを表示する
12: // カレンダー・クラス ======================================================
13: class pahooCalendar {
14:
15: /**
16: * コンストラクタ
17: * @param Number year, month, day 年月日
18: * @return なし
19: */
20: constructor(year, month, day) {
21: //プロパティ
22: this.year = year; //西暦年
23: this.month = month; //月
24: this.day = day; //日
25: }
26:
27: /**
28: * うるう年かどうかを判定する
29: * @param なし
30: * @return Boolean true:うるう年/false:平年
31: */
32: isleap() {
33: let ret; //戻り値
34:
35: if (this.year % 400 === 0) {
36: ret = true;
37: } else if (this.year % 100 === 0) {
38: ret = false;
39: } else if (this.year % 4 === 0) {
40: ret = true;
41: } else {
42: ret = false;
43: }
44:
45: return ret;
46: }
47:
48: /**
49: * 指定した月の日数を返す
50: * @param なし
51: * @return Number 月の日数/(-1):引数エラー
52: */
53: getDaysInMonth() {
54: //引数のバリデーション
55: if (! Number.isInteger(this.year) || ! Number.isInteger(this.month) ||
56: this.month < 1 || this.month > 12) {
57: return (-1);
58: }
59:
60: //月の日数
61: let days = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
62:
63: //うるう年判定
64: days[2] = this.isleap(this.year) ? 29 : 28;
65:
66: return days[this.month];
67: }
68:
69: /**
70: * 曜日番号を求める(ツェラーの公式)
71: * @param なし
72: * @return Number 曜日番号(0:日曜日, 1:月曜日...6:土曜日)/(-1):引数エラー
73: */
74: getWeekNumber() {
75: let year = this.year;
76: let month = this.month;
77: let day = this.day;
78:
79: //引数のバリデーション
80: if (! Number.isInteger(year) || ! Number.isInteger(month) ||
81: ! Number.isInteger(day) ||
82: month < 1 || month > 12) {
83: return (-1);
84: }
85: if (day < 1 || day > this.getDaysInMonth(year, month)) {
86: return (-1);
87: }
88:
89: //ツェラーの公式の変形
90: if (month <= 2) {
91: month += 12;
92: year--;
93: }
94: let c = Math.floor(year / 100);
95: let y = year % 100;
96: let h = (5 * c + y + Math.floor(y / 4) + Math.floor(c / 4) + Math.floor(26 * (month + 1) / 10) + day - 1) % 7;
97:
98: return h;
99: }
100:
101: }
102: // End of Class ======================================================
また、constructor に、あらかじめ年、月、日を渡すことで、メソッドを呼ぶ都度、いちいち年や月を引数と渡す手間を省いた。
117: /**
118: * 1ヵ月カレンダーを表示する.
119: * @param なし
120: * @return なし
121: */
122: function printMonthlyCalendar() {
123: //表示クリア
124: document.getElementById('error').innerHTML = '';
125: document.getElementById('calendar').innerHTML = '';
126:
127: //年月を取得
128: let year = parseInt(document.getElementById('year').value, 10);
129: let month = parseInt(document.getElementById('month').value, 10);
130:
131: //カレンダー・オブジェクトを生成する.
132: let pcd = new pahooCalendar(year, month, 1);
133:
134: //月の日数,1日の曜日番号を取得する
135: let daysInMonth = pcd.getDaysInMonth();
136: let startWeekNumber = pcd.getWeekNumber(year, month, 1);
137:
138: //バリデーション
139: if ((daysInMonth == (-1)) || (startWeekNumber == (-1))) {
140: //エラーを表示
141: document.getElementById('error').innerHTML = 'エラー:正しい月を入力してください.';
142:
143: //カレンダーの作成と表示.
144: } else {
145: document.getElementById('calendar').innerHTML = makeCalendar(daysInMonth, startWeekNumber);
146: }
147:
148: //カレンダー・オブジェクトを解放する.
149: pcd = null;
150: }
ここでは、pahooCalendar クラスを別ファイル "pahooCalendar1.js" にした。
下記のようにHTML中で "pahooCalendar1.js" と呼び出すことで、他のプログラムでも容易に再利用できる。
1ヵ月カレンダーをTABLEタグとして返す
57: /**
58: * 月の日数と1日の曜日番号を指定し,1ヵ月カレンダーをTABLEタグとして返す.
59: * @param Number daysInMonth 月の日数
60: * @param Number startWeekNumber 1日の曜日番号
61: * @return String TABLEタグ
62: */
63: function makeCalendar(daysInMonth, startWeekNumber) {
64: //カレンダーの行列サイズ
65: const MAX_ROW = 6;
66: const MAX_COL = 7;
67: //曜日の種類
68: const SUNDAY = 'sunday';
69: const SATURDAY = 'saturday';
70: const WEEKDAY = 'weekday';
71: //曜日表
72: const WEEK_LIST = [SUNDAY, WEEKDAY, WEEKDAY, WEEKDAY, WEEKDAY, WEEKDAY, SATURDAY];
73:
74: //カレンダー配列を準備する.
75: let calendarArray = newArray2d(MAX_ROW, MAX_COL, 0);
76:
77: //カレンダーの1行目
78: let html =`
79: <tr>
80: <th class="sunday">日</th>
81: <th class="weekday">月</th>
82: <th class="weekday">火</th>
83: <th class="weekday">水</th>
84: <th class="weekday">木</th>
85: <th class="weekday">金</th>
86: <th class="saturday">土</th>
87: </tr>
88: `;
89:
90: //カレンダー配列に日付を格納する.
91: for (let day = 1, row = 0, col = startWeekNumber; day <= daysInMonth; day++, col++) {
92: //週の終わりに達したら,次の週へ
93: if (col >= MAX_COL) {
94: col = 0;
95: row++;
96: }
97: //日付を代入
98: calendarArray[row][col] = day;
99: }
100:
101: //カレンダー配列をTABLEタグに展開する.
102: calendarArray.forEach (function (rows, row) {
103: if ((row > 0) && (rows[0] == 0)) return; //空行ならスキップ
104: //1週間分を展開する.
105: html +="<tr>";
106: rows.forEach (function(val, weekNumber) {
107: html += '<td class="' + WEEK_LIST[weekNumber] +'">';
108: html += (val > 0) ? val : ' ';
109: html += '</td>';
110: });
111: html += "</tr>";
112: });
113:
114: return html;
115: }
2次元配列を初期化する
37: /**
38: * 2次元配列を用意し,初期化する.
39: * @param Number rows 行数
40: * @param Number cols 列数
41: * @param Number val 初期値
42: * @return Array 2次元配列
43: */
44: function newArray2d(rows, cols, val=0) {
45: /** これと同じ作用をする.
46: * let arr = new Array(rows);
47: * for (let i = 0; i < rows; i++) {
48: * arr[i] = new Array(cols);
49: * for (let j = 0; j < cols; j++) {
50: * arr[i][j] = val;
51: * }
52: * }
53: */
54: return Array.from(new Array(rows), _ => new Array(cols).fill(val));
55: }
IE対応
「2.1 変数と定数」で、JavaScript ES6(ES2015) に則ると宣言した。ChromeやSafariなどのモダンブラウザであれば、それでいいのだが、世の中にはまだ IE11 ユーザーが多い。そして、IE11 では JavaScript ES6(ES2015) に準拠していない。
IEでは class宣言 をはじめ、いくつかの命令が備わっておらず、これらを代替しつつ、モダンブラウザでも動作する手法を紹介しておくことにする。
12: //IE用isInteger
13: Number.isInteger = Number.isInteger ||
14: function (n) {
15: return (typeof n === 'number') && isFinite(n) && (Math.floor(n) === n);
16: };
IEは Number.isInteger をサポートしていない。そこで、代替のメソッドを用意した。
JavaScriptでは、すでにあるメソッドを上書き(オーバーライド)できる。
これを利用し、Number.isIntegerが存在すれば、そのままオーバーライドする。無ければ(||演算子)、function以下の処理をメソッドとしてオーバーライドする。
まず、typeof演算子 を使って引数がNumber型であること。なおかつ、isFinite 関数を使ってInfinity、NaN、undefinedのいずれでもないこと。なおかつ、floor メソッドを使って、n以下の最大の整数がn自身であること。これらが揃えば整数であることは自明なので、trueを返す。
37: /**
38: * 2次元配列を用意し,初期化する.
39: * @param Number rows 行数
40: * @param Number cols 列数
41: * @param Number val 初期値
42: * @return Array 2次元配列
43: */
44: function newArray2d(rows, cols, val) {
45: let arr = new Array(rows);
46: for (let i = 0; i < rows; i++) {
47: arr[i] = new Array(cols);
48: for (let j = 0; j < cols; j++) {
49: arr[i][j] = val;
50: }
51: }
52: return arr;
53: }
最後に――これが一番大きな変更点だが――IEは class宣言 が使えない。そこで、代替手段として、グローバル空間で無名関数に名前(オブジェクト名)を与える処理をクラスに見立てる。この部分がコンストラクタになる。
18: /**
19: * コンストラクタ
20: * @param Number year, month, day 年月日
21: * @return なし
22: */
23: pahooCalendar = function (year, month, day) {
24: this.year = year;
25: this.month = month;
26: this.day = day;
27: }
29: //プロパティ
30: pahooCalendar.prototype.year = 0; //西暦年
31: pahooCalendar.prototype.month = 0; //月
32: pahooCalendar.prototype.day = 0; //日
33:
34: /**
35: * うるう年かどうかを判定する
36: * @param なし
37: * @return Boolean true:うるう年/false:平年
38: */
39: pahooCalendar.prototype.isleap = function () {
40: let ret; //戻り値
41:
42: if (this.year % 400 === 0) {
43: ret = true;
44: } else if (this.year % 100 === 0) {
45: ret = false;
46: } else if (this.year % 4 === 0) {
47: ret = true;
48: } else {
49: ret = false;
50: }
51:
52: return ret;
53: }
119: //カレンダー・オブジェクトを生成する.
120: let pcd = new pahooCalendar(year, month, 1);
カプセル化
どのようなポリシーでカプセル化するかは、複数のプログラムを作ってみないと決まらない。複数のプログラムで繰り返し使用するような処理は、カプセル化することで開発作業を省力化できる。会社によっては、自社でカプセル化したパッケージを持っている。
まずはユーザー関数として定義しておき、次のプログラムを作るときにメソッドにするかどうか考えればいいだろう。
逆に言うと、そのプログラム内は何回も呼び出すが、他のプログラムで利用しないような処理はユーザー定義関数のままで十分である。
なお、少なくともHTML文とデータの受け渡しをするような処理はカプセル化には相応しくない。HTML文のid名はファイルによって変わるものだから、カプセル化に馴染まない。
今回のように、データ受け渡し処理はユーザー定義関数に任せ、そこからクラスを呼び出すようにするのが無難だ。
コラム:何をカプセル化するか
ソフト開発会社によってはルールを決めているところもあるし、そうでない場合は、プロジェクトやチームでルール決めをしておいた方がいいだろう。
たとえば演算結果が単純な整数ではないもの――球面三角法がこれに当たる。Mathオブジェクトの三角関数は、単位がラジアンであるが、地図座標(緯度・経度)は度分秒に変換した方が扱いが楽である。また、経度は0以上360未満だが、緯度は-90度以上+90度以下という違いもある。
Mathオブジェクトを継承し、こうした系に即したメソッドを用意するのが無難である。
日付(年月日)や時刻(時分秒)も独特な系である。Dateオブジェクトを継承し、旧暦計算をメソッドとして加えるといいだろう。
なお、継承については、次回説明する。
クラスを別ファイルにすることで、再利用や保守が容易になる。このように、処理を再利用や保守が容易になるように分離することをカプセル化と呼ぶ。