かもメモ

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

Google Spreadsheet ◯年の月末日のリストを自動で作りたい

忘れてたのでメモ

Spreadsheet では 2024年1月 のような表記でも日付フォーマットの場合日にちを持っている。なので、2024-01-01 の 2024年1月 と 2024-01-31 の 2024年1月 は別物として扱われる。
例えば年始から各月末の変化をグラフにする際に年始は年始の 2024-01-01 と 各月末 2024-01-31, 2024-02-29, 2024-03-31 ... 2024-12-31 の項目を作る必要があり都度月末を入力するのは面倒なので、年だけ入れれば各月の月末の日を自動で生成させたい。
退屈なことは関数にやらせよう。

ゴール

A B
1 2024

この B1 の年を使って自動的に年始 1/1 と 1月〜12月の月末日を出力させる

結論 ARRAYFORMULA と SEQUENCE と EOMONTH を利用する

年始の日付は単純に DATE を使えば良い

=DATE(B1, 1, 1)

各月の末日をリストで出力

=ARRAYFORMULA( EOMONTH( DATE(B1, SEQUENCE(12), 1), 0) )

月末日をリスト表示

解説

  1. SEQUENCE(12) で月になる 1 〜 12 の連番を取得
  2. ARRAYFORMULA を使って各月の日付をリスト出力
  3. EOMONTH(日付, 0) で当月末日に変換

1. SEQUENCE(12) で月になる 1 〜 12 の連番を取得する

参考: SEQUENCE 関数 - Google ドキュメント エディタ ヘルプ

=SEQUENCE(12)

1〜12 が出力される SEQUENCE(12)

2. ARRAYFORMULA を使って各月の日付をリスト出力する

SEQUENCE を使って出力した数字を月として使えば良い

=DATE(B1, SEQUENCE(12),1)
# => 2024-01-01

これだと 1/1 の日付だけになってしまう。
SEQUENCE(12) が出力する 1〜12 全てに DATE を適応するには配列処理ができる ARRAYFORMULA の中で使用する必要がある

=ARRAYFORMULA(DATE(B1, SEQUENCE(12), 1))

ARRAYFORMULA(DATE(B1, SEQUENCE(12), 1))

3. EOMONTH(日付, 0) で当月末日に変換する

後は EOMONTH 関数で DATE 関数ラップして当月末に変換すれば良い
参考: EOMONTH - Google Docs Editors Help

=ARRAYFORMULA(EOMONTH(DATE(B1, SEQUENCE(12), 1), 0))

ARRAYFORMULA(EOMONTH(DATE(B1, SEQUENCE(12), 1), 0))

まとめ

連番を作るのに ROW を使う方法は関数を入力するセルに合わせて変更しなければならずイケてないな〜と感じてたので、SEQUENCE を使う方法を知れて良かったです!

おわり ₍ᐢ. ̫.ᐢ₎


[参考]

TypeScript React クリップボードにコピーする hooks とコンポーネント作ってみた

input タグに入力された値をクリップボードにコピーする hooks と コンポーネントを作ってみたのでログとしてメモ

📝 Clipboard.writeText() を使ってクリップボードにテキストをコピーすることができる

clipboardjs のようなライブラリもあったのですが、ブラウザで動作するフロントエンドなら Clipboard API を使えばクリップボードに書き込みが可能そうだったのでライブラリなしに実装しました

Clipboard: writeText() method
The writeText() method of the Clipboard interface writes the specified text to the system clipboard, returning a Promise that is resolved once the system clipboard has been updated.
cf. Clipboard: writeText() method - Web APIs | MDN

const onCopy = async (text: string) => {
  try {
    await navigator.clipboard.writeText(text);
  } catch (error) {
    console.error(error.message);
  }
}

Clipboard にテキストをコピーする hooks

// /hooks/useCopyClipboard.ts
import { useCallback, useEffect, useState } from "react";

export const useCopyClipboard = () => {
  const [hasCopied, setHasCpied] = useState(false);
  const [error, setError] = useState<Error | null>(null);

  const onCopy = useCallback(async (text: string) => {
    try {
      await navigator.clipboard.writeText(text);
      setHasCpied(true);
    } catch (err) {
      if (err instanceof Error) {
        console.error(err.message);
        setError(err);
        return;
      }

      console.error(err);
      setError(new Error("Failed to copy"));
    }
  }, []);

  useEffect(() => {
    let timeoutId: number | null = null;
    if (hasCopied) {
      timeoutId = window.setTimeout(() => {
        setHasCpied(false);
      }, 1500);
    }

    return () => {
      if (timeoutId) {
        window.clearTimeout(timeoutId);
      }
    };
  }, [hasCopied]);

  return {
    onCopy,
    hasCopied,
    isError: !!error,
    error,
  };
};

useCopyClipboard hooks から返される onCopy に渡した文字列をクリップボードにコピーするシンプルな hooks です (※ Chakra UI の useClipboard を参考にしました)

input に入力された文字列をクリップボードにコピーする

import { useRef } from "react";
import { useCopyClipboard } from "./hooks/useCopyClipboard";

export default function App() {
  const inputRef = useRef<HTMLInputElement>(null);
  const { onCopy, hasCopied, isError } = useCopyClipboard();

  const handleCopyClipboard = () => {
    if (!inputRef.current) {
      return;
    }
    onCopy(inputRef.current.value);
  };

  return (
    <div className="App">
      <input ref={inputRef} type="text" defaultValue=""  />
      <button type="button" onClick={handleCopyClipboard}>
        {hasCopied ? "Copied!" : "Copy!"}
      </button>
      {isError && <small className="text-error">Failed to copy<small>}
    </div>
  );
}

Sample

iframe だとクリップボードにコピーできないので codesandbox を直接開いてください

ライブラリを使うこと無くクリップボードに任意のテキストをコピーさせることができてびっくりしました!
と、同時に Clipboard API にはブラウザのサポートが完全なわけではないですがクリップボードから読み込みもあるので、evil なサイトではクリップボードの入力値を不正に API にも送るとか出来てしまうのかな〜なども気になるところでした。

Clipboard API - Browser compatibility | MDN Clipboard API - Web APIs | MDN

evil な使い方が出来てしまうんので navigator.clipboard.readText はサポートがマチマチなのかも?

おわり ₍ᐢ. ̫.ᐢ₎


[参考]

ES2023 JavaScript 配列から特定の要素を削除するのに toSpliced が便利

フロントエンドでやることはだいたい配列の操作だと感じてます。
その中でも元の配列を非破壊に特定のインデックスの要素を配列から削除するのが意外と面倒でした

今まで方法

1. filter で削除する要素を取り除く

const result = array.filter((item) => item.id !== deleteID);

この方法は非破壊に要素を削除した配列を削除できるが、配列を全て捜査する分処理が無駄になる

2. 削除する index を探して slice で取り除く

const deleteIndex = array.findIndex((item) => item.id === deleteID);
const result = [
  ...array.slice(0, deleteIndex),
  ...array.slice(deleteIndex + 1)
];

削除する要素の index を見つけるまで配列を捜査して、slice で配列から削除する要素を除いた配列を作成する方法
slice で index の要素を削除した配列を作成するコードが若干見通しが悪い

3. 配列を先にコピーして splice で要素を削除する

const deleteIndex = array.findIndex((item) => item.id === deleteID);
const copyArray = […array];
// or const Array = structuredClone(array); (deep copy)
copyArray.splice(deleteIndex, 1);
const result = copyArray;

.splice(deleteIndex, 1) で特定の index の要素を削除できるが、splice() は元の配列を変更してしまう破壊メソッドなので、先に配列を スプレッド構文 (Shallow copy)structuredClone (Deep copy) コピーしておく必要がある

toSpliced() メソッド

toSpliced() は Array インスタンスのメソッドで、 splice() メソッドに対応するコピーメソッドです。これは、指定された位置の要素を除去したり置き換えたりした新しい配列を返します。
cf. Array.prototype.toSpliced() - JavaScript | MDN

toSpliced() メソッドは splice() メソッドでできる配列の要素の削除・追加・置き換えを非破壊的にでき、新しい配列を返してくれる

const deleteIndex = array.findIndex((item) => item.id === deleteID);
const result = array.toSpliced(deleteIndex, 1);

従来の方法の 2, 3 を合わせてシンプルにしたように書ける!

Tips toSpliced()splice() の挙動の違い

splice() を使うことはあまりないと思いますが、破壊/非破壊だけでなく返り値に違いがあるので注意が必要です

  • toSpliced() は元の配列を変化させない (非破壊)・新しい配列を返す
  • splice() は元の配列を変化させる (破壊)・削除した要素の配列を返す
Sample

要素の削除

// toSpliced
const a1 = [1, 2, 3, 4, 5];
const res1 = a1.toSpliced(1, 2);

console.log(a1);   // [1, 2, 3, 4, 5]
console.log(res1); // [1, 4, 5]

// splice
const a2 = [1, 2, 3, 4, 5];
const res2 = a2.splice(1, 2);

console.log(a2);   // [1, 4, 5]
console.log(res2); // [2, 3]

要素を削除して追加

// toSpliced
const a1 = [1, 2, 3, 4, 5];
const res1 = a1.toSpliced(1, 2, 'a', 'b', 'c');

console.log(a1);   // [1, 2, 3, 4, 5]
console.log(res1); // [1, 'a', 'b', 'c', 4, 5]

// splice
const a2 = [1, 2, 3, 4, 5];
const res2 = a1.splice(1, 2, 'a', 'b', 'c');

console.log(a2);   // [1, 'a', 'b', 'c', 4, 5]
console.log(res2); // [2, 3]

これからは配列からの要素の削除は toSpliced() を使って見通しよく書いていけそうで嬉しいです!
配列からの削除めんどいなと思って調べなかったら気づくこと無く slicefilter で書いているところでした… ES2022, ES2023 で配列の非破壊メソッドが結構追加されているようなので、サボらずちゃんとキャッチアップしようと思ったのでした

おわり₍ᐢ. ̫.ᐢ₎


[参考]

ダンジョン飯の 九井 諒子 さん「竜の学校は山の上」の人だったのか!