中安拓也のブログ

プログラミングについて書くブログ

【競プロ】TypeScriptでAtCoderをやる

はじめに

仕事でいつも使っているTypeScriptを競技プログラミング(AtCoder)でも使う方法について調べました。

AtCoderのTypeScriptの対応状況

AtCoderはTypeScriptに対応しているため、言語のセレクトボックスでTypeScript(3.8)を選択してあげればOKです。

f:id:l08084:20200906185552p:plain
AtCoderはTypeScriptに対応している

標準入力をどうやるか

競技プログラミングだと必須である標準入力をTypeScriptでどうやればいいのか調べました。 C++だとcinで済む標準入力ですが、TypeScriptだと下記のように書いてあげる必要があります。

import * as fs from 'fs';

const input = fs.readFileSync("/dev/stdin", "utf8");

標準入力の仕方がわかれば、あとは計算結果をおなじみのconsole.logで出力するだけになります。続いて、実際に簡単な問題を解いてみます。

実際に問題を解いてみる

提出の練習として出題される問題であるA - Welcome to AtCoderをTypeScriptで解いてみます。

f:id:l08084:20200906193209p:plain
該当問題の標準入力と標準出力の形式

C++でこの問題を解くと下記のようになります。

#include <iostream>

using namespace std;

int main()
{
  int a, b, c;
  string s;
  
  cin >> a;
  cin >> b >> c;
  cin >> s;
  
  cout << a + b + c << " " << s;
}

TypeScriptでこの問題を解くと次のようになります。

import * as fs from 'fs';

const input = fs.readFileSync("/dev/stdin", "utf8");

const a = +input.split('\n')[0];
const b = +input.split('\n')[1].split(' ')[0];
const c = +input.split('\n')[1].split(' ')[1];
const s = input.split('\n')[2];

console.log(`${a + b + c} ${s}`);

fs.readFileSync("/dev/stdin", "utf8")で取得した標準入力の文字列から、split('\n')split(' ')を使って整数と文字列を取り出しています。

参考サイト

Node.jsの標準入力と - Qiita

TypeScriptでAtCoderに挑戦してみる - Qiita

【Java】BigDecimal型を使う

はじめに

仕事で金利に関するロジックを書いた時に、JavaのBigDecimal型を使ったので、BigDecimal型の使い方についてメモすることにしました。

なぜBigDecimal型を使うのか

BigDecimalには下記のような特徴があるため、誤差・意図しない値の丸めが発生することが許されない金額の計算のようなケースでは、float, double型ではなくBigDecimalの使用が推奨されます。

1. float, double型と違って少数の計算で誤差が発生しない

BigDecimalと同様に少数を表現する型として、float型とdouble型があります。float型とdouble型は、値を2進数で保持する関係上、循環小数としてしか表現できない10進数の少数が存在するため、少数を計算する過程で値を丸める必要があり、その過程で計算結果に誤差が発生してしまいます。 その反面、BigDecimal型では、値を「整数 * 10の何乗」という形式で保存しているため、10進数の有限小数のすべてを誤差なく表現することができます。

2. 値の丸めかたを指定することができる

BigDecimal型では、値の計算をすべてメソッド経由で実施するため、値の丸めかたを指定することができます。

// 1. float, double型では、少数の計算で誤差が発生するケースがある
double doubleAnswer = 3.3 / 1.1;
System.out.println("double: " + doubleAnswer); // double: 2.9999999999999996

BigDecimal bigDecimalAnswer = BigDecimal.valueOf(3.3).divide(BigDecimal.valueOf(1.1));
System.out.println("bigDecimal: " + bigDecimalAnswer); // bigDecimal: 3

// 2. 値の丸めかたを指定することができる
double doubleAnswer2 = 10.0 / 3.0;
System.out.println("double: " + doubleAnswer2); // double: 3.3333333333333335

BigDecimal bigDecimalAnswer2 = BigDecimal.valueOf(10.0).divide(BigDecimal.valueOf(3.0), 2, RoundingMode.HALF_UP);
System.out.println("bigDecimal: " + bigDecimalAnswer2); // bigDecimal: 3.33

BigDecimalの生成

数値からBigDecimalを生成するときは、valueOfメソッドを使用し、文字列から生成する場合は、コンストラクタを使用します。

BigDecimal value1 = BigDecimal.valueOf(1234.56);// 数値からの生成
BigDecimal value2 = new BigDecimal("1234.56");// 文字列から生成

BigDecimalの定数

0, 1, 10は定数として定義されています。

BigDecimal.ZERO
BigDecimal.ONE
BigDecimal.TEN

値の比較

equalsメソッド使用した場合、値のscaleが一致していないと、値が同一でもfalseが出力されてしまいます。

System.out.println(BigDecimal.valueOf(0.0).equals(BigDecimal.valueOf(0))); // 値のscaleが異なるため、falseになってしまう
System.out.println(BigDecimal.valueOf(0).equals(BigDecimal.valueOf(0))); // true

そのため、BigDecimalで値の比較を行う場合は、compareToメソッドを使用しましょう。compareToメソッドでは値は等しいがscaleが異なるBigDecimalオブジェクトを等しいとみなします。(例えば、2.0と2.00は等しいとみなされる)

BigDecimal one = BigDecimal.valueOf(1.00);
System.out.println(one.compareTo(BigDecimal.ZERO)); // 1
System.out.println(one.compareTo(BigDecimal.ONE)); // 0
System.out.println(one.compareTo(BigDecimal.TEN)); // -1

compareToメソッドでは、数値がパラメーターの値よりも小さい場合は、-1、等しい場合は0、大きい場合は1を返します。

加算

BigDecimal three = BigDecimal.valueOf(3.00);
BigDecimal four = BigDecimal.valueOf(4.0);

System.out.println(three.add(four)); // 7.0

減算

BigDecimal three = BigDecimal.valueOf(3.00);
BigDecimal four = BigDecimal.valueOf(4.0);

System.out.println(three.subtract(four)); // -1.0

乗算

BigDecimal three = BigDecimal.valueOf(3.00);
BigDecimal four = BigDecimal.valueOf(4.0);

System.out.println(three.multiply(four)); // 12.00

除算

scaleを指定せずに、divideメソッドを使うこともできますが、割り切れないときにArithmeticExceptionが発生するのでscale指定版を使用するほうが無難です。

BigDecimal three = BigDecimal.valueOf(3.00);
BigDecimal four = BigDecimal.valueOf(4.0);

System.out.println(three.divide(four, 3, RoundingMode.HALF_UP)); // 0.750

参考サイト

BigDecimal (Java Platform SE 8)

【Java】BigDecimalをちゃんと使う~2018~ - Qiita

BigDecimalの使い方 | Java好き

なぜBigDecimalを使わなければならないのか | Java好き

Java9以降のBigDecimalの小数点切り上げ・切り捨て・四捨五入-スケ郎のお話

【Angular】ngClassで三項演算子を使う

はじめに

AngularのngClass(テンプレート内でclass属性を動的に変更する機能)で三項演算子を使う方法を説明します。

<div class="block" [ngClass]="isRed ? 'red' : 'blue'"></div>

環境

  • Angular: 8.2.14

実装

f:id:l08084:20200718172344p:plain
isRed が true の時は赤色になる

ngClassで三項演算子を使う方法について説明するために、isRedtrueの時に赤色になり、isRedがfalseの時に青色になるプログラムを実装します。

まず、三項演算子を使わない書き方の例です。

app.component.scss

.block {
  border: solid 3px black;
  height: 200px;
  margin: 20px;
  width: 400px;

  &.red {
    background-color: red;
  }

  &.blue {
    background-color: blue;
  }
}

app.component.html

<div class="block" [ngClass]="{ red: isRed, blue: !isRed }"></div>

f:id:l08084:20200718192254p:plain
isRed が false の時は青色になる

続いて、三項演算子を使った場合の書き方の例です。

app.component.html

<div class="block" [ngClass]="isRed ? 'red' : 'blue'"></div>

三項演算子を使っていない場合と比較すると、{}で囲っていない、class名を''で囲っているといった違いがあります。

三項演算子を使うと条件文を一度書くだけで済むため、条件文が長い場合などは、三項演算子を使ったほうがすっきりします。

参考サイト

AngularのngClassで条件に応じてスタイルを切り替える(三項演算子) - Qiita

【Angular + Firebase】アプリケーションをデプロイする

はじめに

AngularとFirebaseを使って作成したアプリをFirebaseのHostingという機能を使ってデプロイします。

前提条件

  • Angularプロジェクトは作成ずみ
  • FirestoreやAutheticationを使用するためにFirebaseのプロジェクトも既に作成している

環境

ng --versionコマンドの実行結果

Angular CLI: 8.3.20
Node: 12.13.1
OS: darwin x64
Angular: 8.2.14
... animations, common, compiler, compiler-cli, core, forms
... language-service, platform-browser, platform-browser-dynamic
... router

Package                           Version
-----------------------------------------------------------
@angular-devkit/architect         0.803.20
@angular-devkit/build-angular     0.803.20
@angular-devkit/build-optimizer   0.803.20
@angular-devkit/build-webpack     0.803.20
@angular-devkit/core              8.3.20
@angular-devkit/schematics        8.3.20
@angular/cdk                      7.3.7
@angular/cli                      8.3.20
@angular/fire                     5.2.3
@angular/material                 7.3.7
@ngtools/webpack                  8.3.20
@schematics/angular               8.3.20
@schematics/update                0.803.20
rxjs                              6.5.3
typescript                        3.5.3
webpack                           4.39.2

デプロイ準備

では、Firebase Hostingへのデプロイの準備を初めていきます。まず、ターミナルからfirebaseを扱うツールをグローバルインストールします。

sudo npm i -g firebase-tools

インストールが終わったら下記のコマンドでfirebaseにアクセスします。

firebase login

firebase loginコマンドを実行すると、Firebaseがエラーレポートを収集していいか聞かれますので、お好みでYかnを入力してエンターキーを押下します。

i  Firebase optionally collects CLI usage and error reporting information to help improve our products. Data is collected in accordance with Google's privacy policy (https://policies.google.com/privacy) and is not used to identify you.

? Allow Firebase to collect CLI usage and error reporting information? Yes
i  To change your data collection preference at any time, run `firebase logout` and log in again.

質問に答えると、ブラウザが開かれてGoogleアカウントでのログインを求められるので、Firebaseプロジェクトを作成した時のGoogleアカウントでログインします。

f:id:l08084:20200620175304p:plain
Googleアカウントでのログインを求められる

firebaseへのログインが完了したら、firebase initというコマンドをターミナルに入力します。

firebase init

コマンドを入力すると、Firebaseのどの機能をセットアップするか聞かれるので、Hostingを選択します。

f:id:l08084:20200620175806p:plain
Hostingを選択する

続いて、「このディレクトリーに対してデフォルトで設定するFirebaseプロジェクトを選択してください」という質問が表示されます。

FirestoreやAutheticationを使用するためにFirebaseのプロジェクトを既に作成ずみなので、Use an existing projectを選択します。

=== Project Setup

First, let's associate this project directory with a Firebase project.
You can create multiple project aliases by running firebase use --add, 
but for now we'll just set up a default project.

? Please select an option: 
  Use an existing project 
  Create a new project 
❯ Add Firebase to an existing Google Cloud Platform project 
  Don't set up a default project

Use an existing projectを選択した後に、Firebaseプロジェクトの一覧が表示されるので、該当のプロジェクトを選択します。(今回はtaikinプロジェクトを選択する)

First, let's associate this project directory with a Firebase project.
You can create multiple project aliases by running firebase use --add, 
but for now we'll just set up a default project.

? Please select an option: Use an existing project
? Select a default Firebase project for this directory: taikin (taikin)
i  Using project taikin (taikin)

続いて、「公開用のディレクトリーとしてどのディレクトリを使用するか」質問されるので、dist/project_nameを入力します。(今回はdist/kintai)

=== Hosting Setup

Your public directory is the folder (relative to your project directory) that
will contain Hosting assets to be uploaded with firebase deploy. If you
have a build process for your assets, use your build's output directory.

? What do you want to use as your public directory? 

最後に、SPAアプリケーションかどうか質問されるので、YESと回答します。

? Configure as a single-page app (rewrite all urls to /index.html)? Yes

このように質問に回答していくと、次のようなfirebase.jsonファイルが作成されます。

もし、質問に対する回答を間違えても最終的にfirebase.jsonファイルを手動で修正すれば大丈夫です。

{
  "hosting": {
    "public": "dist/project_name",
    "ignore": [
      "firebase.json",
      "**/.*",
      "**/node_modules/**"
    ],
    "rewrites": [{
      "source": "**",
      "destination": "/index.html"
    }]
  }
}

アプリケーションを本番ビルドする

Angularプロジェクトを本番ビルドして、dist配下に成果物が作成されるのを確認します。

もし、src/enviroment.prod.tsファイルに適切な設定をしていない場合は、事前に設定を完了させておく必要があります。今回は本番環境と開発環境で同じFirebaseのプロジェクトを使用しているため、src/enviroment.tsの内容をそのままsrc/enviroment.prod.tsにコピーしました。

ng build --prod

デプロイ実施

ng build --prodによる本番ビルドが完了したら、firebase deployコマンドを実施して、いよいよデプロイを実施します。

$ firebase deploy

=== Deploying to 'taikin'...

i  deploying hosting
i  hosting[taikin]: beginning deploy...
i  hosting[taikin]: found 25 files in dist/kintai
✔  hosting[taikin]: file upload complete
i  hosting[taikin]: finalizing version...
✔  hosting[taikin]: version finalized
i  hosting[taikin]: releasing new version...
✔  hosting[taikin]: release complete

✔  Deploy complete!

Project Console: https://console.firebase.google.com/project/taikin/overview
Hosting URL: https://taikin.web.app

デプロイが正常に完了すると、上記のようにURLが表示されるので、デプロイしたアプリケーションにアクセスすることができます。

参考サイト

Firebase CLIでAngularアプリをFirebase Hostingにデプロイする - Qiita

5分でAngularチュートリアルアプリをFirebaseにデプロイする方法 - Qiita

Angular + Firebase でメモアプリを作りました

f:id:l08084:20200620195338p:plain
作ったメモアプリ。名前はスリーメモ

はじめに

Firebaseの勉強のためにスリーメモというアプリを作りました。Evernoteみたいなメモアプリです。モバイル対応はまだできていないのですが、パソコンで何かメモ取りたいんだよね〜という人は試してみてください。

スリーメモのURL: https://taikin.web.app/lp

スリーメモの機能

スリーメモの機能について紹介します。

SNS認証

f:id:l08084:20200620201505p:plain
スリーメモのログイン画面

FacebookとTwitterのアカウントを持っている場合は、SNSのアカウントを使ってスリーメモのアカウント登録・認証を実施することができます。

フォルダ

f:id:l08084:20200620204142p:plain
フォルダでメモを分類

フォルダを作成することでメモを分類ごとに整理することができます。

スリーメモで使われている技術

スリーメモは、フロントエンドフレームワークにAngular、CSSフレームワークにAngular Materialを使用しています。Firebaseの機能のうち、Hosting, Authentication, Firestoreの3種類の機能を使用しています。

【JavaScript】[バグ]配列にemptyという要素が挿入されてしまう

はじめに

TypeScriptを使用し、配列に配列の 要素数 + 1 の数値を代入するメソッドを3回呼んだところ、配列が[1, 2, 3]ではなく、[empty, 1, empty, 3, empty, 5]になってしまった。

本事象が発生した誤ったコード

  public array: number[] = [];

  constructor() {
    this.addArray();
    this.addArray();
    this.addArray();
    console.log(this.array);
    // [1, 2, 3]ではなく、[empty, 1, empty, 3, empty, 5]と表示されてしまう
  }

  /**
   * 配列に 配列の要素数 + 1 の数字を代入する
   *
   * @memberof AppComponent
   */
  public addArray() {
    let item = this.array.length++;
    this.array.push(item);
  }

環境

  • typescript: 3.5.3

本事象が発生した原因

this.array.length++の部分でArray.lengthに1を加算することによって、意図せず配列の要素数(Array.length)を一つ増加し空白(empty)を作ってしまっていることが原因となります。

Array.lengthは書き込み可能なプロパティであるため、加算するとその分配列の要素数が増加して空白ができてしまいます。

例えば、次の例のように空の配列のArray.lengthに5を加算するとemptyが5件代入されている配列が作成されます。

    this.array = [];
    this.array.length += 5;
    console.log(this.array); // [empty × 5]

最初の誤ったコードを意図した通りに動くように修正すると次のようなコードになります。

  /**
   * 配列に 配列の要素数 + 1 の数字を代入する
   *
   * @memberof AppComponent
   */
  public addArray() {
    let item = this.array.length;
    this.array.push(++item);
  }

参考サイト

Push an empty element into javascript array - Stack Overflow

Array.length - JavaScript | MDN

【Angular + Firebase】ログイン中のアカウントの情報を表示する

f:id:l08084:20200606211323p:plain
ログイン中のアカウントの情報を表示している

はじめに

ログインしているアカウントのサムネイル画像とアカウント名を表示する機能を実装します。

環境

フロントサイドのフレームワークとしてAngularを、バックエンドにFirebaseを使用しています。CSSフレームワークはAngular Materialです。

  • Angular CLI@8.3.20
  • Node@12.13.1
  • Angular@8.2.14
  • firebase@6.3.4
  • angular/fire@5.2.1

ログイン中のアカウントの情報を表示する

それでは、ヘッダー上にログイン中のアカウントの情報を表示する機能を実装していきます。

認証のサービスクラスを作成する

まず、ログインしているアカウントの情報を取得するため、認証関連の処理をまとめたサービスクラスであるAuthenticationServiceを作成します。

authentication.service.ts

import { Injectable } from '@angular/core';
import { AngularFireAuth } from '@angular/fire/auth';
import { Observable } from 'rxjs';
import { User } from 'firebase';

/**
 * 認証関連のサービスクラス
 *
 * @export
 * @class AuthenticationService
 */
@Injectable({
  providedIn: 'root'
})
export class AuthenticationService {
  constructor(public afAuth: AngularFireAuth) {}

  /**
   * ログインしているアカウントの情報を返す
   *
   * @returns {User}
   * @memberof AuthenticationService
   */
  public getCurrentUser(): User {
    return this.afAuth.auth.currentUser;
  }

  /**
   * ログアウトする
   *
   * @returns {Promise<void>}
   * @memberof AuthenticationService
   */
  public signOut(): Promise<void> {
    return this.afAuth.auth.signOut();
  }

  /**
   * ログインしているアカウントの情報を返す
   *
   * @returns {Observable<User>}
   * @memberof AuthenticationService
   */
  public getUser(): Observable<User> {
    return this.afAuth.user;
  }
}

AuthenticationServiceは、ログインしているアカウントの情報を返すメソッドや、ログアウトするメソッドを持っています。

ヘッダーにアカウントの情報を表示する

HTMLテンプレート

ヘッダー上でアカウントの情報を表示している部分のテンプレート(HTML)について説明します。

header.component.html

<!-- ログインアカウント情報(ログインしている時のみ表示される) -->
<!-- ログインしているアカウントの情報を表示したい -->
<div class="right-icon" *ngIf="authenticationService.getUser() | async">
  <button mat-button class="account-link" [matMenuTriggerFor]="menu">
    <img class="thumbnail" *ngIf="currentUser.photoURL" src="{{currentUser.photoURL}}" />
    {{currentUser.displayName}}<mat-icon>expand_more
    </mat-icon>
  </button>
  <!-- Angular Materialのメニューを使用している -->
  <mat-menu #menu="matMenu">
    <button mat-menu-item (click)="signOut()">
      <mat-icon>exit_to_app</mat-icon>ログアウト
    </button>
  </mat-menu>
</div>

*ngIf="authenticationService.getUser() | async"と書くことでログイン時のみ(アカウントが存在する時のみ)にヘッダー上にアカウントの情報を表示することができます。

また、photoURLプロパティとdisplayNameを表示することでアカウントのサムネイル画像とアカウント名を表示しているのと、Angular MaterialのMenuを使用することで、アカウント名をクリックするとメニューでログアウトボタンを表示する仕様になっています。

コンポーネントクラス

コンポーネントクラスではAuthenticationServiceを呼び出すことで、ログアウト、アカウント情報の取得処理を実装しています。

header.component.ts

import { Component, OnInit, Input, EventEmitter, Output } from '@angular/core';
import { SpinnerService } from 'src/app/services/spinner.service';
import { Router } from '@angular/router';
import { ToastService } from '../../services/toast.service';
import { AuthenticationService } from 'src/app/services/authentication.service';
import { Observable } from 'rxjs';

/**
 * 画面ヘッダーのコンポーネントクラス
 *
 * @export
 * @class HeaderComponent
 * @implements {OnInit}
 */
@Component({
  selector: 'app-header',
  templateUrl: './header.component.html',
  styleUrls: ['./header.component.scss']
})
export class HeaderComponent implements OnInit {
  @Input() isHandset$: Observable<boolean>;
  @Output() drawerToggled = new EventEmitter<void>();

  public currentUser: firebase.User;

  constructor(
    private router: Router,
    private _toastService: ToastService,
    private authenticationService: AuthenticationService,
    private spinnerService: SpinnerService
  ) {}

  ngOnInit() {
    this.retrieveUserProfile();
  }

  /**
   * ログアウト処理
   *
   * @memberof HeaderComponent
   */
  public async signOut() {
    // スピナー表示
    this.spinnerService.show();
    // ログアウトAPIを呼び出す
    try {
      await this.authenticationService.signOut();
      // ログアウトが成功したら、ログイン画面に遷移
      this.router.navigate(['/login']);
      this._toastService.open('ログアウトしました。');
    } catch (error) {
      this._toastService.open('ログアウトに失敗しました。');
      console.log(error);
    } finally {
      this.spinnerService.hide();
    }
  }

  /**
   * ログインしているユーザーの情報を取得する
   *
   * @private
   * @memberof HeaderComponent
   */
  private retrieveUserProfile() {
    this.currentUser = this.authenticationService.getCurrentUser();
  }
}

header.component.scss

.right-icon {
  align-items: baseline;
  cursor: pointer;
  display: flex;
  margin: 0 0 0 auto;

  .thumbnail {
    border-radius: 12px;
    width: 24px;
    height: 24px;
  }
}

上記のようにコードを書くと、ヘッダーにアカウントの情報を表示することができるようになります。

f:id:l08084:20200607185126p:plain
Twitterでログインした場合の表示例

参考サイト

Firebase でユーザーを管理する