GAミント至上主義

Web Monomaniacal Developer.

TypeScriptでdate-fnsを使って先月の最初と最後の日付を取得する

集計系だとデフォルトで先月1ヶ月分のってのをよくやる。結局使わなくなったけどメモ。

日付でだいたい必要なものはdate-fnsで足りてます。TypeScriptでも問題なく使えます。

date-fns.org

実行時の日にちになるように関数。

import { format, startOfMonth, endOfMonth, subMonths } from 'date-fns'

const DATE_FORMAT = 'yyyy-MM-dd'
const lastMonth = (): Date => subMonths(new Date(), 1)
const defaultDateStringFrom = (): string => format(startOfMonth(lastMonth()), DATE_FORMAT)
const defaultDateStringTo = (): string => format(endOfMonth(lastMonth()), DATE_FORMAT)

プログラミングTypeScript ―スケールするJavaScriptアプリケーション開発

プログラミングTypeScript ―スケールするJavaScriptアプリケーション開発

  • 作者:Boris Cherny
  • 発売日: 2020/03/16
  • メディア: 単行本(ソフトカバー)

Mac OSでFirebaseローカルエミュレータを動かす。Javaが必要

Firebaseのローカル開発環境で、Pub/Subだけ動かしてなかったので、最近リリースさればばかりのUIとともに試してみることにしました。
Mac OSのバージョンは10.15.4。

firebase.googleblog.com

CLIのバージョンが8.4.0以上なのを確認

$ firebase --version
8.4.0

起動しようとしたところ、Javaがうんたらのポップアップともに下記のエラー。nodeのバージョンは気にしない。

firebase emulators:start                     
i  emulators: Starting emulators: functions, hosting, pubsub
⚠  Your requested "node" version "10" doesn't match your global version "14"
i  hosting: Serving hosting files from: public
✔  hosting: Local server: http://localhost:5000
i  pubsub: Pub/Sub Emulator logging to pubsub-debug.log

Error: pubsub: Pub/Sub Emulator has exited with code: 1

ログには

No Java runtime present, requesting install.

検索したところ、下記の要件に
エミュレータを使用したローカルでのアプリのテスト  |  Cloud Pub/Sub ドキュメント  |  Google Cloud

Java JRE(バージョン 7 以降)がインストールされている。

とあったのでまずはJREをインストール。

無料Javaソフトウェアをダウンロード


なんか設定で許可するのが必要だったけど無事インストール。

しかし、もう一度叩いてみても同じエラーでした。
どうやらコマンドラインから使うにはJDKも必要なもよう。


こちらからJDKのバージョン8をインストールしました。

java.com


これで無事先程のコマンドのエラーもなくなり、UIの機能をダウンロードし、サーバーが起動しました。

firebase emulators:start

http://localhost:4000/をブラウザで開くとこんな感じ。
f:id:uyamazak:20200527114740p:plain

まだ開発始めたばかりで使う機能は限られているけどFirestoreとか捗りそう。

まずはこれでFunctionsへのリクエストのテストを書きたいけどどうすんだろ。

ユーザーの閲覧履歴をRedisのSorted Setを使って保存&表示する

ユーザーの閲覧履歴をDB(MySQL)からBigQueryに移すにあたり、既存のDBを使っている「最近見たお仕事」を表示する機能も変更が必要になりました。

「最近見たお仕事」は文言は異なるものの、よくECなどでも「最近チェックした商品」、不動産系では「最近見た物件」などと表示されてるやつです。

BigQueryに入っている履歴を使うのも考えましたが大きく以下の3点から別の実装を検討しました。

  • スキャン料金を抑えたい
  • クエリに時間がかかる
  • タイムラグがあるためさっき見たページが表示されない可能性がある

セッションも考えたけど、同じくDBに入ってしまうので、負荷がかかるのでいまいち。

そこで考えたのがRedis。ずっと残しておく必要も無いデータなのでちょうど良さそう。

ハッシュ型でもできそうですが、いろいろ使える型を見ているとSorted Setが今回の用途にぴったりでした。

RedisのSorted Setとは

一つのキーに対してスコアを持った複数の値(メンバーと呼ばれる)を保存でき、常にスコア順に入っているのでスコア順で高速に取得ができます。
またSetなので、重複は入りません。最近みたページには丁度いい

その特徴からよくランキングに使われています。
qiita.com

今回はスコアにタイムスタンプ値を使うことで「最近見たお仕事」を実装しました。

保存する処理

ZADDコマンドを使います。

使用する言語、ライブラリなどにより異なりますが、生のRedisコマンドで書くとこんな感じ。

redis> ZADD recenrtyview:{ユーザーID}  タイムスタンプ値 "{ページID}"

適当なprefix (上記だとrecenrtyview)とユーザーIDをキーとして、事前に出しておいたタイムスタンプ値と、ページのIDを入れておきます。
この処理をページを見たときの処理に入れておけばOK。

有効期限を設定する

今回はさらにその追加した値に有効期限を付けて、ゴミが溜まらないようにしました。

下記を参考に念のため、MULTIを使ってトランザクションもしておきます。
qiita.com

EXPIREは秒で指定するため、今回は30日、 60*60*24*30 = 2592000を使います。

redis> MULTI
redis> ZADD recenrtyview:{ユーザーID}  タイムスタンプ値 "{ページID}"
redis> EXPIRE recenrtyview:{ユーザーID}  2592000
redis> EXEC

もし同じページIDを見た場合はスコアのタイムスタンプ値と、有効期限の両方が更新されます。

取得する処理

今回は新しい順、タイムスタンプ値が大きい順に取得したいので、ZRANGEの逆版、ZREVRANGEを使います。

ZREVRANGE – Redis

redis> ZREVRANGE recenrtyview:{ユーザーID} {start} {stop}

start, stopのindexは0始まりなので、最新10件であれば下記のような感じ。

redis> ZREVRANGE recenrtyview:{ユーザーID} 0 9

これで、保存されたページIDが欲しい順番で返ってくるので、それぞれの情報を取得して、表示すれば完了です。

今回実装したのは下記のサイトのそれぞれのお仕事詳細ページの下の方で使われています。
シニア・中高年の求人・仕事探しなら【シニアジョブ】
f:id:uyamazak:20200526164000p:plain
情報の入れ替わりが激しいのでぼかしといた


ということで、ランキング以外にもSorted Setが使うのがぴったりな用途を見つけたので記事にしました。
RedisのSorted Setを使うことで、高速で、かつ構造もシンプルでバグも少ない実装が出来た気がします。