かもメモ

自分の落ちた落とし穴に何度も落ちる人のメモ帳

コードで永遠を描いちゃぉ

こんにちは、こんばんわ、おはようございます。
2020年の春から G's Academy に通っていました。インターネッツに生きる永遠の5歳児です。

TL;DR コードは描く

自分語り。(コレ必要か?)
僕自身は高校生の時 f(x) = x + 2 みたいな呪文と動く点 x が〇〇のときの解を求めよ のような問題に、なんで点 x は動くんですか?そもそも動く点 x って何???? と、数IAの段階で "なんもわからん。" と挫折ししたのでした。 その挫折から世界のことは yes / no の二択ではないのに受験やテストは○か✗の結果で判断されるのでこの戦場は不利だと悟りました。それなら戦い方を変えればええやんと ○・✗ で判断できない所を目指し美術系に進んだのでした。完全に人間性が捻くれていますね。

そして潜り込んだ美術系の学校で、絵を書いたり、宇宙について考えたり、ゴミを溶接したり、美味しい雑草を探したり、炬燵を走らせたり到底将来社会の役に立ちそうにもない阿呆の限りを尽くしていた訳ですが、あろうことか就職をしてしまい紆余曲折を経てプログラミングにも触れるようになってしまいました!芸術家になるという意味に於いては何たる敗北でありましょう!!

まぁ、そんな事はどうでもいいのですが、前述の通り数学的基礎学力皆無なので雰囲気でコードを書いていました。社員が僕1人だったからレビューされるとか良し悪しもよく分からずネットの検索結果と質問投稿サービスに投稿してボロクソに言われるを繰り返す試行錯誤の日々を送っていたのです。( F-site の皆様には大変お世話になりました。)
そんな当時は Flash が盛り上がっていた時代で「プログラム書けるならFlashでゲーム作れるやろ」と無茶振りされたのをきっかけに FlashActionScript を触るようになり、コードで絵が描ける!それも人に反応したり動く絵が描ける!!すごい!!! ということに気づき感動したのでした。
そこで初めて高校生の時に挫折した f(x) = x + 2function(x) { return x + 2 } であり、動く点 x は setInterval で高速に関数を呼び出しているから返される値が変わって x と言う名前で描画される点が動いているように見えるのか〜と理解したのでした。なるほどね。完全理解。(高校の時に先にプログラミングに触れていたら僕の人生は大きく変わっていたことでしょう)

プログラミングで絵が描けると気づいてからは複雑な絵は ActionScript を 1フレームで描くのが管理しやすいとか Processing でパーティクル作ったりとプログラミングが楽しくなっていました。
結局はプログラミングも自分の作りたいものを見せたい方法で作成するための手段にしか過ぎない。アウトプットをどうしたしたいか・は作りたい絵を作るための手段をどうするかで、筆を取るのか、カメラを持つのか、彫刻刀や溶接マシンを持つのか、キーボードの前に座るのかの違いでしがない。と、考えるようになったのでした。
なので、今もコード書くではなく、描くだと思っているのです。どうでもいい話を終わります

では、プログラミングで描くこと選択する理由はなにか。アクションを起こしたりというのもありますが、シンプルなところで言えば無限なものを描けることな気がします。
例えば、無限に続く海岸線や水平線の絵は物理的なキャンバスに描くことは難しいです。プログラミングなら f(x) = x + 2 のように無限な線を引くことができるのです。嗚呼、数学をやっておけばもっとすごい絵が描けたでしょうに…
もっとすごい絵を自分で描けるようになりたい。その思いが ガッコ の門を叩いた理由でもありました。

コードで永遠を描く 📛

せっかくなので、コードで永遠を書いていきたいと思います。
インターネッツに触れている方は耳にしたことがあるかもしれないこのこの言葉
ゆゆ式は終わらない
そう、ゆゆ式は永遠に続いていて毎週の放送を楽しまれている方が居る。そんな世界線があるのです!

さて、この blog を見たことがある方はお察しかと思いますが、僕の中では
アイカツ!は人生
でありますから、アイカツ!も永遠に続いている世界線があるはずです。

あー今週はアイカツ!何話だったっけ?分からないと困りますね。
プログラミングを使ってこの問題を解決していきましょう。僕の幸せのために

無限に続く永遠データ

まず、永遠とは何かを考えます。(ろまんちっくですね。)
ほい。無限を表せる永遠データは、現在値と次の値が取得できる関数で構成されていれば永遠を表現できそうです。 (ロマン無)

const stream = (no) => [no, () => stream(no + 1)];

const story = stream(1);
// 1話
console.log(story[0])
// 2話
console.log(story[1]()[0])
// 3話
console.log(story[1]()[1]()[0])
// n 話 ( n - 初期値 1 回 [1]() を繰り返す )
console.log(story[1]()...[0])

アイカツ!は毎週放送されるので、178話、2016年3月31日 を初期値として、ここから次の話数と放送日が取得できるデータ構造を作れば、永遠にアイカツ!の話数と放送日が判るデータが表現できそうです!

interface storyData {
  no: number; // 話数
  date: string; // "YYYY-MM-DD" 放送日
}

const getStoryData = (no: number): storyData => {
  return {
    no: no,
    // 話数から放送日を返す
    date: getOnAirDateByStoryNo(no), 
  }
};

const storyStream = (no: number) => {
  return [getStoryData(no), () => storyStream(no + 1)];
};

イメージとしてはこんな感じ。 storyStream[0]{no: 178, date: '2016年3月31日'} が入り storyStream[1]()[0] に次の放送情報、storyStream[1]()[1]()[0] にその次の…と入っていけばOK。

初期値の設定と、話数から放送日を返す関数をつくる

放送日は毎週放送されるので、放送日は前の放送日 +7 日で表現できそうです。
初期値が 178 話 2016年3月31日 なので、179 話は 2016年3月31日 +7 * 179 - 178 という感じ。
日付の操作には dayjs を使ってみます

import * as dayjs from 'dayjs'

const INI_STORY_DATA = {
  no: 178,
  date: '2016-03-31',
};

// 話数の差分を返す
const generateStoryDiff = (initialNo) => (no) => {
  return no - initialNo;
};

// 初期日から step 週間進んだ日付を返す
const generateOnAirDateByStep = (initialDate) => {
  const startDate = dayjs(initialDate);
  return (step) => {
    const week = 7;
    return startDate.add(step * week, 'day');
  }
};

// 初期話数・放送日を渡して、話数から放送日を取得できる関数を返す
const generateOnAirDate = (initialNo, initialDate) => {
  const getOnAirDateByStep = generateOnAirDateByStep(initialDate);
  const getStoryStepDiff = generateStoryDiff(initialNo);
  
  return (no) => {
    const storyStep = getStoryStepDiff(no);
    return getOnAirDateByStep(storyStep);
  }
};

// 話数と放送日をフォーマットしたデータを返す
const generateStoryData = (getOnAirDate) => (no) => {
  return {
    no,
    date: getOnAirDate(no).format('YYYY-MM-DD')
  };
};

// 永遠に続くデータ構造を返す
const generateStoryStream = (getStoryData) => {
  const storyStream = (no) => {
    return [getStoryData(no), () => storyStream(no + 1)];
  };

  return storyStream;
};

// 初期値を設定して永遠に続くアイカツ!話数と放送日のデータを返す
const getStoryStream = ({ no, date }) => {
  const getStoryData = generateStoryData(generateOnAirDate(no, date));
  const stream = generateStoryStream(getStoryData);
  return stream(no);
};

// 178話始まりのデータ
const story178 = getStoryStream(INI_STORY_DATA);
story178[0]; // => { no: 178, date: '2016-03-31' }
// 179話
story178[1]()[0]; // => { no: 179, date: '2016-04-07' }

アイカツ!は終わらないコンテンツになりました!!!!
₍ ᐢ. ̫ .ᐢ ₎👌 ヤッタネ

話数から放送日を取得する

永遠にアイカツ!の放送日がわかるようになったので、この永遠データを使って話数から放送日を取得できると便利ですね。(さっき差分を取れる関数を作ったので、それを使えば楽ですが敢えて永遠データから取得する面倒くさいことをします!)

// 178話始まりのデータ
const story178 = getStoryStream(INI_STORY_DATA);

const getOnAirDate = (stream, no) => {
  const step = stream[0].no - no;
  console.log(step, stream);
  if (step === 0) {
    return stream[0].date;
  }
  return getOnAirDate(stream[1](), no);
};

// 180話の放送日
getOnAirDate(story178, 180); // => 2016-04-14
// 300話の放送日
getOnAirDate(story178, 300); // => 2018-08-02

永遠データをループさせて都度次の放送日を求めているので、効率的な方法ではないのですが永遠データからn話の放送日を取得することができました。
この永遠に再帰でループさせられるという点が永遠データの永遠たる所以なのかなと思います。(効率的ではないので本当に無駄…)

今日の日付から今週何話放送なのかを求める

また無駄に永遠データを使って、今週は何話が放送されるのか取得できるようにしてみます。
今日 <= 放送日 になった時の話数が今週放送される話数です。

const getThisWeekStoryNo = (date) => {
  const currentDate = dayjs(date);

  const getStoryNo = (stream) => {
    const diff = currentDate.diff(stream[0].date);
    if (diff <= 0) {
      return stream[0]
    }
    return getStoryNo(stream[1]());
  };

  return getStoryNo;
};

// 2016-04-18 の次の放送は?
getThisWeekStoryNo('2016-04-18')(story178);
// => { no: 181, date: '2016-04-21' }

// 2020-12-18 の次の放送は?
getThisWeekStoryNo('2020-12-18')(story178)
// => { no: 425, date: '2020-12-24' }

次は 2020年 12月24日に 425話が放送ですね!きっとクリスマス会に違いありません!! 今年は何でツリーを切り倒すのか気になりますね。たのしみ〜
はい。永遠データをループさせて今週の放送話数も取得できるようになしました!!

まとめ

と、こんな具合に永遠データを使ってアイカツ!を終わらないコンテンツにすることができました!
これを bot 化するとかして使えばとっても便利そうです!!
₍ ᐢ. ̫ .ᐢ ₎👌 ヤッタネ

起点となる 178 話のデータから毎回ループでデータ作成をしているので効率はめちゃめちゃ悪いですが、アイカツ!は終わらないコンテンツという永遠データを描くという浪漫が目的だったのでヨシ! (普通に考えれば初期値からの差分でほしいデータだけ作成すればよいのです。本当に無駄な遊びをしました)

全然関係のない宣伝

このサイトのデザインとコーディングと絵描いてるので見てくれ〜!
宇宙の事知るのめちゃ楽しい!!

そして最後に、僕が言いたいたった一つのこと… それは… アイカツ!みて…

おわり。


JavaScript 配列の要素を immutable に変更する方法を考えてみる

よくあるオブジェクトが要素のリストでオブジェクトの内容を変更したい。
そして配列は immutable に扱いたい。

interface Idol {
  id: number;
  name: string;
  type: string;
  unit?: string;
};

const data: Idol[] = [
  { id: 1, name: 'Hosimiya Ichigo', type: 'cute', unit: 'soleil' },
  { id: 2, name: 'Kiriya Aoi', type: 'cool', unit: 'soleil' },
  { id: 3, name: 'Shibuki Ran', type: 'sexy' },
  { id: 4, name: 'Todo Yurika', type: 'cool' },
  { id: 5, name: 'Kanzaki Mistuki', type: 'sexy' },
  { id: 6, name: 'Nastuki Mikuru', type: 'pop' },
  { id: 7, name: 'Ozora Akari', type: 'cute' },
];

map で変更する

const updateList = (list) => (id, data) => {
  return list.map((item) => {
    if (item.id === id) {
      return {
        ...item,
        ...data,
      }
    }
    return item;
  });
};

const newData = updateList(data)(3, { unit: 'soleil' });
/*
[
  {id: 1, name: "Hosimiya Ichigo", type: "cute", unit: "soleil"},
  {id: 2, name: "Kiriya Aoi", type: "cool", unit: "soleil"},
  {id: 3, name: "Shibuki Ran", type: "sexy", unit: "soleil"},
  {id: 4, name: "Todo Yurika", type: "cool"},
  {id: 5, name: "Kanzaki Mistuki", type: "sexy"},
  {id: 6, name: "Nastuki Mikuru", type: "pop"},
  {id: 7, name: "Ozora Akari", type: "cute"}
]
*/

配列をすべてループで回すので扱うデータが大きいとパフォーマンスに影響がありそうだけど、シンプルにデータの変更ができる

更新するデータのインデックスを取得して、更新データの前後を slice で取り出して結合する

const updateList = (list) => (id, data) => {
  const index = list.findIndex((item) => item.id === id);
  // 変更対象がない時はコピーした元データを返す
  if (index === -1) {
    return [...list];
  }
  
  return [
    ...list.slice(0, index),
    {
      ...list[index],
      ...data,
    },
    ...list.slice(index + 1),
  ];
};

updateList(data)(7, { unit: 'luminas' });
/*
[
  {id: 1, name: "Hosimiya Ichigo", type: "cute", unit: "soleil"},
  {id: 2, name: "Kiriya Aoi", type: "cool", unit: "soleil"},
  {id: 3, name: "Shibuki Ran", type: "sexy"},
  {id: 4, name: "Todo Yurika", type: "cool"},
  {id: 5, name: "Kanzaki Mistuki", type: "sexy"},
  {id: 6, name: "Nastuki Mikuru", type: "pop"},
  {id: 7, name: "Ozora Akari", type: "cute", unit: "luminas"}
]
*/

Array.prototype.slice メソッドは取り出せる要素がない時、空配列 [] を返すので変更対象が配列の先頭でも最後でもエラーにならない

e.g.

const list = [1, 2, 3, 4, 5]
list.slice(0, 0); // => []
list.slice(list.length); // => []

id をキーにしたオブジェクトを作成して、変更後に配列に戻す

// 配列を key をキーにしたオブジェクトに変換する
const mapToObjectbyKeys = (list) => (key) => {
  return list.reduce((obj, item) => {
    return {
      ...obj,
      [item[key]]: {...item}
    }
  }, {});
};

// キーのリスト順にオブジェクトを配列に戻す
const objectToArrayByList = (obj) => (keyList) => {
  return keyList.map((key) => {
    return {...obj[key]};
  });
};

const updateList = (list) => (key, id, data) => {
  // key のリストを作成
  const keyList = list.map((item) => item[key]);
  // key をキーにしたオブジェクトを作成
  const dataObject = mapToObjectbyKeys(list)(key);
  // データを更新したオブジェクトを作成
  const updateData = {
    ...dataObject,
    [id]: {
      ...dataObject[id],
      ...data,
    }
  };
  // 配列に戻して返す
  return objectToArrayByList(updateData)(keyList);
};

updateList(data)('id', 5, { unit: 'tristar' });
/*
[
  {id: 1, name: "Hosimiya Ichigo", type: "cute", unit: "soleil"},
  {id: 2, name: "Kiriya Aoi", type: "cool", unit: "soleil"},
  {id: 3, name: "Shibuki Ran", type: "sexy"},
  {id: 4, name: "Todo Yurika", type: "cool"},
  {id: 5, name: "Kanzaki Mistuki", type: "sexy", unit: "tristar"},
  {id: 6, name: "Nastuki Mikuru", type: "pop"},
  {id: 7, name: "Ozora Akari", type: "cute"}
]
*/

やりたいことに対して壮大すぎる気がする…
内部的なデータはオブジェクトに変換したもので持っておき、インデックス順に並べて表示する時だけ配列に戻すという運用方法ならありかもしれない。

関数内で配列のコピーを作成してしまう

immutable 原理主義でないなら配列を丸っとコピーして、関数内で破壊的なメソッドを使って変更して返してしまえば簡単そう

コピーした配列を直接操作する

const updateList = (_list) => (id, data) => {
  const list = [..._list];
  const index = list.findIndex((item) => item.id === id);
  if (index === -1) {
    return list;
  }
  
  const item = list[index];
  list[index] = {
    ...item,
    ...data,
  };
  return list;
};

updateList(data)(6, { unit: 'WM' });
/*
[
  {id: 1, name: "Hosimiya Ichigo", type: "cute", unit: "soleil"},
  {id: 2, name: "Kiriya Aoi", type: "cool", unit: "soleil"},
  {id: 3, name: "Shibuki Ran", type: "sexy"},
  {id: 4, name: "Todo Yurika", type: "cool"},
  {id: 5, name: "Kanzaki Mistuki", type: "sexy"},
  {id: 6, name: "Nastuki Mikuru", type: "pop", unit: "WM"},
  {id: 7, name: "Ozora Akari", type: "cute"},
]
*/

splice を使うパターン

const updateList = (_list) => (id, data) => {
  const list = [..._list];
  const index = list.findIndex((item) => item.id === id);
  if (index === -1) {
    return list;
  }
  // 置き換えられる要素が返される
  list.splice(index, 1, {
    ...list[index],
    ...data,
  });
  return list;
};

updateList(data)(1, { unit: 'Cosmos' });
/*
[
  {id: 1, name: "Hosimiya Ichigo", type: "cute", unit: "Cosmos"},
  {id: 2, name: "Kiriya Aoi", type: "cool", unit: "soleil"},
  {id: 3, name: "Shibuki Ran", type: "sexy"},
  {id: 4, name: "Todo Yurika", type: "cool"},
  {id: 5, name: "Kanzaki Mistuki", type: "sexy"},
  {id: 6, name: "Nastuki Mikuru", type: "pop"},
  {id: 7, name: "Ozora Akari", type: "cute"}
]
*/

Array.prototype.splce() は破壊メソッドで、戻り地は変更される値なのでそのまま return はできない

for break を使う

配列をコピーしてしまっているので for で回して変更対象のデータを置き換えたら break してしまうのもアリかも

const updateList = (_list) => (id, data) => {
  const list = [..._list];

  for(let i = 0, l = list.length; i < l; i += 1) {
    if (list[i].id === id) {
      list[i] = {
        ...list[i],
        ...data,
      };
      break;
    }
  }
  return list;
};

updateList(data)(4, { name: 'Arisugawa Otome', type: 'pop' });
/*
[
  {id: 1, name: "Hosimiya Ichigo", type: "cute", unit: "soleil"},
  {id: 2, name: "Kiriya Aoi", type: "cool", unit: "soleil"},
  {id: 3, name: "Shibuki Ran", type: "sexy"},
  {id: 4, name: "Arisugawa Otome", type: "pop"},
  {id: 5, name: "Kanzaki Mistuki", type: "sexy"},
  {id: 6, name: "Nastuki Mikuru", type: "pop"},
  {id: 7, name: "Ozora Akari", type: "cute"}
]
*/

伝統的な書き方って感じだけどOK。

所管

データがめちゃめちゃ大きくならないなら map を使うのがわかりやすそう。
それか配列を丸っとコピーしてしまって、findIndex で変更データを探して置き換えるパターンが見やすいのかな?

Array.prototype.splice が変更された新しい配列を返してくれるメソッドなら楽だったのにな…って感じ。
lodash とかライブラリを使うといい感じに操作できる関数があるのかもしれない。

TODOアプリとか結構あるあるなデータ変更だと思うんだけど、みんなどうしてるのか教えて〜

おわり


[参考]

ハンズオンJavaScript

ハンズオンJavaScript

道に迷ったときはアイカツ!を見よう

JavaScript OS の判定をしたい

ショートカットの案内を表示するのに Mac, iPhone, iPad なら それ以外は Ctrl を表示したい。という要望でクラアンと側で OS の判定をしたメモ

navigator.userAgent / navigator.platform を使う

MDN のドキュメントには navigator.userAgent, navigator.platform 共に信頼性が低いと書かれてるけど、クライアント側で判定しようとしたら現実的にこれらで判別するしかなさそう。

NavigatorID.userAgent
ユーザーエージェント文字列の検出に基づくブラウザーの識別は信頼性が低く、ユーザーエージェント文字列はユーザーが設定可能なので推奨されません。
cf. NavigatorID.userAgent - Web API | MDN

Mac OS 10.15.7 googleChrome での場合はこんな感じ

const getUserAgent = () => {
  return (navigator && navigator.userAgent) || false;
};

console.log(getUserAgent());
// Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)
// AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36

NavigatorID.platform
ブラウザーのプラットフォームを表す文字列を返します。仕様書ではブラウザーが常に空文字列を返すことを許可していますので、信頼できる答えを得るためにこのプロパティを頼らないようにしてください。
cf. NavigatorID.platform - Web API | MDN

navigator.platform は OS の情報だけ

const getPlatform = () => {
  return (navigator && navigator.platform) || false;
};

console.log(getPlatform());
// MacIntel

OS を判定する

navigator.userAgentnavigator.platform も文字列なので、そこに特定の文字が含まれるかで判定すればいいので、indexOfString.includes を使えばOK
String.includesIE が未対応で polyfill が必要になりそうなので対応ブラウザに IE が含まれるなら indexOf を使うのが良いかも)

navigator.userAgentnavigator.platform とで微妙に含まれる文字が異なるっぽいのでどちらを使うかで別々の判定リストを作成するのが良さそう

navigator.userAgent を使って判定

const OS_LIST = {
  'Windows': 'windows nt',
  'MacOS': 'mac os',
  'iPhone': 'iphone',
  'ipad': 'iPad',
  'Android': 'android',
  'Linux': 'linux',
};

const getUserAgent = () => {
  return (navigator && navigator.userAgent) || false;
};

indexOf を使って判定

const checkIncludeText = (target) => (obj) => {
  let result = false;
  Object.entries(obj).some(([key, value]) => {
    if ( target.indexOf(value) !== -1 ) {
      result = key;
      return true;
    }
    return false;
  });

  return result;
};

const checkOS = () => {
  const ua = getUserAgent();
  let os = 'unknown';
  if (!ua) return os;
  
  return checkIncludeText(OS_LIST)(ua.toLowerCase());
};

checkOS();
// => MacOS

includes を使って判定

オブジェクトを回して判定するメソッドの中身を変えればOK

const checkIncludeText = (target) => (obj) => {
  let result = false;
  Object.entries(obj).some(([key, value]) => {
    if ( target.includes(value) ) {
      result = key;
      return true;
    }
    return false;
  });

  return result;
};

const checkOS = () => {
  const ua = getUserAgent();
  let os = 'unknown';
  if (!ua) return os;
  
  return checkIncludeText(OS_LIST)(ua.toLowerCase());
};

checkOS();
// => MacOS

navigator.platform を使って判定

navigator.platformuserAgent に含まれる文字と微妙に異なるっぽいので、こちらを使って判定する場合は別のリストを作成しておけばOK

const OS_LIST = {
  'MacOS': 'mac',
  'iPhone': 'iphone',
  'ipad': 'iPad',
  'Windows': 'windows',
  'Android': 'android',
  'Linux': 'linux',
};

const getPlatform = () => {
  return (navigator && navigator.platform) || false;
};

indexOf を使って判定

const checkIncludeText = (target) => (obj) => {
  let result = false;
  Object.entries(obj).some(([key, value]) => {
    if ( target.indexOf(value) !== -1 ) {
      result = key;
      return true;
    }
    return false;
  });

  return result;
};

const checkOS = () => {
  const platform = getPlatform();
  let os = 'unknown';
  if (!platform) return os;
  
  return checkIncludeText(OS_LIST)(platform.toLowerCase());
};

checkOS();
// => MacOS

includes を使って判定

const checkIncludeText = (target) => (obj) => {
  let result = false;
  Object.entries(obj).some(([key, value]) => {
    if ( target.includes(value) ) {
      result = key;
      return true;
    }
    return false;
  });

  return result;
};

const checkOS = () => {
  const platform = getPlatform();
  let os = 'unknown';
  if (!platform) return os;
  
  return checkIncludeText(OS_LIST)(platform.toLowerCase());
};

checkOS();
// => MacOS

複数での判定は test を使うのが良さそう

indexOf, includes は単一のテキストが含まれているかでしか判定できないので、Mac || iPhone || iPad のような複数にマッチさせたい場合は正規表現を使える test() を使うのが良さそうです。

e.g. Mac, iPhone, iPad かどうか判定したい

const IS_MAC_OR_IOS_REGEXP = /mac|iphone|ipad/;

const getUserAgent = () => {
  return (navigator && navigator.userAgent) || false;
};

const getPlatform = () => {
  return (navigator && navigator.platform) || false;
};

// userAgent で判定
IS_MAC_OR_IOS_REGEXP.test(getUserAgent());
// => true or false

// platform で判定
IS_MAC_OR_IOS_REGEXP.test(getPlatform());
// => true or false

const isMac = () => {
  return IS_MAC_OR_IOS_REGEXP.test(getPlatform());
}

const save = `${isMac() ? '⌘' : 'Ctrl'} + s`;

example

See the Pen Check OS by userAgent & platform by KIKIKI (@kikiki_kiki) on CodePen.

感想

navigator.userAgentnavigator.platform も変更が可能なので完璧な判定ができるわけではないですが、変更してる人は自分の意志なことが多いだろうしフロントで判別するならこんな感じでいいんじゃないかな〜ってイメージです。ブラウザは無視して OS だけ判定するなら navigator.platform の方がシンプルな文字列が返ってくるので扱いやすそうな印象を持ちました。
単一の判定なら indexOf()includes() を使って複数での判定なら test() を使うのが良さそうです。

まぁ自前で実装せずに判定できる信頼性のあるライブラリ入れちゃうのが簡単で良いと思いますが。(なんでこの記事書いたんでしょうねw)

おわり。


[参考]