メモ帳

読むな

【GoogleAppsScript】朝に雨が降っていたら傘を持って帰るのを忘れないようにSlackに教えてもらう

(最近SlackとGASの記事ばっか書いてる気がする...)

はじめに

以下を解消するのが目的です

  • 朝に雨が降ってると退勤時に雨が降ってようが降ってまいが、会社の傘立てに傘を忘れる確率が100%(降ってるときは傘立てが事務所にあるから取りに戻る必要がある)
  • 傘を一つしか持ってないから2日連続で雨のときに無事死亡する

2つ目はずぶ濡れ出勤すれば済む問題なので1つ目を解消します:)

あと、スマホと傘を常時一緒に持っているわけではないのでスマートタグは使いません

どうしたか

弊社では退勤時、Slackの#timecardに「お先に失礼します。」という投稿をする慣習があります。
その慣習を使って、投稿された「お先に失礼します。」のメッセージのスレッドに傘を持ち帰る旨の投稿をして傘の置き忘れを防ぎます。

チャンネルではなくスレッドに書き込む理由は、投稿された自動メッセージがリモートワークの方など他の社員にとって邪魔な情報になる可能性があるからです。

「お先に失礼します。」に関しての自動化は過去記事に記載していますのでよければ以下もご覧ください :) iliiliiiliili.hatenablog.jp

手順

1. SlackBotの作成

1-1. Event Subscriptionsの設定

https://api.slack.com/apps/からSlackBotを作成します。
この辺りの作成手順はググれば出てくるので割愛します。

メッセージの投稿をトリガーとしてWebhookを使用するのでEvent Subscriptionsをオンにします。

Webhook先のURLをVerifiedにするために一旦GASを以下のコードでデプロイしておきます。

const params = JSON.parse(e.postData.getDataAsString());
if (params.challenge) {
  return ContentService.createTextOutput(params.challenge);
}

SlackのEvent Subscriptionsの設定画面

Request URLVerifiedになればOK

eventは以下を設定しました。(チャンネルのみで使用するのであればmessage.channelsだけでOK。多分。)

1-2. OAuth & Permissions の設定

次に、傘を忘れないようにするメッセージを投稿するためにOAuth & Permissionsを設定します

SlackのOAuth & Permissionsの設定画面

今回はメッセージの書き込みとアプリBotの表示のカスタマイズをしたいのでScopesには以下を設定

アプリBotを使うのでBot User OAuth Tokenに表示されているトークンをメモっておく

2. GASの作成

2-1. パラメータの確認

doPost()でSlackからの投稿を受け取って処理します。

function doPost(e) {
  if (params.event.user !== SLACK_USER_ID) {
    return;
  }
  if (
    params.event.type === "message" &&
    params.event.text === "お先に失礼します。"
  ) {
    checkBringBackUmbrella(params.event.ts);
  }
}

やってること

  • 自分以外のSlackUserIdの場合は何もしない
  • テキストが「お先に失礼します。」の時にのみ天気予報のチェック(+Slackへの投稿)を行う

2-2. 天気予報のAPIを叩く

次に、天気予報のAPIを叩いて当日の雨量を取得します。
天気予報を取得するためのAPIにはOpen-Meteoを使用しました。

open-meteo.com

const date = new Date().toLocaleDateString();
// YYYY-MM-DDのformatに整形
const format = date.split("/").map((row) => (row.length === 1 ? "0" + row : row));
const formattedDate = [format[2], format[0], format[1]].join("-");

const LONGTIDE = 雨量を取得したい地域の経度;
const LATITUDE = 雨量を取得したい地域の緯度;
const res = UrlFetchApp.fetch(
  "https://api.open-meteo.com/v1/forecast?timezone=Asia/Tokyo&latitude=" +
    LATITUDE +
    "&longitude=" +
    LONGTIDE +
    "&hourly=rain&start_date=" +
    formattedDate +
    "&end_date=" +
    formattedDate
);

new Date().toLocaleDateString()の戻りが2022/9/22じゃなくて9/22/2022になるのなんでなん...?
ちなGASのタイムゾーンは東京にしてるし、appsscript.jsontimeZoneAsia/Tokyoになってる)

2-3. Slackにメッセージを投稿する

午前7~9時のいずれかの雨量が0.1以上だったら会社に傘を持ってきているとみなして「お先に失礼します。」のメッセージのスレッドに対して投稿する。
(なお、会社用の鞄に常時折りたたみ傘が入ってるので、10時以降に雨が降った場合は傘立てには傘は入っていないものとしています。)

const src = JSON.parse(res);
// 7,8,9時の雨量
const morningRains = src.hourly.rain.slice(7, 10);
// すべて0.0の場合は雨が降っていなかった
if (morningRains.every((rain) => rain === 0.0)) {
  return;
}
// 雨が降っている場合はSlackのスレッドに書き込む
const SLACK_TOKEN = "SlackのAppBotのトークン";
const SLACK_CHANNEL_NAME = "チャンネル名";
UrlFetchApp.fetch("https://api.slack.com/api/chat.postMessage", {
  method: "post",
  payload: {
    token: SLACK_TOKEN,
    channel: SLACK_CHANNEL_NAME,
    text: "<@"+SLACK_USER_ID+">今日は午前中の雨量が0.1以上でした。傘があれば持って帰りましょう。",
    icon_emoji: ":umbrella:",
    username: "放置傘警察",
    thread_ts: ts,
    reply_broadcast: false,
  },
});

これで雨のときにはSlackが教えてくれるようになりました:)

Slackのスレッド画面

コード全文

最後に

会社以外の傘立てに忘れたらおしまいだね!!

参考

open-meteo.com

【GoogleAppsScript】Slackに投稿したらNature RemoのAPIを使って照明とエアコンを消す

はじめに

外出時にいつもパートナーに外出する旨のメッセージを送ってるんだけど↓の3つをやらなくちゃいけないのがめんどい

  1. Nature Remoで照明を消す
  2. Nature Remoでエアコンを消す
  3. Slackでメッセージを送る

「Slackにメッセージを送るのが前提としてあるんだったら他のフローは省略できるのでは...?」って思ったのがきっかけです

(過干渉すぎん?っていうツッコミはなしでお願いします!)

Nature Remoとは?

nature.global

やること

  1. Slackにメッセージを投稿する
  2. SlackからGASにイベントを投げてもらう
  3. GASからNature RemoのAPIを叩く

1は人間が実行して2,3を自動化します:)

使うもの

  • Nature Remo API
  • GoogleAppsScript
  • Slack Bot

(いつも GoogleAppsScript なのか GoogleAppScripts なのかが覚えられずググってまう)

手順

1. Slackでアプリbotの作成

https://api.slack.com/apps からアプリBotを作成して、Event Subscriptionsからトリガーとなるイベントを設定する
今回はチャンネルでもDMでも使いたかったから以下を設定

Request URL で検証するために以下の内容でGASを作成して一旦Webアプリケーションとしてデプロイする

function doPost(e) {
  const params = JSON.parse(e.postData.getDataAsString());
  return ContentService.createTextOutput(params.challenge);
}

実行URLをSlack側のRequest URLに入力
↓みたいにVerified になったらSave Changesボタンを押せるようになるので保存

Slack Apiの画面

2. GASからNature RemoのAPIを叩く

https://home.nature.global/ からNature RemoのBearerトークンの発行

電化製品を操作するためのIDの取得には https://api.nature.global/1/appliances/ へGETリクエストを送って確認する

①電気を消す

UrlFetchApp.fetch(
  "https://api.nature.global/1/appliances/" + LIGHT_ID + "/light",
  {
    method: "post",
    headers: { Authorization: "Bearer " + NATURE_REMO_TOKEN },
    payload: {
      appliance: LIGHT_ID,
      button: "off",
    },
  }
);

②エアコンを消す

UrlFetchApp.fetch(
  "https://api.nature.global/1/appliances/" + AIRCON_ID + "/aircon_settings",
  {
    method: "post",
    headers: { Authorization: "Bearer " + NATURE_REMO_TOKEN },
    payload: {
      appliance: AIRCON_ID,
      button: "power-off",
    },
  }
);

クエリパラメータでもbodyでも動作するのなんでなん...?

Slackからのイベントでメッセージが特定の言葉だったときにこれらを実行する

function doPost(e) {
  const SLACK_USER_ID = "自分のSlackUserId";
  const params = JSON.parse(e.postData.getDataAsString());
  if (
    params.event.type === "message" &&
    ["トリガーにしたいメッセージの配列"].includes(params.event.text) &&
    params.event.user === SLACK_USER_ID
  ) {
    turnOffAppliances();
    return;
  }
  return;
}

コード全文

最後に

スマートスピーカー持ってません

参考

developer.nature.global swagger.nature.global

api.slack.com

はてブロの記事が投稿から1年以上経過している場合にアラートを表示する

経緯

ありがたいことに投稿日から数年経過しても、検索からアクセスされてる記事がいくつかあるんだけど、情報が古いから○iitaみたいに この記事は公開日からn年以上が経過しています。情報が古い可能性がありますので十分ご注意ください。 っていう注意書きを画面上に表示させたかった

注意

カスタマイズ

1. CSSを追加

管理画面からデザインカスタマイズ画面を開く
はてブロ管理画面 デザインCSS

デザインCSSにalert用のスタイルを追加 (Bootstrapのwarning alertと同じ)

.warning-alert {
    font-size: 12px;
    background-color: #ebd1b6;
    padding: 0.75rem 1.25rem;
    color: #856404;
    background-color: #fff3cd;
    border: 1px solid #fae39f;
    border-radius: 0.25rem;
}

2. JavaScriptの追加

設定詳細設定画面を開く

設定>詳細設定>&lt;head&gt;要素にメタデータを追加w:100

<head>要素にメタデータを追加に以下のJSを追加する

document.addEventListener("DOMContentLoaded", function () {
  const dtElements = document.querySelectorAll(".date.entry-date.first time");
  if (dtElements.length === 0) {
    return;
  }
  const nowTs = new Date().getTime();
  dtElements.forEach((element) => {
    if (!element) {
      return;
    }
    const dt = element.getAttribute("datetime");
    const postedTs = new Date(dt).getTime();
    const diffYears = ((nowTs - postedTs) / (1000 * 60 * 60 * 24) / 365).toFixed();

    // 記事公開日から1年以上経過している場合にalertを表示する
    if (diffYears >= 1) {
      const div = document.createElement("div");
      div.innerText = `この記事は公開日から${diffYears}年以上が経過しています。情報が古い可能性がありますので十分ご注意ください。`;
      div.classList.add("warning-alert");
      element.closest(".entry-header").prepend(div);
    }
  });
});

上記の設定が完了後に記事一覧・詳細画面で以下のように表示されます:)

記事に表示されるalert


MySQLのDELETE文をEXPLAINで確認する

MySQL5.5でDELETE文をEXPLAINで見ようとしたらエラーになった

mysql> EXPLAIN DELETE FROM samples WHERE title = "title";
ERROR 1064 (42000): You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'DELETE FROM samples WHERE title = "title"' at line 1

EXPLAINでDELETE文が使えるのは5.6.3からで5.5系では実行できないみたい(泣)

dev.mysql.com

MySQL 5.6.3 現在、EXPLAIN に使用できる説明可能なステートメントは、SELECT、DELETE、INSERT、REPLACE、および UPDATE です。MySQL 5.6.3 より前では、SELECT が唯一の説明可能なステートメントです。

5.5で確認したいときは代替として以下で対応する

EXPLAIN SELECT 1 FROM samples WHERE title = "title";

MySQLで連番のカラムを作成する

いつ使うの?

  • AUTO_INCREMENTのidとは別に仮の連番をMySQLで生成したいとき

前提

↓みたいなテーブルにMySQLで生成した連番のidを追加する

id category
4291 アニメ
11 映画
9302 漫画
322 ドラマ

SQL

SELECT
    category,
    @tmp_id := @tmp_id + 1 tmp_id
FROM
    (
SELECT
    @tmp_id := 0
FROM DUAL
) tmp, samples

結果

id category tmp_id
4291 アニメ 1
11 映画 2
9302 漫画 3
322 ドラマ 4

こんな処理を使わなきゃいけない場合はだいたい設計がおかしいから見直そうな!

コンストラクタで非同期処理を実施しているクラスのテストをJestで書く

constructor()で非同期処理を実施しているクラスのテストを書きたかったときの備忘録

↓みたいにconstructor()で非同期処理を実施していて、Promiseをreturnしていない

export default class A {
  private readonly list: any;

  constructor() {
    new Repository().fetch().then((res) => (this.list = res));
  }

  public b() {
    return this.list;
  }
}

テストしたい部分をasync()で囲ってあげればOK

test("sample", () => {
  async () => {
    expect(new A().b()).toBe([]);
  };
});
$ npm run test
> xxxx@1.0.0 test
> jest

 PASS  __tests__/test.test.js
  ✓ sample (1 ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        1.056 s
Ran all test suites.

自信ないんだけど合ってる...? 間違ってたり、他に方法があったらご指摘ください!

TypeScriptでJestを使う

必要なパッケージのinstall

*1

$ npm install --save-dev @babel/preset-typescript

babel.config.js に追記

module.exports = {
  presets: [
    ["@babel/preset-env", { targets: { node: "current" } }],
    "@babel/preset-typescript" // 追加
  ],
};

テストの実施

$ npm run test

> xxxxxx@1.0.0 test
> jest

 PASS  __tests__/sample.test.js
  ✓ jest sample (18 ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        0.665 s, estimated 1 s

テストが通ることを確認できればOK


全部公式に書いてあるからちゃんと読もうな

jestjs.io


*1:必要なパッケージにts-jestを含めていましたがbabelを使用している場合は不要のため修正しました。
babel+jestで型チェックも行いたい場合は必要になります。id:munieru_jp様コメントありがとうございました!

Slackの投稿を1ヶ月単位でスプレッドシートに書き込む【GoogleAppsScript】


現状・やりたいこと

  • プライベートで使ってるSlackがフリープラン
  • 過去の投稿を残しておきたい
  • 保存する投稿はDMのやりとり

やること

1. Slackでユーザーbotの作成

試してないけどアプリbotだとできなさそうだからユーザーbotにした
(てかDMにアプリbotって追加できないよね。。。?)

メッセージ取得に必要な権限を設定してユーザーbotの作成

2. SlackAPIでSlackに投稿した1ヶ月分の投稿の取得

エンドポイントは https://api.slack.com/api/conversations.history

最初channelにDM相手のユーザーIDを指定しててずっとchannel_not_foundが返ってきてハマった。。。
↓DMにもチャンネルIDあるの知らんかった

SlackチャンネルID

メッセージ取得件数がデフォルトで100件ほどだからresponse_metadata.next_cursorが空になるまでfetchMessages()を実行する

function fetchMessages(results = [], cursor = "") {
  const res = UrlFetchApp.fetch(
    "https://api.slack.com/api/conversations.history?channel=" +
      CHANNEL_NAME +
      "&oldest=" +
      oldest +
      "&latest=" +
      latest +
      "&cursor=" +
      cursor,
    {
      method: "get",
      payload: {
        token: USER_TOKEN,
      },
    }
  );

  const src = JSON.parse(res);
  if (!src.ok) {
    Logger.log(src);
    throw new Error("レスポンス不正");
  }

  results.push(...src.messages);
  // response_metadata.next_cursorがある場合は再帰処理
  if (src?.response_metadata?.next_cursor) {
    return fetchMessages(results, src.response_metadata.next_cursor);
  }
  return results;
}

3. スプレッドシートにシートの作成と書き込み

function myFunction() {
  const ss = SpreadsheetApp.openById(SHEET_ID);
  const sheet = ss.insertSheet();
  const dt = new Date();
  const prev = new Date();
  prev.setMonth(dt.getMonth() - 1);
  sheet.setName(prev.getFullYear() + "-" + (prev.getMonth() + 1)); // 年月毎にシートの作成

  const src = fetchMessages();
  // 日付の昇順で登録したいからsort
  src.sort((a, b) => a.ts - b.ts);
  const results = [];
  src.map((row) => {
    const dt = new Date(row.ts * 1000).toLocaleString("JP");
    results.push([dt, USERS[row.user], row.text]); // 時間・ユーザー名・メッセージ
  });

  sheet.getRange(1, 1, results.length, 3).setValues(results); // 一括書き込み
}

4. GASのトリガー設定

トリガーの設定を毎月1日に設定する

GoogleAppsScriptのトリガー設定

あとはスプレッドシートに書き込まれればOK

Googleスプレッドシート

コード全文

感想

UrlFetchApp.fetchparams指定できんの知らんかった
文字列連結むりすぎ

developers.google.com

参考

api.slack.com developers.google.com


特定のファイルを後からgitignoreに追加する方法

.gitignoreに追加してもリモートリポジトリにまだ残ってるやんけ!って毎回ググってるからいい加減覚えような

.gitignoreに除外するファイル(ディレクトリ)を追加してから以下を実施する

$ git rm --cached ファイル名
$ git add .
$ git commit -m "chore: .gitignoreに追加"

ディレクトリを除外する場合は$ git rm --cached -r ディレクトリ名でOK


webpackのbuildでmain.js.LICENSE.txtを出力させない

webpackでproduction buildしたときdist/にmain.js.LICENSE.txtを生成させたくなかった

const TerserPlugin = require("terser-webpack-plugin");

module.exports = {
  // ...省略
  optimization: {
    minimizer: [
      new TerserPlugin({
        extractComments: false,
      }),
    ],
  },
};

extractCommentsがデフォルトでtrueになっているのでfalseにする。
デフォルトは@preserve @license @cc_onアノテーションがあるコメントを拾ってくるっぽい。
allにすると全てのコメントが出力される


Slackbotからの通知メッセージをattachmentsでリッチにする

通常のテキストはこんな感じ↓

これを↓にする

GASのコード

感想

最初attachmentsに渡す値を配列にしてなかったから送信されなくてハマった。。。
個人的な用途としてはcolor, title, title_linkだけで十分かも。(footer_iconとかいらんくね?)

参考

api.slack.com


【TypeScript】Graph API を使って Instagram の投稿を取得する【2022年6月版(v14.0)】

React x TypeScriptでInstagramの投稿をサイト上に表示したかったときの備忘録
使用したGraph APIはv14.0だけど12.x, 13.xあたりも同じっぽい(多分)(未検証)

全体的な流れのうち、1, 2は書くのめんどいので割愛

  1. Instagramをプロアカウントに変更する
  2. facebookInstagramをリンクさせる
  3. facebook developerアカウントでアプリの作成
  4. Graph APIで使用するアクセストークンの有効期限の延長
  5. instagram business idの取得
  6. instagramの投稿の取得してサイトに表示する

1. facebook アプリの作成

アプリタイプにビジネスを選択してアプリの作成

基本情報は適当に入力してアプリ作成後に表示されるアプリIDapp secretをメモっておく

2. 無期限アクセストークンを取得する

グラフAPIエクスプローラ画面で短期アクセストークンを取得する

以下の項目を指定する

  • Facebookアプリ
    • facebook developerアカウントで作成したアプリ
  • ユーザーまたはページ
  • アクセス許可
    • pages_show_list
    • business_management
    • instagram_basic
    • instagram_manage_comments
    • instagram_manage_insights
    • pages_read_engagement
    • pages_read_user_content
    • public_profile (デフォルトで設定済み)

インスタの投稿だけだし instagram_basic しか使わんだろって思って instagram_basicpublic_profile のみにしてたら無期限アクセストークンが取得できなくてハマった。(公式読め)

Generate Access Token を押すとアクセス許可のためのダイアログが表示されるから許可

③アクセストークン左のinfoマークを押してアクセストークンデバッガーの画面に行く

有効期限が1時間以内になっているので「アクセストークンを延長」を押す

④ 長期アクセストークンが表示されるのでデバッグボタンを押す

発行されたアクセストークンの有効期限が「受け取らない」になってればOK

3. instagram business id を取得する

グラフAPIエクスプローラ画面か、Postmanとかで↓を叩くと、

https://graph.facebook.com/v14.0/me?fields=accounts{instagram_business_account}&access_token=[2.で取得したアクセストークン]1

↓下記の形式でデータが戻ってくるので accounts.data.instagram_business_account.id をメモっておく

{
  "accounts": {
    "data": [
      {
        "instagram_business_account": {
          "id": "123456"
        },
        "id": "123456"
      }
    ],
    "paging": {
      "cursors": {
        "before": "xxxxxx",
        "after": "xxxxxx"
      }
    }
  },
  "id": "123456"
}

4. instagramの投稿の取得

https://graph.facebook.com/v14.0/[3で取得したinstagram business id]?fields=media.limit([取得したい件数]){[取得したいfieldをカンマ区切りで指定]}&access_token=[2で取得したアクセストークン] 2 (URLに()使うのモヤる)

をPostmanとかで叩くと、↓の形式で戻ってくるので media.data を使ってサイトに描画する

{
  "media": {
    "data": [
      {
        "media_url": "https://scontent.cdninstagram.com/v/xxxxxxxxxxxx/",
        "media_type": "CAROUSEL_ALBUM",
        "id": "123456"
      }
    ],
    "paging": {
      "cursors": {
        "before": "xxxxxxxxxxxx",
        "after": "xxxxxxxxxxxx"
      },
      "next": "https://graph.facebook.com/v14.0/xxxxxxxxxxxx"
    }
  },
  "id": "123456"
}

5. 記事の取得

export default class Instagram {
  public async fetchPosts(limit: number): Promise<Post[]> {
    return await axios
      .get(
        `https://graph.facebook.com/${process.env.GRAPH_VERSION}/${process.env.GRAPH_BUSSINESS_ID}`,
        {
          params: {
            fields: `media.limit(${limit}){media_url,thumbnail_url,permalink,media_type}`,
            access_token: process.env.GRAPH_ACCSESS_TOKEN,
          },
        }
      )
      .then((res) => res.data.media.data)
      .catch((error) => {
        console.error(error);
      });
  }
}

(※ import などは省略)

今回はReactで実装

<section>
  {this.props.posts.map((row: Post, index: number) => (
    <Box {...row} key={index}></Box>
  ))}
</section>

最終的に↓みたいなマークアップで描画されればOK

<a target="_blank" rel="noopener noreferrer" href="https://www.instagram.com/p/xxxxxx/">
  <img src="https://scontent.cdninstagram.com/v/xxxxxx">
</a>

参考

developers.facebook.com developers.facebook.com developers.facebook.com developers.facebook.com

※本記事は2022/06/26時点の情報です。



  1. URLの中で動的な部分は[]で囲っています。

  2. URLの中で動的な部分は[]で囲っています。

hoverしたときにテキストの枠線を左から右に引く

要点

  • 通常時は疑似要素のborder-bottom: 1px solid #000; を指定したまま width: 0%; にしておく
  • hoverしたときにwidth: 100%; にする

ボタンをhoverしたときに背景色をスライドさせる

これのこと

HTML

<button type="button">Button</button>

ボタン部分

button {
  background-color: #fff;
  width: 200px;
  display: block;
  padding: 10px 0;
  border: 1px solid #000;
  position: relative;
  z-index: 1;
}
button:hover {
  color: #fff;
}

ボタン背景部分

button::before {
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
  z-index: -1;
  content: '';
  background-color: #000;
  transform-origin: right top;
  transform: scale(0, 1);
  transition: transform .3s;
}
button:hover::before {
  transform-origin: left top;
  transform: scale(1, 1);
}

要点

  • transform: scale(0, 1); で疑似要素の背景を非表示にしておく
  • hoverしたときにtransform: scale(1, 1);にして表示する
  • あとはtransform-originでスライドさせたい方向の値を指定する

コード全文

button {
  background-color: #fff;
  width: 200px;
  display: block;
  padding: 10px 0;
  border: 1px solid #000;
  position: relative;
  z-index: 1;
}
button:hover {
  color: #fff;
}
button::before {
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
  z-index: -1;
  content: '';
  background-color: #000;
  transform-origin: right top;
  transform: scale(0, 1);
  transition: transform 0.3s;
}
button:hover::before {
  transform-origin: left top;
  transform: scale(1, 1);
}

その他

感想

右から左、みたいに特定の方向に向かってスライドさせたい場合っていちいち疑似要素使わなかん感じ?クソめんどくね?


スプレッドシートで列名のアルファベットを取得する

B2セルで=COLUMN()したときにBじゃなくて2が表示される

俺がほしいのはBなんだよ!!

ということで完成形

=SUBSTITUTE(REGEXEXTRACT(ADDRESS(ROW(), COLUMN()), "\$.+?\$"), "$", "")

長すぎる......
セル確認したときにぱっと見でどこ参照しているのか分かりづらすぎるんだけどもっと簡単に取得できる方法ない??

やってること

  1. ADDRESS()でセル参照を文字列で取得
  2. $B$2が取得できるのでREGEXEXTRACT()$で囲まれている文字列を検索
  3. SUBSTITUTE()$を空文字に変換

実際はBだけを使うことはないのでINDIRECT()と一緒に使ってセル参照する