概要
フロントエンド勉強会
プログラミング問題を解きながらTypeScriptに慣れ親しむことを目指す。
環境構築
npm init -y
npm install typescript ts-node
npx tsc --init
touch main.ts
実行
npx ts-node main.ts
問題
1. FizzBuzz
inputsに半角区切りで正の整数が与えられる。
各自然数において、
- 3の倍数ならFizz
- 5の倍数ならBuzz
- 3の倍数かつ5の倍数ならFizzBuzz
- いずれの条件にも当てはまらなければ数字そのまま
を出力する。
const inputs = '';
const arr = inputs.trim().split(' ').map((n) => parseInt(n));
function fizzBuzz(n: number): string {
if (n % 15 === 0) {
return 'FizzBuzz';
} else if (n % 3 === 0) {
return 'Fizz';
} else if (n % 5 === 0) {
return 'Buzz';
}
return `${n}`;
}
arr.forEach((n: number) =>
console.log(fizzBuzz(n))
);
ワンライナー
const inputs = '';
const fizzBuzz = (n: number): string => (['', 'Fizz'][Number(n % 3 === 0)] + ['', 'Buzz'][Number(n % 5 === 0)]) || ('' + n);
inputs.trim().split(' ').forEach((n: number) =>
console.log(fizzBuzz(+n))
);
['', 'Fizz'][Number(n % 3 === 0)]
n%3===0
はboolean型。
boolean型をNumberに通すと、
Number(true) // 1
Number(false) // 0
3の倍数なら ['', 'Fizz'][1] = 'Fizz'
3の倍数でないなら ['', 'Fizz'][0] = ''
となる。
5の倍数の箇所 ['', 'Buzz'][Number(n % 5 === 0)]
も同様。
これで、
3の倍数なら 'Fizz' + '' = 'Fizz'
5の倍数なら '' + 'Buzz' = 'Buzz'
3の倍数かつ5の倍数なら 'Fizz' + 'Buzz' = 'FizzBuzz'
になった。
ただしどちらの倍数でもないときは '' + '' = ''
となる。
そこで論理和(||)を用いる。
論理和は左がTruthyなら左をそのまま評価、Falsyなら右を評価する。
今回は '' + '' = ''
の場合空文字となり、Falsyな値になるため、右が評価され、('' + n)
となる。
>> true || 'aaa'
true
>> false || 'aaa'
'aaa'
>> '' || 'aaa'
'aaa'
>> 'bbb' || 'ccc'
'bbb'
>> 0 || 'aaa'
'aaa'
>> 1 || 'aaa'
1
TSに限らずJSでも
string + number = string
となるため、number型であるnに空文字を足してあげれば `${n}`
と同じ表現になる。
>> 100 + ''
'100'
>> '' + 100
'100'
>> `${100}`
'100'
また、論理積(&&)は論理和とは逆に、左がTruthyなら右を評価し、Falsyなら左をそのまま評価する。
>> true && 'aaa'
'aaa'
>> false && 'aaa'
false
>> '' && 'aaa'
''
>> 'bbb' && 'ccc'
'ccc'
>> 0 && 'aaa'
0
>> 1 && 'aaa'
'aaa'
さらにNull合体演算子(??)というのも存在し、これは左辺が null
または undefined
ならば右辺を評価する。 null
または undefined
でなければそのまま左辺を評価する。
>> null ?? 'aaa'
'aaa'
>> undefined ?? 'aaa'
'aaa'
>> false ?? 'aaa'
false
>> NaN ?? 'aaa'
NaN
>> 'aaa' ?? null
'aaa'
+n
については単項プラスを参照。
-n
は符号が逆転する。
ビット否定でも2重否定で数値にできる。
>> +'100'
100
>> +'q'
NaN
>> -'100'
-100
>> -'-100'
100
>> ~~'100'
100
>> ~~'q'
0
2. 各桁の和
正の整数の各桁の数字を加算した値を出力する。
ans1
let n = 123456789;
let sum = 0;
while (n > 0) {
sum += n % 10;
n = (n / 10) | 0;
}
console.log(sum);
ans2
const arr = inputs.trim().split('').map((n) => parseInt(n));
let sum = 0;
arr.forEach((n): void => {
sum += n;
});
console.log(sum);
forEach部分は以下のようにも書き換え可能。
参考: for-of文 - 拡張for文
for (let n of arr) {
sum += n;
}
for (let i in arr) {
sum += arr[i];
}
for (let i = 0;i < arr.length;i++) {
sum += arr[i];
}
Array.lengthはドキュメントの通り、配列の長さを取得するプロパティだが、代入することで配列の長さを操作できる。
- Array.length 1
>> const alphabet = ['a', 'b', 'c', 'd', 'e'];
>> alphabet.length
5
>> alphabet.length = 0;
>> alphabet
[]
- Array.length 2
>> const alphabet = ['a', 'b', 'c', 'd', 'e']
>> alphabet.length = 3;
>> alphabet
['a', 'b', 'c']
また、for文で書けるならwhile文でも書ける。
const arr = inputs.trim().split('').map((n) => +n);
let sum = 0;
while (arr.length > 0) {
sum += arr.pop() ?? 0;
}
console.log(sum);
Array.popはundefinedを返す可能性があるので、返った場合はNull合体演算子(??)で0にする。
undefinedを返すときはMDNにある通り配列が空のとき。
蛇足だが、while文は最初の時点で条件に満たなければ内部の処理が実行されないが、条件を満たしていなくとも一度は実行されるdo...whileというのもある。
ans3
console.log(
inputs.trim().split('').reduce((prev, cur) => +cur + prev, 0)
);
3. 文字列内に同じ文字がいくつずつ含まれるか
a, b, cからなる文字列が与えられる。
文字列内に同じ文字がいくつずつ含まれるかを求める。
ans1
interface CountInterface {
a: number;
b: number;
c: number;
}
const counts: CountInterface = {
a: 0,
b: 0,
c: 0
};
input.trim().split('').forEach((c) => {
switch (c) {
case 'a':
counts.a += 1;
break;
case 'b':
counts.b += 1;
break;
case 'c':
counts.c += 1;
break;
}
});
console.table(counts);
出力例
>> input = 'abcabcabc'
┌─────────┬────────┐
│ (index) │ Values │
├─────────┼────────┤
│ a │ 3 │
│ b │ 3 │
│ c │ 3 │
└─────────┴────────┘
ans2
const CHARS = ['a', 'b', 'c'] as const; // type: readonly ['a', 'b', 'c']
type CharsType = typeof CHARS[number]; // type: 'a' | 'b' | 'c'
const countsMap = new Map<CharsType, number>(
[
['a', 0],
['b', 0],
['c', 0]
]
);
const CharSet = new Set<string>(CHARS);
input
.trim()
.split('')
.filter((c: string): c is CharsType => CharSet.has(c))
.forEach((c: CharsType) => {
const currentValue = countsMap.get(c)!;
countsMap.set(c, currentValue+1)
});
console.table(countsMap);
出力例
>> input = 'abcabcabc'
┌───────────────────┬─────┬────────┐
│ (iteration index) │ Key │ Values │
├───────────────────┼─────┼────────┤
│ 0 │ 'a' │ 3 │
│ 1 │ 'b' │ 3 │
│ 2 │ 'c' │ 3 │
└───────────────────┴─────┴────────┘
補足1
filter((c: string): c is CharsType => CharSet.has(c))
問題の制約でinputsは a, b, c
いずれかになっているとは言え、プログラム上では型安全が確定しているわけではない。
そこでfilterの返り値に型ガードを用いれば解決するのだが、一応 a, b, c
いずれかに該当する確認をするために new Set<string>(CHARS)
でSetを作成し、Set.hasを用いる。
CHARS
は ('a' | 'b' | 'c')[]
型のため、 Array.includesの引数に 'a' | 'b' | 'c'
より広いstringを入れることはできない。
なので new Set<string>(CHARS)
の形でstring型のSetにキャストしている。 new Array<string>
にキャストすればincludesが使えるが、Setを紹介したいためにこの形とした。
キャストとは実行時にある値の型を別の型に変換すること。
引用:型アサーション「as」(type assertion)
補足2
countsMap.get(c)!
末尾 !
について。
これはnon nullアサーション演算子という。
これはnullまたはundefinedを取り得る値の場合に用いて、nullまたはundefinedを除く型アサーションする演算子。
Map.getは与えられたキーに合致する値があればそれを返却するのだが、なければundefinedを返却する。
今回は補足1の方で存在することが確定しているため、ないことがない。
なのでundefinedは返ってこないものとしてnon nullアサーションしている。
また、似たものとしてオプショナルチェーン(?)がある。
これについては下記リンクを参照されたい。
4. 文字列内に同じ文字種がいくつ含まれるか
3の応用。
英大文字、英小文字、数字が混ざった文字列が与えられる。
文字列内に英大文字、英小文字、数字がそれぞれ何種類ずつあるかを求める。
const CHAR_TYPE = ['upper', 'lower', 'num'] as const;
type CharsType = typeof CHAR_TYPE[number];
type CountInterface = {
[k in CharsType]: number;
};
const counts: CountInterface = {
upper: 0,
lower: 0,
num: 0,
};
input.trim().split('').forEach((c) => {
// 英小文字a~z
if (/^[a-z]+$/.test(c)) {
counts.lower += 1;
}
// 英大文字A~Z
if (/^[A-Z]+$/.test(c)) {
counts.upper += 1;
}
// 数字0~9
if (/^[0-9]+$/.test(c)) {
counts.num += 1;
}
});
console.table(counts);
出力例
>> input = 'abc123XYZdef456IJK'
┌─────────┬────────┐
│ (index) │ Values │
├─────────┼────────┤
│ upper │ 6 │
│ lower │ 6 │
│ num │ 6 │
└─────────┴────────┘
補足
type CountInterface = {
[k in CharsType]: number;
};
// 下記のように解釈される
type CountInterface = {
upper: number;
lower: number;
num: number;
};
typeでないとできないので注意