LeetCode 197 Rising Temperature

概要

LeetCodeのsql問題、rising-temperatureという問題を解きました URLは以下です(ログインしないと見られません)
https://leetcode.com/problems/rising-temperature/description/

問題文

Table: Weather

+---------------+---------+
| Column Name   | Type    |
+---------------+---------+
| id            | int     |
| recordDate    | date    |
| temperature   | int     |
+---------------+---------+
id is the column with unique values for this table.
There are no different rows with the same recordDate.
This table contains information about the temperature on a certain day.
 

Write a solution to find all dates' Id with higher temperatures compared to its previous dates (yesterday).

Return the result table in any order.

回答

昨日よりも気温が上がっているレコードを取得する必要があります。
あるレコードより一つ前のレコードを参照する場合、LAG関数を使います。

https://dev.mysql.com/doc/refman/8.0/ja/window-function-descriptions.html#function_lag

LAG関数は第1引数にカラム名、第2引数に何個前のレコードを見るか指定できます。
さらにOVER句のなかでORDER BYで参照する並び順を指定できます。

select
    id
from
    (
        SELECT
            id,
            temperature,
            recodeDate,
            LAG(temperature, 1) OVER (
                ORDER BY
                    recordDate
            ) beforeTemp,
            LAG(recodeDate, 1) OVER (
                ORDER BY
                    recordDate
            ) beforeDate,
        FROM
            Weather
    ) as lagTable
where
    temperature > beforeTemp
    and DATEDIFF(day, recodeDate, beforeDate) = 1

DATEDIFFは日付のDIFFを返す関数です。
最初に昨日であるという条件を忘れて付けなかったので、二日前のレコードを取得してテストが落ちました。ちゃんと考えれていますね、、。

pnpmでyarnやnpmで管理していたパッケージが参照できなかった時に見ること

概要

pnpmはnpmやyarnと同じパッケージ管理ツールです。
pnpmは、ライブラリの依存パッケージの共有によるディスク容量の節約や、インストール速度の向上が見込めるようです。
pnpm.io

ただ、pnpmはyarnやnpm と依存関係の管理方法が異なるので、切り替えた時にうまく依存関係を解決できないケースがあるようです。

yarnで動いていた依存関係がpnpmで解決できない時

自分の場合は、使っていたライブラリがさらに依存しているパッケージがうまくインストールされずに、not found パッケージ名というエラーに遭遇しました。
公式サイトによると、以下のように案内されています。

ヒント
使用しているツールがシンボリックリンクでうまく機能しない場合でも、node-linker の設定を hoisted にすることで pnpm を使い続けることができるかもしれません。 この設定によって pnpm は、npm や Yarn Classic と似た node_modules ディレクトリを作成するようになります。

https://pnpm.io/ja/npmrc#node-linker 上記の記事により詳しい解説がされています。

hoisted - シンボリックリンクは作成されず、フラットな node_modules が作成されます。 npm や Yarn Classic によって作成される node_modules と同じです。 この設定を使用すると、Yarnのライブラリーの 1 つが巻き上げに使用されます。

このhoistedの設定は.npmrcファイルにnode-linker=hoistedと記載すると良いようです。
ただ、これって結局pnpmをyarnと同じように動かしていると思うので、必要に迫られない限りは設定しないほうが良さそうですね。

LeetCodeをやってみました。

概要

LeetCodeが面白いときいて初めてみました。
この記事でつらつらと感想などを雑に書いていきたいと思います

ログイン後の画面

こちらがログイン時の画面です。 Interviewがトップページのバーにあるのは面白いですね、ただのコード道場ではなく面接に向けた訓練場って感じがしました。

問題一覧画面

こちらは問題一覧画面です。技術分野や難易度別に問題を選ぶことができます。 Easyの問題を解いてみたのですが、おそらくジュニア級(経験1 ~ 2年くらい)にちょうどいい問題だなと感じました。

問題画面

こちらは問題を解くエディタ画面です。
なんとなくSQLを選んでみました。問題にテーブル構成と、出力すべき要件が書かれています。
自分は気合を入れないと英語が読めないので、問題文の読解にも少し時間を使います。
逆に言えば英語のいい勉強になりますね。

回答

Runしてテストケースが通ったらSubmitします。これで用意された全てのテストケースが通ればOKです。

結果

無事にテストが全て通ったらAcceptedになりました。

TypeScriptのconstやas constの挙動について

概要

TypeScriptには定数を宣言する仕組みとして、constやas constが存在します。
ただ、オブジェクトや配列をconstで定義したとしても、中の値は自由に書き換えれるなどの注意点があります。
これを例とともに見ていきたいと思います。
動作環境: TS5.4.3

const

constは定数を宣言できますが、配列やオブジェクトで扱うときに注意が必要です。 数値や文字列をconstで定義して、後から置き換えようとするともちろん怒られます。

const num = 0
num = 1 // Cannot assign to 'num' because it is a constant.(2588)

一方でオブジェクトの値の置き換えは通ってしまいます。

const obj = {test: 1}
obj.test = 2 // 通る

これは、一度宣言したobj自体をまるっと書き換えているのではなく、objの存在はそのままで中のプロパティのみ書き換えているからです。
配列の場合もconstで宣言しても内部の要素は書き換え可能です。

const arr = [1, 2, 3]
arr[0] = 5

なお、オブジェクト自体を書き換えようとすると怒られます

const obj = {test: 1}
obj = {} // Cannot assign to 'obj' because it is a constant.(2588)

constの中身をreadonlyにするとどうなるか

内部のプロパティを書き換えられないように、readonlyで試しに書いてみます。 この場合、プロパティを書き換えることはできなくなります。

type place = {
    readonly value: number
    readonly name: string
}

const places: place = {
  value: 0,
  name: 'コンビニ',
}
places.name = 'スーパー' // Cannot assign to 'name' because it is a read-only property.(2540)

一方で、以下のようなreadonlyオブジェクトを持つ配列の場合は、配列操作で実質的に値を置き換えれるので注意が必要です。

type place = {
    readonly value: number
    readonly name: string
}

const places: place[] = [
    {
        value: 0,
        name: 'コンビニ',
    },
]

// 配列の要素を削除
places.pop()

// 新しい要素を入れる
places.push({
    value: 1,
    name: '学校',
});

console.log(places);
// [LOG]: [{
//   "value": 1,
//   "name": "学校"
// }] 

as const

as constは配列やオブジェクト全体をreadonlyのリテラルとして扱います。
よって内部プロパティを書き換えることはできません。

const obj = {test: 1} as const
obj.test = 2 // Cannot assign to 'test' because it is a read-only property.(2540)


const arr = [1, 2, 3] as const
arr[0] = 5 // Cannot assign to '0' because it is a read-only property.(2540)

arr.pop() // Property 'pop' does not exist on type 'readonly [1, 2, 3]'.(2339)

MySQL 8.0.18で導入されたEXPLAIN ANALYZEについて

概要

MySQL 8.0.18でEXPLAIN ANALYZEという句が導入されたのを知ったので、どんなものかメモします。
公式の説明はexplainのEXPLAIN ANALYZE による情報の取得という段落に記載されています。

以下の情報を取得できるようです。

  • 推定実行コスト
  • 戻された行の推定数
  • 最初の行を返す時間
  • すべての行 (実際のコスト) を返す時間 (ミリ秒)
  • イテレータによって返された行数
  • ループ数

実行されるコストや時間など、本番環境で実行する前に知っておきたい情報が揃ってますね。

EXPLAIN ANALYZEを実行してみる

下準備で3テーブル用意してそれぞれ1000行Insert

CREATE TABLE Users (
    UserID INT AUTO_INCREMENT PRIMARY KEY,
    UserName VARCHAR(50),
    Email VARCHAR(100) UNIQUE,
    SignUpDate DATE,
    ProfileDescription TEXT
);

CREATE TABLE Blogs (
    BlogID INT AUTO_INCREMENT PRIMARY KEY,
    UserID INT,
    Title VARCHAR(100),
    Content TEXT,
    PublishDate DATE,
    FOREIGN KEY (UserID) REFERENCES Users(UserID)
);

CREATE TABLE Comments (
    CommentID INT AUTO_INCREMENT PRIMARY KEY,
    BlogID INT,
    UserID INT,
    Comment TEXT,
    CommentDate DATE,
    FOREIGN KEY (BlogID) REFERENCES Blogs(BlogID),
    FOREIGN KEY (UserID) REFERENCES Users(UserID)
);

DELIMITER //

CREATE PROCEDURE InsertDummyData()
BEGIN
  DECLARE i INT DEFAULT 1;
  
  WHILE i <= 1000 DO
    INSERT INTO Users (UserName, Email, SignUpDate, ProfileDescription)
    VALUES (CONCAT('User', i), CONCAT('user', i, '@example.com'), CURDATE(), 'This is a sample description.');
    
    INSERT INTO Blogs (UserID, Title, Content, PublishDate)
    VALUES (i, CONCAT('Blog Post ', i), 'This is sample blog content.', CURDATE());
    
    INSERT INTO Comments (BlogID, UserID, Comment, CommentDate)
    VALUES (i, i, 'This is a sample comment.', CURDATE());
    
    SET i = i + 1;
  END WHILE;
END;

//
DELIMITER ;

CALL InsertDummyData();

インデックス有りでSQL実行

explain analyze select * from Users
inner join Blogs on Users.UserID = Blogs.UserID
inner join Comments on Comments.BlogID =  Blogs.BlogID;

インデックス有り結果

-> Nested loop inner join  (cost=802 rows=1000) (actual time=0.0995..4.66 rows=1000 loops=1)
    -> Nested loop inner join  (cost=452 rows=1000) (actual time=0.0836..2.98 rows=1000 loops=1)
        -> Filter: (comments.BlogID is not null)  (cost=102 rows=1000) (actual time=0.0588..1.03 rows=1000 loops=1)
            -> Table scan on Comments  (cost=102 rows=1000) (actual time=0.058..0.952 rows=1000 loops=1)
        -> Filter: (blogs.UserID is not null)  (cost=0.25 rows=1) (actual time=0.00163..0.00174 rows=1 loops=1000)
            -> Single-row index lookup on Blogs using PRIMARY (BlogID=comments.BlogID)  (cost=0.25 rows=1) (actual time=0.00148..0.00152 rows=1 loops=1000)
    -> Single-row index lookup on Users using PRIMARY (UserID=blogs.UserID)  (cost=0.25 rows=1) (actual time=0.00145..0.00149 rows=1 loops=1000)

インデックスが効かないjoinで実行

explain analyze select * from Users
inner join Blogs on Users.UserID = Blogs.UserID
inner join Comments on Comments.CommentDate =  Blogs.PublishDate;

インデックスが効かないjoin 結果

-> Inner hash join (comments.CommentDate = blogs.PublishDate)  (cost=100461 rows=100000) (actual time=3.27..179 rows=1e+6 loops=1)
    -> Table scan on Comments  (cost=0.0189 rows=1000) (actual time=0.00992..0.598 rows=1000 loops=1)
    -> Hash
        -> Nested loop inner join  (cost=452 rows=1000) (actual time=0.0427..2.65 rows=1000 loops=1)
            -> Filter: (blogs.UserID is not null)  (cost=102 rows=1000) (actual time=0.0286..1 rows=1000 loops=1)
                -> Table scan on Blogs  (cost=102 rows=1000) (actual time=0.0281..0.917 rows=1000 loops=1)
            -> Single-row index lookup on Users using PRIMARY (UserID=blogs.UserID)  (cost=0.25 rows=1) (actual time=0.00141..0.00144 rows=1 loops=1000)

Costが1000以下だったのが100461まで増加していますね。このように可視化して見れるのは面白いですね。

JestのExpected: ["object", "property"] Received: serializes to the same string について(オブジェクトの値検証)

概要

JestでObjectの値をtoBeで比較するとExpected: ["object", "property"] Received: serializes to the same stringというエラーが発生することがあります。
これはtoBeが厳密比較をしており、オブジェクトや配列の中身が一致していても同じとは判定されないためです。

JavaScriptやTypeScriptで以下のような比較をしても、値の一致ではなく同じメモリ上にあるオブジェクトかどうかで判定するため、一致しないと判定されることと同じ理屈です。

const data = {
  id: 1,
  name: 2,
}

const expected = {
  id: 1,
  name: 2,
}

console.log(data === expected) // false

対応策

tostrictequalを使うことでオブジェクトの値比較をすることができます。 https://jestjs.io/ja/docs/expect#tostrictequalvalue

const data = {
  id: 1,
  name: 2
};

const expected = {
  id: 1,
  name: 2
};

expect(data).toStrictEqual(expected) // true

なおtoEqualという比較関数も存在します。

https://jestjs.io/ja/docs/expect#toequalvalue

しかし、この関数はundefinedの値は評価しないため、{a: undefined, b: 2}{b: 2} は同じであると評価されます。
なのでより厳密に比較するtoStrictEqualがおすすめです。

PHPの曖昧判定を使う時に注意しておくべきこと

PHPの曖昧判定

PHPは厳密ではない比較をする関数がいくつかあります。
厳密比較をしないことによって意図しない判定をしてしまうことがあります。
今回はemptyを使った時の注意点を、実装とユニットテストの観点から見てみます。

ちなみに自分は絶対厳密比較しか許さないという派閥では全くなく、あくまで注意すべきくらいの気持ちでいます。

サンプルコードと注意点

例えば、空文字だった場合に処理したい内容を、emptyで比較するとします。
なお空文字以外で動作すると意図しない挙動になるコードと仮定します(サンプルは文字列返しているだけですが)。

実装

    public function checkEmpty(string $value)
    {
        if (empty($value)) {
            return "Value is empty";
        } else {
            return "Value is not empty";
        }
    }

テストコード

    public function test_空文字の時にValue is emptyを返すこと()
    {
        $sample = new Sample();
        $result = $sample->checkEmpty("");
        $this->assertSame($result, "Value is empty");
    }

    public function test_空文字の時にValue is not emptyを返すこと()
    {
        $sample = new Sample();
        $result = $sample->checkEmpty("a");
        $this->assertSame($result, "Value is not empty");
    }

一見このテストコードは返すパターンと返さないパターンをテストできているように見えます。
しかし、emptyは"0"もtrueを返します(PHP8.3現在)。

そのため、空文字の時のみ期待通りの動きをする、というテストが出来ていません。
なんなら実装自体も空文字の時だけ"Value is empty"を返す、という動きになっていません。

このような意図しない値の判定が生じうるケースは、if($var)などの判定でも起こり得ると思います。

エンジニアは前後のコードやこの関数がどのように呼ばれるか知っているため、"0"という値が現実的に入っていることはない、ということが分かってこのような実装をすることがあるかもしれません。実際問題ないケースがほとんどだと思います。
ただ、今後もこの関数がセーフティに使われることを保証するなら、曖昧な判定は避けたほうが好ましいと思います。

また、emptyif($var)を使う場合、判定の挙動を実装・ユニットテストで都度意識する必要があるため、実は厳密比較の方が考えることが少なくなるメリットもあると思います。

対応策

ではどうするかと言うと、なるべく厳密比較を用いるのが良いと思います。

修正したコード

    public function checkEmpty(string $value)
    {
        if ($value === "") {
            return "Value is empty";
        } else {
            return "Value is not empty";
        }
    }

上記コードは文字列を厳密比較しているので、"0"はelseに入ります。
またテストコードは先ほどのサンプルでしっかり返すパターンと返さないパターンをテスト出来ているようになりました。

曖昧にfalseyな値を判定することは便利なケースもありますし、使うべきではないとまでは思いませんが、意図しない判定にならないように注意することは大事ですね。