20200405のJavaScriptに関する記事は26件です。

【async/await】JavaScriptで非同期処理プログラムを作成する【標準入力】

はじめに

「解きながら学ぶJava」(その名の通り「〜〜するプログラムを作成せよ。」といった問題を解きながらJavaを学んでいく書籍)に書かれている問題を"JavaScriptで書く"ということをやっている中で見事に詰まりました。

JavaScriptは、JavaやRubyのようにクラスを用いて綺麗に定義、みたいなことがしづらいですし、標準入力も若干面倒なので結構苦戦しました
(今回はreadlineモジュールを使用)

ちなみに私は、上記の本を持っておきながら、Javaを触ったことはありません。笑

どんな問題?

キーボードから次々と整数値を読み込んで、合計と平均を求めるプログラムを作成せよ。加算する整数の個数は最初に読み込むこと。

という問題です。

実際に書いたプログラム

4_26.js
const readline = require('readline');
const log = console.log;
const rl = readline.createInterface({
    input: process.stdin,
    output: process.stdout
});

let num_int;
let input_int;
let sum = 0;

async function readInt() {
    const rl = readline.createInterface({input: process.stdin});
    const ait = rl[Symbol.asyncIterator]();
    log('整数:');
    input_int = parseInt((await ait.next()).value);
};

function checkInt() {
    rl.question('何個加算しますか: ', async function (answer) {
        num_int = parseInt(answer);
        for(let i = 0; i < num_int; i++) {
            await readInt();
            sum += input_int;
        }
        rl.close();
        log(`合計は${sum}です。`);
        log(`平均は${sum / num_int}です。`);
    });
};

log('整数を加算します。');
checkInt();

コードの中身は深くは追いませんが、ざっくりポイントだけ記載します。

  • 大元の処理を行うcheckInt関数に、「何個加算しますか?」の問に対して入力された数の回数分だけfor文で処理をループさせている

  • checkInt関数とは別に、readInt関数を定義し、加算対象の数を入力する処理を書いている

  • 標準入力は、readlineモジュールを使用し、繰り返し入力が必要になる部分にasync/awaitを使用

async/awaitを使用することで非同期処理が可能となり、コンソールへの繰り返し入力が可能となります。

実行するとこんな感じになります(↓)

整数を加算します。
何個加算しますか: 3
整数:
48
整数:
67
整数:
89
合計は204です。
平均は68です。

上記のコードはあくまで一例ですので、ご参考までに。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Rails]画像選択時にプレビュー表示

本記事投稿のいきさつ

アプリの中で画像投稿機能を実装したとき、ただフォームを作成するだけでは画像を選択しても表示がされず何を選んだのか確認をすることが出来ません。
そこで、画像を選択した時点でプレビュー表示することができればいいなと思い、実際に機プレビュー機能を作成したため、記録として残したいと思います。
今回はユーザーのプロフィール画像の編集画面を想定します。
そのため、既に登録されている画像は最初からプレビューさせた状態で表示をさせます。

前提

  • 変種画面はprofile_edit.html.haml
  • 画像の保存先は Usersテーブル: imageカラム
  • file_field本体は隠し、画像が選択されていない時はiconを、 選択されている時はプレビュー画像をクリックすることで画像選択できるようにします。
  • jQueryを使用しますが、必要なGemのインストール等は既に出来ているとします。
  • ユーザー機能はDeviseを使用しています。
  • cssは今回の内容に含まれません。

フォーム作成

まず、今回はhamlでビューのフォームを作成します。

profile_edit.html.haml
= form_for current_user, url: {action: 'profile_update'} do |f|

    .form-group 
      .image_form
        .image_form__contents
          -# ラベルでfile_fieldとicon、プレビュー画像を紐付けます
          = f.label :image, class: 'image_label' do
            .prev-contents
              -# 既に登録されている画像があれば表示をさせます
              - if current_user.image.present?
                .prev-content
                  = image_tag current_user.image.url, alt: "preview", class: "prev-image"
              -# 既に登録されている画像がなければiconを表示させます
              - else
                = icon('fas', 'image', class: 'photo-icon')
            -# file_fieldはdisplay: none;で隠します
            = f.file_field :image, class: 'image_form__contents__field hidden_file'

FileReader

今回はFileReaderを使用します。
FileReaderとはHTML5世代の機能でユーザーのPC内にあるファイルやバッファ上の生データに対して、読み取りアクセスを行えるオブジェクトです。

jsファイルの編集

今回はimage_preview.jsを作成して、そこに記述していきます。

image_preview.js
$(document).on('turbolinks:load', function () {
  $(function () {
    // 画像をプレビュー表示させる.prev-contentを作成
    function buildHTML(image) {
      var html =
        `
        <div class="prev-content">
          <img src="${image}", alt="preview" class="prev-image">
        </div>
        `
      return html;
    }

    // 画像が選択された時に発火します
    $(document).on('change', '.hidden_file', function () {
      // .file_filedからデータを取得して変数fileに代入します
      var file = this.files[0];
      // FileReaderオブジェクトを作成します
      var reader = new FileReader();
      // DataURIScheme文字列を取得します
      reader.readAsDataURL(file);
      // 読み込みが完了したら処理が実行されます
      reader.onload = function () {
        // 読み込んだファイルの内容を取得して変数imageに代入します
        var image = this.result;
        // プレビュー画像がなければ処理を実行します
        if ($('.prev-content').length == 0) {
          // 読み込んだ画像ファイルをbuildHTMLに渡します
          var html = buildHTML(image)
          // 作成した.prev-contentをiconの代わりに表示させます
          $('.prev-contents').prepend(html);
          // 画像が表示されるのでiconを隠します
          $('.photo-icon').hide();
        } else {
          // もし既に画像がプレビューされていれば画像データのみを入れ替えます
          $('.prev-content .prev-image').attr({ src: image });
        }
      }
    });
  });
});

上記で出てくるDataURISchemeとは、
簡単にいうと、画像やらJavascriptやらそういったHTMLのコンテンツを文字列として定義出来るものです。
以上でで、画像が表示されます。

終わり

最後まで見ていただきありがとうございました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

GitHub の「Fork」ボタンを「パクる」に置き換える UserScript

// ==UserScript==
// @name         Replace Fork with パクる for GitHub
// @namespace    https://foooomio.net/
// @version      0.1
// @description  だから、そういうことじゃなくて?
// @author       foooomio
// @license      MIT License
// @match        https://github.com/*
// @run-at       document-idle
// @grant        none
// ==/UserScript==

// 元ネタ: https://twitter.com/IiToshihide/status/1246487047545556992

(() => {
  'use strict';

  const $ = document.querySelector.bind(document);

  function main() {
    $('.pagehead-actions .octicon-repo-forked').nextSibling.textContent = 'パクる';
  }

  new MutationObserver(main).observe(
    $('#js-repo-pjax-container'),
    { childList: true }
  );

  main();
})();
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【プログラミング学習4日目】JavaScriptをかじってみた

はじめに

独学プログラミング学習の4日目

忘れっぽいので備忘録代わりに残しておこうと思う
※人に見せる前提ではないよ

目標

  • 簡単なページをサクっと作れるようになる
  • Web上に自分のポートフォリオを公開
  • アウトプットのスキルを身につける

JavaScriptってなに?

こちらを見てみると,動的にコンテンツを更新したり,マルチメディアを管理したり,その他多くのことができるスクリプト言語と書いてある

つまりどういうことだってばよ?となっているのだが,
おそらくページ内のものを動かしたり,なにかのアクションをトリガーとして動作するプログラムを作れるということだと思っておく
使ってみればわかるよね

実際に使ってみた

今回作ったのは,こちら

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>JavaScriptのお勉強</title>
</head>
<body>
  <input type="text" id="message" placeholder="ここに文字を入力">
  <select id="count">
    <option value="1">1</option>
    <option value="2">2</option>
    <option value="3">3</option>
    <option value="4">4</option>
    <option value="5">5</option>
    <option value="10">10</option>
  </select>
  <button class="submit">送信</button>
<script src="./jslearning.js"></script>
</body>
</html>
'use strict';

const displayText = () => {
  const texts = document.getElementById("message").value;
  const counts = document.getElementById("count").value;
  for(let i = 0; i < counts; i++){
    let p = document.createElement('p');
    p.textContent = texts;
    document.body.appendChild(p);
  }
}
const buttons = document.querySelectorAll('button');
for(let i = 0; i < buttons.length ; i++) {
  buttons[i].addEventListener('click', displayText);
}

テキストボックスに入力した文字列を,セレクトボックスで選択した数値の分だけ表示させるプログラムですね.
今回,これを作るにあたって調べた項目は,

  1. inputとselectのvalueを取得する方法
  2. for以外の繰り返し処理

この2つだけ!

HTMLから値を取得する

HTMLから値を取得する方法はいくつかあるけど,今回はdocumentオブジェクトを使ったよ
理由はなんとなく,それが一番基本になんだと思い込んでいたから.

Documentオブジェクトとは,DOMツリー(HTMLを木構造にした状態のこと)を取得して,ドキュメントの操作ができるようになるすごいやつだよ
難しい事は考えないで,HTMLの操作ができるって思ってくれればいいよ

さて,こいつ↓
<input type="text" id="message" placeholder="ここに文字を入力">
の値を取得してるのが,
JavaScriptにかかれているこいつ↓
const texts = document.getElementById("message").value;
だよ.
今回はidをキーにして値を取得したけど,classとかnameとかで探すこともできるよ.

セレクトボックス↓
<select id="count">
~~~
</select>

から値を取得してるのが,こいつ↓
const counts = document.getElementById("count").value;

値が取れればあとはforで表示させるだけだよ

for以外の繰り返し処理

JavaScriptの繰り返し処理にはたくさんの種類があるよ
細かい特徴とかは調べてないけど,

name 説明
for 基本
while 無限ループ楽しい
foreach 配列くるくる回せる
for in オブジェクトを回せるっぽい
for of オブジェクトを回せるっぽい
map 戻り値がある
filter 配列に入ってる値を計算して返してくれるらしい

こんな感じかな?
いっぱいあるからどの場面で何を使うかとかわからないんだけど,いい見分け方とかないかな?

おわり

いろいろ調べたけど,考えが散らばりすぎたので今日は一旦ここまで
JavaScriptはHTMLを操作して,なにかのイベント(クリックとか)後に表示させたい時とかに使うってことはわかったよ
テトリスとかお絵かきの森が作れるんだよ!って聞いたけど,どうやるんだろう・・・・

明日はcallback関数について調べてみようかな

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【node.js】validationを新しいバージョンに対応コードに変更する

解決したい問題

パッケージvalidationのバージョンが古いので新しいバージョンの書き方に変更したい。

環境

OS: macOS
express: ^4.17.1
ejs: ^2.6.2
express-validator: ^6.4.0

変更前のコード(古いコード)

router.post('/', (req, res, next) => {
    var request = req;
    var response = res;
    req.check('name', 'NAMEは必ず入力して下さい。').notEmpty();
    req.check('password', 'PASSWORDは必ず入力して下さい。').notEmpty();
    req.getValidationResult().then((result) => {
        if (!result.isEmpty()) {
            var content = '<ul class="error">';
            var result_arr = result.array();
            for(var n in result_arr) {
                content += '<li>' + reault_arr[n].msg + '</li>'
            }
            content += '</ul>';
            var data = {
                title: 'Login',
                content: content,
                form: req.body
            }
            response.render('login', data);
        } else {
            var nm = req.body.name;
            var pw = req.body.password;

            User.query({where: {name: nm}, andWhre: {password: pw}})
              .fetch()
              .then((model) => {
                  if (model == null) {
                      var data = {
                          title: '再入力',
                          content: '<p class="error">名前またはパスワードが違います</p>',
                          form: req.body
                      };
                      respose.render('login', data);
                  } else {
                      request.session.login = model.attributes;
                      var data = {
                          title: 'Login',
                          content: '<p>ログインしました!<br>トップページに戻ってメッセージを送信下さい。</p>',
                          form: req.body
                      }
                      respose.render('login', data);
                  }
              });
        }
    })
});

このreq.checkがエラーになる。

変更後

上に↓のコードを書き足す。

const { check, validationResult } = require('express-validator');
router.post('/', 

[
    check('name', 'NAMEは必ず入力して下さい。').notEmpty(),
    check('password', 'PASSWORDは必ず入力して下さい。').notEmpty()
],

(req, res, next) => {
    var request = req;
    var response = res;

    const errors = validationResult(req);

    if (!errors.isEmpty()) {
        var content = '<ul class="error">';
        var result_arr = errors.array();
        for(var n in result_arr) {
            content += '<li>' + result_arr[n].msg + '</li>'
        }
        content += '</ul>';
        var data = {
            title: 'Login',
            content: content,
            form: req.body
        }
        response.render('login', data);
    } else {
        var nm = req.body.name;
        var pw = req.body.password;

        User.query({where: {name: nm}, andWhere: {password: pw}})
            .fetch()
            .then((model) => {
                if (model == null) {
                    var data = {
                        title: '再入力',
                        content: '<p class="error">名前またはパスワードが違います</p>',
                        form: req.body
                    };
                    response.render('login', data);
                } else {
                    request.session.login = model.attributes;
                    var data = {
                        title: 'Login',
                        content: '<p>ログインしました!<br>トップページに戻ってメッセージを送信下さい。</p>',
                        form: req.body
                    }
                    response.render('login', data);
                }
            }).catch((error) => {
                var data = {
                  title: '再入力',
                  content: '<p class="error">名前またはパスワードが違います。</p>',
                  form: req.body
                };
                res.render('login', data);
                console.log(error);
            });
    }
});

大きく変わったのはcheckの位置です。

参考

↓この本の学習中に、バージョンが古くてエラーになった。
https://www.amazon.co.jp/Node-js%E8%B6%85%E5%85%A5%E9%96%80-%E7%AC%AC2%E7%89%88-%E6%8E%8C%E7%94%B0-%E6%B4%A5%E8%80%B6%E4%B9%83/dp/4798055220

↓最新バージョンの参考
https://express-validator.github.io/docs/

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【React + Material-UI】数字当てゲームを作ろう!【2020年4月版】

はじめに

この記事は「Reactのチュートリアルを終えて、何かを作ってみたい」という読者を想定しています。
分からない部分が出た場合はReactの公式ドキュメントに立ち返りましょう。

material-uiの公式ドキュメントも要チェック

前にReact+Material-UIで作ったもの

今回作ったもの

React Number Guess
ランダムに生成された1~100の数字を推測するゲームです。「60より上ですか?」などの質問をしていき、数字が絞れたら「〇〇ですか?」で正解を出します。右側に予想履歴が表示されます。
スクリーンショット 2020-04-05 13.52.31.png

Githubのソースコード

戦略

ReactコンポーネントにはMaterial-UIを使い、保持するstateをReactのuseStateで実装します
また、今回はuseEffectを使い、読み込み時に1~100のランダムな数字を生成します。

component, containerの洗い出し

component

  • 数字ボタン: 0~9の数字を入力できるボタンコンポーネント。色はピンクで固定。

スクリーンショット 2020-04-05 17.58.53.png

  • 大きいボタン: 「より下ですか?」「クリア」「予想する」ボタンなど、数字ボタン以外に使うコンポーネント。色をpropsで指定できる。

スクリーンショット 2020-04-05 17.59.07.png

  • ディスプレイ: 現在の質問を表示するコンポーネント

スクリーンショット 2020-04-05 17.59.53.png

  • 予想履歴: いままでの予想の一覧を表示するコンポーネント

スクリーンショット 2020-04-05 18.00.34.png

  • 正解モーダル: 正解したときに表示されるモーダルコンポーネント

スクリーンショット 2020-04-05 18.26.58.png

その他、タイトルと説明はコンポーネントにせず、containerにそのまま書きます。

container

  • NumberGuess: すべてのコンポーネントを集めて表示するコンテナ

stateの洗い出し

  1. inputNumber: 予想に使う数字(int)
  2. questionContent: 質問の内容。"より下ですか?"、"より下ですか?"、"ですか?"の3種類(string)
  3. history: 質問の履歴が格納された配列(array)。 [[34,"より下ですか?", "はい"],[17,"より下ですか?", "はい"],[7,"より上ですか?", "はい"]] のような2次元配列
  4. answer: 答えの数字。はじめにuseEffectでランダムに設定される(int)
  5. open: 正解したときにmodalを開くために必要なboolean
  6. guessTimes: 質問した回数(int)

インストール

create react app

npx create-react-app react-calculator
cd react-calculator

@material-ui/core

// npm を使う場合
npm install @material-ui/core

// yarn を使う場合
yarn add @material-ui/core

バージョン情報

  • react: 16.13.1
  • @material-ui/core: 4.9.9

ファイル構成

スクリーンショット 2020-04-05 18.28.24.png

コンポーネントの実装

Display

Display.js
import React from 'react';
import { makeStyles } from '@material-ui/core/styles';
import Box from '@material-ui/core/Box';
import Typography from '@material-ui/core/Typography';

const useStyles = makeStyles((theme) => ({
    box: {
        width: "100%",
        margin: "10px auto",
    },
  }));

export default function Display(props) {
  const classes = useStyles();

  return (
      <div>
       <Box className={classes.box}>
           <Typography variant="h3">
                {props.children}
           </Typography>
       </Box>
      </div>
  );
}

NumberButton

NumberButton.js
import React from 'react';
import { makeStyles } from '@material-ui/core/styles';
import Button from '@material-ui/core/Button';

export default function NumberButton(props) {
  const useStyles = makeStyles((theme) => ({
    margin: {
      margin: theme.spacing(1),
      backgroundColor: "#f48fb1",
    },
  }));
  const classes = useStyles();

  return (
      <div>
        <Button className={classes.margin} onClick={props.onClick}>
            {props.children}
        </Button>
      </div>
  );
}

ポイント

  • propsでonClickイベントを受け取って、ButtonのonClickに接続しています

LongButton

LongButton.js
import React from 'react';
import { makeStyles } from '@material-ui/core/styles';
import Button from '@material-ui/core/Button';

export default function LongButton(props) {
  const useStyles = makeStyles((theme) => ({
    margin: {
      margin: theme.spacing(2),
      padding: theme.spacing(1),
      backgroundColor: props.backgroundColor,
    },
  }));
  const classes = useStyles();

  return (
      <div>
        <Button className={classes.margin} onClick={props.onClick}>
            {props.children}
        </Button>
      </div>
  );
}

ポイント

  • 数字ボタンと他のボタンでスタイルを変えようと思いましたが、同じような見た目になってしまいました。こちらは背景色をpropsに渡すことができます。

GuessHistory

GuessHistory.js
import React from 'react';
import { makeStyles } from '@material-ui/core/styles';
import Typography from '@material-ui/core/Typography';
import Box from '@material-ui/core/Box';

const useStyles = makeStyles((theme) => ({
    box: {
        textAlign: "left",
        margin:"20px 0",
        padding:"0 20px"
    },
  }));

export default function GuessHistory(props) {
  const classes = useStyles();

  const history = props.history;

  return (
      <div>
        <Typography>予想履歴</Typography>
        <Box className={classes.box}>
          {
            history.map((guess, index) => 
            <Box key={index} marginBottom="10px">
              <Typography variant="h5">
                {index+1}.  {guess[0]}{guess[1]}:  {guess[2]}
              </Typography>
            </Box>
            )
          }
        </Box>
      </div>
  );
}

ポイント

  • historyをpropsで受け渡しています。history.mapで単一のguessを受け取り、Box内で表示しています。
  • mapを使うときはコンポーネントにkeyをつけるのを忘れずに。

正解モーダル

SuccessModal.js
import React from 'react';
import { makeStyles } from '@material-ui/core/styles';
import Modal from '@material-ui/core/Modal';
import Backdrop from '@material-ui/core/Backdrop';
import Fade from '@material-ui/core/Fade';
import Box from '@material-ui/core/Box';
import Typography from '@material-ui/core/Typography';

const useStyles = makeStyles((theme) => ({
  modal: {
    display: 'flex',
    alignItems: 'center',
    justifyContent: 'center',
  },
  paper: {
    backgroundColor: theme.palette.background.paper,
    border: '2px solid #000',
    boxShadow: theme.shadows[5],
    padding: theme.spacing(2, 4, 3),
  },
}));

export default function TransitionsModal(props) {
  const classes = useStyles();
  const guessTimes = props.guessTimes;
  const answer = props.answer;

  const open = props.open

  return (
    <div>
      <Modal
        aria-labelledby="transition-modal-title"
        aria-describedby="transition-modal-description"
        className={classes.modal}
        open={open}
        closeAfterTransition
        BackdropComponent={Backdrop}
        BackdropProps={{
          timeout: 500,
        }}
      >
        <Fade in={open}>
          <div className={classes.paper} >
              <Box marginBottom="30px" id="transition-modal-title">
                <Typography  variant="h4">正解です!</Typography>
              </Box>
              <Box  id="transition-modal-description">
                <Typography variant="h5">正解の数字: {answer}</Typography>
                <Typography variant="h5">正解までの質問数: {guessTimes}</Typography>
              </Box>
          </div>
        </Fade>
      </Modal>
    </div>
  );
}

ポイント

  • 元となるコンポーネントはmaterial-uiの公式ドキュメント、モーダルから引っ張ってきました
  • propsでopenを受け取ることで、コンテナのopenstateがtrueになったときに子のopenも更新されます。

コンテナの実装

NumberGuess.js
import React, {useState, useEffect} from 'react';
//components
import NumberButton from '../components/NumberButton';
import LongButton from '../components/LongButton';
import Display from '../components/Display';
import GuessHistory from '../components/GuessHistory';
import SuccessModal from '../components/SuccessModal';
//material-ui
import Box from '@material-ui/core/Box';
import Grid from '@material-ui/core/Grid';
import Typography from '@material-ui/core/Typography';

export default function NumberGuess() {
    //states
    const [inputNumber, setInputNumber] = useState(0);
    const [questionContent, setQuestionContent] = useState("");
    const [history, setHistory] = useState([]);
    const [answer,setAnswer] = useState(0);
    const [open, setOpen] = useState(false);
    const [guessTimes, setGuessTimes] = useState(0);

    //ページが読み込まれたときに一度だけ実行させる
    useEffect(() => {
        setAnswer(Math.floor( Math.random() * (100) ) + 1);
    }, [])

    //数字ボタンをクリックしたとき
    const handleNumberClick = (num) => {
        let newInputNumber = inputNumber*10 + num;
        if(newInputNumber >= 1 && newInputNumber <= 100){
            setInputNumber(newInputNumber);
        }
    }

    //予想の内容ボタンをクリックしたとき
    const handlequiestionContentClick = (content) => {
        if(content === "biggerThan"){
            setQuestionContent("より上ですか?");
        } else if(content === "smallerThan"){
            setQuestionContent("より下ですか?");
        } else if(content === "exact"){
            setQuestionContent("ですか?");
        }
    }

    //クリアボタンをクリックしたとき
    const handleClearClick = () => {
        setInputNumber(0);
        setQuestionContent("");
    }

    //予想ボタンをクリックしたとき
    const handleSubmitClick = () => {
        //予想の内容が入力されていない場合はすぐに返す
        if(questionContent === "") return;
        //予想した回数を1増やす
        setGuessTimes(guessTimes + 1);

        if(questionContent === "より上ですか?"){
            if(inputNumber < answer){
                setHistory([
                    ...history,
                    [inputNumber, "より上ですか?", "はい"]
                ]);
            } else {
                setHistory([
                    ...history,
                    [inputNumber, "より上ですか?", "いいえ"]
                ]);
            }
        } else if(questionContent === "より下ですか?"){
            if(inputNumber > answer){
                setHistory([
                    ...history,
                    [inputNumber, "より下ですか?", "はい"]
                ]);
            } else {
                setHistory([
                    ...history,
                    [inputNumber, "より下ですか?", "いいえ"]
                ]);
            }
        } else if(questionContent === "ですか?"){
            if(inputNumber === answer){
                setHistory([
                    ...history,
                    [inputNumber, "ですか?", "はい"]
                ]);
                //正解の場合にモーダルを開く
                setOpen(true);
            } else {
                setHistory([
                    ...history,
                    [inputNumber, "ですか?", "いいえ"]
                ]);
            }
        }
        setInputNumber(0);
        setQuestionContent("");
    }
  return (
      <div>
          <Box marginBottom="20px">
            <Typography variant="h4">React Number Guess</Typography>
            <Typography variant="h6">1~100の数字を予想しよう</Typography>
          </Box>
          <Grid container spacing={1}>
              <Grid item sm={6} xs={12}>
                <Display>{inputNumber} {questionContent}</Display>
                <Box display="flex" flexDirection="column">
                    <Box display="flex" justifyContent="center">
                        <NumberButton onClick={() => handleNumberClick(0)}>0</NumberButton>
                        <NumberButton onClick={() => handleNumberClick(1)}>1</NumberButton>
                        <NumberButton onClick={() => handleNumberClick(2)}>2</NumberButton>
                        <NumberButton onClick={() => handleNumberClick(3)}>3</NumberButton>
                        <NumberButton onClick={() => handleNumberClick(4)}>4</NumberButton>
                    </Box>
                    <Box display="flex" justifyContent="center">
                        <NumberButton onClick={() => handleNumberClick(5)}>5</NumberButton>
                        <NumberButton onClick={() => handleNumberClick(6)}>6</NumberButton>
                        <NumberButton onClick={() => handleNumberClick(7)}>7</NumberButton>
                        <NumberButton onClick={() => handleNumberClick(8)}>8</NumberButton>
                        <NumberButton onClick={() => handleNumberClick(9)}>9</NumberButton>
                    </Box>
                    <Box display="flex" justifyContent="center">
                        <LongButton onClick={() => handlequiestionContentClick("biggerThan")} backgroundColor="#f48fb1">より上ですか?</LongButton>
                        <LongButton onClick={() => handlequiestionContentClick("smallerThan")} backgroundColor="#f48fb1">より下ですか?</LongButton>
                        <LongButton onClick={() => handlequiestionContentClick("exact")} backgroundColor="#f48fb1">ですか?</LongButton>
                    </Box>
                    <Box display="flex" justifyContent="center">
                        <LongButton onClick={handleClearClick}  backgroundColor="#bdbdbd">クリア</LongButton>
                        <LongButton onClick={handleSubmitClick}  backgroundColor="#b2ff59">予想する</LongButton>
                    </Box>
                </Box>
              </Grid>
              <Grid item sm={6} xs={12}>
                <GuessHistory history={history}/>
              </Grid>
          </Grid>
          <SuccessModal open={open} guessTimes={guessTimes} answer={answer}/>

      </div>
  );
}

ポイント

  • useEffectを使って、マウント時に一度だけanswerを設定するようにしました。単純にconst answer = ...としていると、ボタンを押すごとに更新されてしまいます。
  • handleSubmitClickのsetHistory内では元のhistoryを展開して、最後に新たな要素を追加しています。

App.js

App.js
import React from 'react';
import './App.css';

import NumberGuess from './containers/NumberGuess'

function App() {
  return (
    <div className="App">
      <div className="container">
        <NumberGuess />
      </div>
    </div>
  );
}

export default App;

App.css

App.css
.App {
  text-align: center;
}

.container{
  margin: 80px auto 80px auto;
  max-width: 900px;
  background-color: #80deea;
}

以上で完成です!

おまけ

こちらの記事を参考に、gh-pagesでサイトを公開しました。
https://shintaro-hirose.github.io/react-number-guess/

今後もReactで簡単なアプリを作っていきます!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

sakuraエディタマクロ(json整形、マークダウン形式変換、改行統一、デコード)

サクラのマクロ

サクラエディタのマクロについて

サクラエディタでは、下記のファイルをマクロとして実行することができる。
https://sakura-editor.github.io/help/HLP000204.html

拡張子 種類
mac キーマクロ
ppa PPAマクロ
js WSH:Jscriptマクロ
vbs WSH:VBScriptマクロ
その他 WSH:他にWSHが認識できるものであれば使える

設定方法・利用方法

https://qiita.com/t_ookubo/items/6ad844ab04fc0170b4eb

利用しているマクロ

(あくまで支援目的なので、細かい精査とかエラーハンドリングとかはしてないです。)

jsonファイル整形

image.png

jsonFormat.js
// 行数を取得
var cnt = GetLineCount(0);
// 改行を除いた行文字列を取得
var textAll = "";
for (var i = 1; i <= cnt; i++) {
    var str = GetLineStr(i).replace(/\r\n/,"").replace(/\n/,"");
    textAll += str;
}
var editJson = "";
var innerFlg = false;
var intentLv = 0;
var quoteType = [];
var blockType = [];
var preChr = "";
for (var i = 0; i < textAll.length; i++) {
    var prefix = "";
    var suffix = "";
    var c = textAll.substring(i, i + 1);
    if (!innerFlg) {
        if (c == " ") {
            continue;
        }
        if (c == "\"" || c == "'"){
            innerFlg = true;
            quoteType[quoteType.length] = c;
            // JSON または 配列の1つ目の要素の時はスペース1個を追加
            var chkText = editJson.replace(/( |\t|\r\n|\n)/g, "");
            if (chkText.length > 0) {
                var preChar = chkText.substring(chkText.length - 1, chkText.length);
                if (preChar == "[" || preChar == "{") {
                    c = " " + c;
                }
            }
        } else {
            if (c == "{" || c == "[") {
                suffix = "\r\n";
                intentLv = intentLv + 1;
                blockType[blockType.length] = c;
                for (var j = 0; j < intentLv; j++) {
                    suffix = suffix + "\t";
                }
            }
            if (c == "}" || c == "]") {
                prefix = "\r\n";
                suffix = "\r\n";
                intentLv = intentLv - 1;
                blockType.pop(1);
            }
            if (c == ",") {
                prefix = "\r\n";
            }
        }
    } else {
        if (quoteType.length > 0 && c == quoteType[quoteType.length - 1]){
            innerFlg = false;
        }
    }
    if (prefix != "") {
        for (var j = 0; j < intentLv; j++) {
            prefix = prefix + "\t";
        }
    }
    var line = "" + prefix + c + suffix;
    if (preChr == "\r\n" && line.substring(0, 1) == "\r\n") {
        line = line.substring(1);
    }
    editJson = editJson + "" + line;
    preChr = line.substring(line.length - 1, line.length);
}
SelectAll(0);
InsText(editJson + "\r\n");

CRLF改行統一

image.png

CRLF.mac
S_SelectAll(0); // すべて選択
S_CopyCRLF(0);  // CRLF改行でコピー
S_Paste(0); // 貼り付け

タブ区切りをMarkdown形式のテーブルに変換

image.png

toMarkdownTable.js
var selStRow;
var selEdRow;
var result = ''; 

// 行非選択状態の場合
if(IsTextSelected === 0 ) {
    selStRow = 0;
    selEdRow = GetLineCount(0);
    SelectAll(0);
} else {
    selStRow = GetSelectLineFrom; // 選択開始行取得
    selEdRow = GetSelectLineTo;   // 選択終了行取得
}

// 項目数を取得
var colCount = GetLineStr(selStRow).split(/\t/).length;

for (var i = selStRow; i <= selEdRow; i++) {
    // i行目を取得し、先頭・終了・タブをパイプラインで置き換える
    result +=  '| ' + GetLineStr(i).split(/\t/).join(' | ').replace( /\r?\n/g , ' |') + '\r\n';

    // 選択1行目の場合は、header行を追加する
    if (i === selStRow) {
        result += '|';
        for (var j = 1;  j <= colCount ; j++) {
            result += '--------|';
        }
        result += '\r\n';
    }
}
InsText(result);

「¥uXXXX」形式ユニコードをアンエスケープ

image.png

unicodeUnescape.js
// 行数を取得
var cnt = GetLineCount(0);
var result = ''; 

for (var i = 1; i <= cnt; i++) {
    // i行目を取得
    var str = GetLineStr(i);
    // \u.... の文字列の配列で取得、大文字小文字は問わない
    uniArray = str.match(/\\u.{4}/ig);
    if (uniArray) { 
        for (var j = 0, len = uniArray.length; j < len; j++) {
            str = str.replace(uniArray[j], String.fromCharCode(uniArray[j].replace('\\u', '0x')))
        }
    }
    result += str;
}
SelectAll(0);
InsText(result);

URLデコード

image.png

decodePercent.js
// 行数を取得
var cnt = GetLineCount(0);
var result = ''; 

for (var i = 1; i <= cnt; i++) {
    // i行目を取得
    var str = GetLineStr(i);
    str = decodeURI(str);

    result += str;
}
SelectAll(0);
InsText(result);
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ワイ「なに!?型のズレを吸収できるやと!?」

前回の記事、
4歳娘「パパ、20歳以上のユーザーを抽出して?」
の続きやで!

休日ワイ

ワイ「最近、コロナウィルスの影響でリモートワークがメインになってしもうたから」
ワイ「家でも快適にお仕事ができるように、42.5インチの特大モニタを買ったで!」
ワイ「やっぱデッカいモニタはええな〜」
ワイ「インデントも思いっ切り入れれるわ」

スクリーンショット 2020-04-04 13.42.28.png

よめ太郎「さすがに入れすぎやろ・・・」

ワイ「私のインデントは53ですいうてな」

よめ太郎「いやフリーザ様どこで勝負してんねん」
よめ太郎「しかもインデント奇数なんかい」

娘(4歳)「ねえ、パパ?」

ワイ「ん?」
ワイ「なんや、娘ちゃん」

娘「その大きいモニタを使って、早速やってほしい作業があるの」

ワイ「おお、ええでええで!」

娘ちゃんの依頼内容

娘「実は、こないだ山で拾ってきた家族風オブジェクトに、子供が増えてて」
娘「オブジェクトの内容が変わっちゃったの」

JavaScript
const family = {
    mother: {
        name: "よめ太郎",
        age: 35
    },
    father: {
        name: "やめ太郎",
        age: 37
    },
    children: [
        {
            name: "娘ちゃん",
            age: 4
        },
        {
            name: "ボブ",
            age: 39
        }
    ]
};

娘「daughterがchildrenになっちゃったの」

ワイ「ん?どういうことや?」

娘「前回は娘が1人だけだったからdaughterっていうオブジェクトだったけど」
娘「子供が1人増えて、childrenという配列になっちゃったの」
娘「だから、20歳以上のユーザーを上手く抽出できなくなっちゃったの」

ワイ「そ、そうなんか」
ワイ「でも、子供たちはどうせ20歳未満なんやから」
ワイ「motherとfatherだけ取り出せばええんちゃうの?」

娘「それが・・・弟のボブは養子で39歳なの」

ワイ「Oh...ボブ...」
ワイ「ワイの知らん間に子供が増えてるから、少しおかしいなって思ったけど」
ワイ「そういう事情やったんか」
ワイ「これは、ワイだけでは無理や」
ワイ「ハスケル子ちゃんを呼ぼう」

ハスケル子もTV会議で参戦

ワイ「もしもし、ハスケル子ちゃん?」

ハスケル子「はい」
ハスケル子「私を呼ぶってことは、ボブの件ですね」

ワイ「さすがハスケル子ちゃん、話が早いわ」

よめ太郎「(いや早すぎやろ)」

オブジェクトと配列が混ざっている件

ワイ「motherとfatherはオブジェクトなのに」
ワイ「childrenは配列やねん」
ワイ「オブジェクトと配列が混ざってんねん」

娘「そうなの」
娘「そこから、20歳以上のユーザーを抽出して」
娘「以下のようなotonaArrayっていう配列を作りたいの」

JavaScript
[
    {
        name: "よめ太郎",
        age: 35
    },
    {
        name: "やめ太郎",
        age: 37
    },
    {
        name: "ボブ",
        age: 39
    }
]

ハスケル子「うーん」
ハスケル子「オブジェクトと配列が混じってて扱いづらいなんて・・・」
ハスケル子「そもそも仕様とか設計の問題のような・・・」
ハスケル子「もっと、サーバサイドでデータを整形してから返したほうが・・・」

ワイ「いや、仕様とか設計とかサーバサイドとかじゃないねん」
ワイ「山で拾ったオブジェクトなんやから」
ワイ「もうボブは家族なんやから」

ハスケル子「やむを得ないですね」
ハスケル子「分かりました」
ハスケル子「大人だけを抽出したotonaArrayという配列を作ればいいんですね」

まずはObject.values()とmap()

ハスケル子「familyというオブジェクトを元に配列を作るので」
ハスケル子「まずはObject.values(family)ですね」

JavaScript
const familyArray =
    Object.values(family)

ハスケル子「↑こうですね」
ハスケル子「オブジェクトのkey名は使わずにvalueだけ取り出して、配列を作ってくれるメソッドです」
ハスケル子「つまり、familyArrayの中身は・・・」

JavaScript
// familyArrayの中身
[
    { name: "よめ太郎", age: 35 },
    { name: "やめ太郎", age: 37 },
    [
        { name: "娘ちゃん", age: 4 },
        { name: "ボブ", age: 39 }
    ]
]

ハスケル子「↑こんな風になります」

ワイ「おお、あとはこの中から20歳以上のユーザーを抽出すんねやな」
ワイ「でも、オブジェクトと配列が混じってるけど大丈夫なん?」

ハスケル子「余裕です」
ハスケル子「配列のflat()メソッドを使って」
ハスケル子「ネストした配列から1段階フラットな配列を作ります」

JavaScript
const flatFamilyArray =
    familyArray.flat()

ハスケル子「↑こうですね」
ハスケル子「すると・・・」

JavaScript
// flatFamilyArrayの中身
[
    { name: "よめ太郎", age: 35 },
    { name: "やめ太郎", age: 37 },
    { name: "娘ちゃん", age: 4 },
    { name: "ボブ", age: 39 }
]

ハスケル子「↑こんな感じのflatFamilyArrayが出来上がります」

ワイ「おお」
ワイ「子供の配列が1段階フラットになって、単なるフラットな配列になってフラットやな!」

よめ太郎「(語彙どうなってんねん)」

ワイ「あとはfilter()で20歳以上のユーザーを抽出するだけやな・・・!」

ハスケル子「はい」
ハスケル子「なので、いちいち変数に入れずにまとめて書くと・・・」

JavaScript
const otonaArray =
    Object.values(family)
        .flat()
        .filter(person => person.age >= 20);

ハスケル子「↑こうですね」
ハスケル子「すると・・・」

JavaScript
// otonaArrayの中身
[
    { name: "よめ太郎", age: 35 },
    { name: "やめ太郎", age: 37 },
    { name: "ボブ", age: 39 }
]

ハスケル子「↑こんな感じの、大人だけを集めたotonaArrayが出来あがります」

ワイ「まじか〜」
ワイ「flat()メソッド便利やな」

ハスケル子「はい」

ワイ「今までのワイやったらflat()メソッドなんて知らんかったから」
ワイ「Array.isArray()メソッドを使って」
ワイ「配列ならこう、配列じゃなければこう・・・みたいな感じで」
ワイ「if文とかで条件分岐しながら」
ワイ「配列の詰め直しをしとったところやわ」

ハスケル子「なるほど」
ハスケル子「つまり・・・」

JavaScript
// まず空配列を生成。
const flatFamilyArray = [];

familyArray.forEach(personOrArray => {
    // if文で配列かどうか判断しながら詰め直していく。
    if (Array.isArray(personOrArray)) {
        // 配列の場合はスプレッドで展開してpush。
        flatFamilyArray.push(...personOrArray);
    } else {
        // 配列じゃない場合はそのままpush。
        flatFamilyArray.push(personOrArray);
    }
});

ハスケル子「↑こういう感じで処理するってことですよね」

ワイ「せやせや」

ハスケル子「空配列を作って、そこにどんどん詰め直していく・・・」
ハスケル子「そういった、副作用と手続きの連続で書くのもまあ」
ハスケル子「分かりやすいといえば分かりやすいので良いんですけど」
ハスケル子「私はやっぱり」

JavaScript
const otonaArray =
    Object.values(family)
        .flat()
        .filter(person => person.age >= 20);

ハスケル子「↑この、メソッドの連続で書ける関数型な感じが好きですね」

ワイ「なるほどなぁ・・・」
ワイ「flat()はなんか、if文を内包してるようで」
ワイ「魔法みたいやな」

ハスケル子「ちょっとそんな感じですよね」
ハスケル子「たとえば・・・」

JavaScript
[3].flat() // -> [3]

[[3]].flat() // -> [3]

ハスケル子「↑こんな感じで」
ハスケル子「元の値が3であろうと[3]であろうと」
ハスケル子「一度[]で包んでflat()してやれば」
ハスケル子「同じ[3]になる・・・」
ハスケル子「こういう型ハックみたいなの楽しいですよね」

ワイ「なるほどな」
ワイ「[3]をflat()しても、ただの3にはならないんやな」
ワイ「せやから[3].flat()してやることで型を合わせられる・・・」

ハスケル子「そうなんです」

ワイ「なるほどなぁ・・・」
ワイ「オブジェクトから配列に変換しつつ、フラットにして、フィルタリングする・・・」
ワイ「って一つの流れの中でできるから」
ワイ「なんかカッコええね」

ハスケル子「はい!」

まとめ

  • flat()はネストした配列を1段階フラットにしてくれる。
    (引数を与えることで2段階、3段階フラットにすることも可能)

ワイ「↑こんな感じやね!」

ハスケル子「はい」
ハスケル子「今回みたく、オブジェクトと配列を同列に扱わないといけない、なんてのは」
ハスケル子「そもそも設計が微妙な感じもしますけど」
ハスケル子「例えば、スクレイピングで取得したデータとかの場合は」
ハスケル子「データの形式を自分で決められないので」
ハスケル子「こういうハックが役に立つこともあるかもしれませんね」
ハスケル子「もし思い出したら、使ってみてください」
ハスケル子「たまに便利ですよ!」

ワイ「なるほどな」

ハスケル子「もっと楽しい、flatMap()なんていうメソッドもありますよ」
ハスケル子「map()してflat()してくれるやつです」
ハスケル子「まるで関数が変身するみたいですよ」

ワイ「関数が変身する・・・?」
ワイ「どういうこと・・・?」

ハスケル子「それはまた次回お話しますね」

ワイ「おお、なんか楽しみやな・・・!」

〜つづく〜

参考文献

注意

もちろんIEでは使えへんで!!!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

jQueryで特定セルをクリックした時にそのセルの内容で検索する

djangoでjQueryをいじっていたら結構難しかったのでメモ。
よく調べたらcellのdataを受け、searchに突っ込むだけですね。Reference様様。

スクリプト

sample.html
<script>
  // jQueryの関数書くところ。
  $(document).ready(function () {
    // id="dataTable"と付くtableを取得する。
    var table = $('#dataTable').DataTable();

    // クリック時のイベントを定義
    $('#dataTable').on('click', 'td', function () {
      // どの列がクリックされたか、列番号を取得(0から開始)
      var row_number = table.column(this).index();
      // 例えば3列目がクリックされたときに限定
      if (row_number == 2 ) {
        // クリックされたセルの内容を得る
        var data = table.cell(this).data();
        // セルの内容が空の時には処理しない
        if( data != undefined ){
          // 検索バーに文字列を突っ込み(search(data))、それで検索(draw())する。
          table.search(data).draw();
        }
      }
    });
  });
</script>

refs

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

C3.js で折れ線グラフを描画する

概要

  • D3.js のラッパーライブラリ C3.js で折れ線グラフを描画する
  • 環境: C3.js 0.7.15 + D3.js 5.15.1

C3.js と D3.js のダウンロード

Release v0.7.15 · c3js/c3 · GitHub から v0.7.15.zip をダウンロードして展開する。c3.css と c3.min.js を取り出す。

Release v5.15.1 · d3/d3 · GitHub から d3.zip をダウンロードして展開する。d3.min.js を取り出す。

シンプルな折れ線グラフを描画するサンプル

c3-js-chart-1.png

<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
<title>C3.js で折れ線グラフを描画する</title>
<link href="c3.css" rel="stylesheet"><!-- c3.css を読み込む -->
</head>
<body>

<div id="my-container" style="width: 640px; height: 480px; border: solid 1px red">
  <div id="my-chart"></div>
</div>

<script src="d3.min.js"></script><!-- D3.js を読み込む -->
<script src="c3.min.js"></script><!-- C3.js を読み込む -->
<script>
let chart = c3.generate({
  bindto: '#my-chart',
  size: { width: 640, height: 480 }, // グラフ描画領域のサイズ
  data: {
    columns: [
      ['データ1', 30, 200, 100, 400, 150, 250],
      ['データ2', 50, 20, 10, 40, 15, 25]
    ],
    labels: true // それぞれの点に数値を表示
  },
  axis: {
    x: {
      label: {
        text: 'X軸',
        position: 'outer-middle'
      }
    },
    y: {
      label: {
        text: 'Y軸',
        position: 'outer-middle'
      }
    }
  }
});
</script>

</body>
</html>

時系列データの折れ線グラフを描画するサンプル

c3-js-chart-2.png

<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
<title>都道府県別のPCR検査陽性者累積数</title>
<link href="c3.css" rel="stylesheet"><!-- c3.css を読み込む -->
</head>
<body>

<div id="my-chart"></div>

<script src="d3.min.js"></script><!-- D3.js を読み込む -->
<script src="c3.min.js"></script><!-- C3.js を読み込む -->
<script>
let chart = c3.generate({
  bindto: '#my-chart',
  size: { width: 800, height: 600 },
  data: {
    x: 'mydate', // 時系列インデックスの項目名
    xFormat: '%m%d', // 時系列インデックスの日付フォーマット
    columns: [
      ['mydate', '0325', '0326', '0327', '0328', '0329', '0330', '0331', '0401', '0402', '0403', '0404'],
      ['東京都', 160, 201, 227, 227, 368, 436, 449, 527, 527, 690, 779],
      ['大阪府', 123, 129, 136, 154, 192, 209, 217, 245, 245, 312, 347],
      ['愛知県', 139, 144, 147, 150, 164, 164, 170, 176, 176, 186, 198],
      ['千葉県', 46, 51, 54, 60, 126, 160, 160, 164, 164, 181, 192],
      ['北海道', 162, 166, 167, 168, 172, 176, 177, 177, 177, 185, 190],
      ['神奈川県', 73, 77, 82, 88, 104, 107, 115, 120, 120, 153, 181],
      ['兵庫県', 87, 87, 87, 90, 125, 132, 136, 147, 147, 167, 172],
    ]
  },
  axis: {
    x: {
      type: 'timeseries',
      tick: {
          format: '%m月%d日'
      },
      label: {
        text: '日',
        position: 'outer-middle'
      }
    },
    y: {
      label: {
        text: 'PCR検査陽性者の累積',
        position: 'outer-middle'
      }
    }
  }
});
</script>

</body>
</html>

参考資料

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

BlocklyでWebページをデザイン

BlocklyでWebページをデザインして表示するサイトを作成しました。JavaScript命令のブロックで動きをプログラミングします。

「ねそプロ」ウェブサイトを作ろう

この教材は、3つのWebページで構成しています。
○ ウェブサイトを作ろう  ← サイト作成
○ ウェブサイトに画像登録 ← 画像の登録
○ ウェブサイトを見よう  ← 作成したサイト表示
 ↓ 教材はこちら
http://iwate-manabi-net.sakura.ne.jp/nesopuro/

ビジュアル → テキストプログラミングへ

2020年から小学校ではプログラミング教育が始まります。多くの学校で教材としてScratch等のビジュアルプログラミング使って指導を行うと思われます。小学校で体験したビジュアルプログラミングからテキストプログラミングへ橋渡しをするための教材として開発しました。HTMLタグ命令のブロックを組み合わせてWebページをデザインします。JavaScript命令のブロックでインタラクティブなページを作成できます。作成したプログラムをテキストとして表示します。ブロックで作成したビジュアルプログラムとテキストを見比べることでWebページの基本を理解させることができます。このテキストをメモ帳等に貼り付け、拡張子を.htmlとして保存するとWebページになります。ブラウザから起動するとボタン等も作動します。

双方向性のあるコンテンツ

2021年から中学校で実施される学習指導要領 技術・家庭 解説において、「コンテンツのプログラミングによる問題を解決する学習活動としては、例えば、学校紹介の Web ページにQ&A方式のクイズといった双方向性のあるコンテンツを追加」と示されています。そこで、次のサンプルを作成しました。
○ 2択クイズ
○ 英単語クイズ(テキスト入力)
○ APIを使って「天気」「地図」を表示
 命令ブロックの組み合わせで様々なデザイン、作動を刷るページの作成ができます。さらに、APIを使って他のWebで公開されているデータの活用も体験できます。

「命令ブロック」について

HTMLとJavaScript命令のブロック、外部Webサイトの画像やAPI等を読み込むブロックを用意しています。

HTML命令ブロック

見出し(特大) <h1>
見出し(大)  <h2>
改行      <br>
段落      <p>
文字色     <font color='red'>
文字の大きさ  <font size='4'>
センタリング  <div align='center'>
グループ    <div id='Group'>
ボタン     <button id='B'>
文字入力    <input type='text'>
画像表示    <img src='sample.jpg'>
 ※ 「ウェブサイトに画像登録」からアップロードした画像を表示します。
 ※ 登録した当日の画像のみ表示します(過去の画像は表示しません)。

JavaScript命令ブロック

ボタンが押された  addEventListener('click', function(){})
グループを非表示  style.display ='none'
グループを表示   style.display ='block'
グループ内容を削除 textContent = null
グループに追加   insertAdjacentHTML('beforeend','')
条件分岐      if(A=='こたえ'){}else{}
くり返し      for (var count=0; count<5; count++){}

外部Webサイトの画像やAPI等のブロック

天気予報(気象庁) <img src='http://www.jma.go.jp/jp/yoho/'>
天気図(気象庁)  <img src='http://www.jma.go.jp/jp/g3/'>
降水量(気象庁)  <img src='http://www.jma.go.jp/jp/amedas/'>
Googleマップ    <iframe src='https://www.google.com/maps/'>

これらの命令ブロックを組み合わせて、Webページを作成します。この実習を通して、生活の課題をネットワークを活用したプログラミングによって解決する方法を考えさせます。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

obnizとブザーで兄弟配管工ゲームのコイン音を出してみた

作ろうと思ったきっかけ

  • ProtoOutoStudio 3回目授業でobnizに触れる
  • ブザーが面白かった

 兄弟配管工ゲームでは、ブロックを叩くとコインが出てきますよね。その時の効果音を再現してみようと思いました。

”あの”効果音の音階を調べてみた

コインの音

が、楽譜が読めないぞ....。オクターブもよく分からん....。
よし、トライ&エラーか???(音痴です)

まずはベースとなる”ド”を鳴らす

”ド”の音が何Hzか調べました。今回はこちらのサイトを参考に周波数を設定していきます。

【鳴らしてみた様子】

音を鳴らすだけなら、obnizのJavascriptパーツライブラリから試すと爆速で確認できます。

 obniz IDを入力し、Test Runをクリックすると、指定した音が鳴ります。デフォルトは1000Hzになっているので、鳴らしたい音に合わせるといいかなと思います。
image.png

 今回はobnizのボタンを押すと、指定の音が鳴るようなプログラムをNode.jsで動かしています。ソースコードはこちら。

const Obniz = require('obniz');
var obniz = new Obniz("OBNIZ-ID");  // OBNIZ-IDに自分のIDを入れます
obniz.onconnect = async function () {

  // スピーカーを呼び出す
  var speaker = obniz.wired("Speaker", {signal:0, gnd:1});

  // ディスプレイ処理
  obniz.display.clear();  // 一旦クリアする
  obniz.display.print("Hello obniz!");  // Hello obniz!という文字を出す

  // スイッチの反応を常時監視
  obniz.switch.onchange = function(state) {
    if (state === "push") {
      // 押されたとき
      console.log("pushed");
      // ディスプレイ処理
      obniz.display.clear();  // 一旦クリアする
      obniz.display.print("pushed");  // pushed という文字を出す
      // 音を鳴らす
      speaker.play(523.25); // ドの音
    } else if (state === "none") {
      // none で押してないとき
      obniz.display.clear();  // 一旦クリアする
      // スピーカーで音を鳴らさない stop
      speaker.stop();
    }
  }
}

 上述のプログラムを使う際は、obniz.jsが必要となります。次のコードでインストールしておきましょう。

npm i obniz

あの効果音に使われている音を鳴らす

あの効果音は”シ”と”ミ”の2音で構成されているようです。といっても”ミ”は1つ上のオクターブになっているようです。

では、音階表を参考にしてみると....。

image.png

”シ”は987.76Hz、”ミ”は1318.51Hzでつくります。

 音を鳴らしてみるとこのようになりました。

 よさそうですね!あとは自動化してタイミング調整だな....

組み合わせる

 音を順番に出力するのは簡単ですが、タイミングがわからない!!!!!!!!
楽譜を見ると次のようになっています。

image.png

 こちらのサイトを参考にしてみると、200bpsのテンポであると読み解きました。200bpsとは、1分間に200拍の速度です。つまり、

  1分間(60秒) ÷ 200 = 0.3秒

となります。楽譜をみると、どうやら”シ”と”ミ”の間は何も記号がないので1拍だとして、”ミ”の後は2拍ありそうなので0.6秒として作りました。

結果....。


 
 思っていたより音が低そうなので、1オクターブあげてみましょう。

 こちらの方が近そうですね!でも何か足りない気がしてくる....
何度も同じ音を聞いていると分からなくなってしまうなぁ。でもこれで良しとしますか。

余談

 圧電スピーカーで効果音作るの楽しそう....沼に片足いれはじめてしまうぅ!

最終的なソースコード

const Obniz = require('obniz');

var obniz = new Obniz("Obniz_ID");  // Obniz_IDに自分のIDを入れます
obniz.onconnect = async function () {

  // スピーカーを呼び出す
  var speaker = obniz.wired("Speaker", {signal:0, gnd:1});

  // ディスプレイ処理
  obniz.display.clear();  // 一旦クリアする
  obniz.display.print("Hello obniz!");  // Hello obniz!という文字を出す

  // スイッチの反応を常時監視
  obniz.switch.onchange = async function(state) {
    if (state === "push") {
      // 押されたとき
      console.log("pushed");
      // ディスプレイ処理
      obniz.display.clear();  // 一旦クリアする
      obniz.display.print("pushed");  // pushed という文字を出す
      // 音を鳴らす
      speaker.play(1975.53);     // シ
      await obniz.wait(300);   // Wait
      speaker.play(2637.02);    // ミ
      await obniz.wait(600);   // Wait
      speaker.stop();
    } else if (state === "none") {
      // none で押してないとき
      obniz.display.clear();  // 一旦クリアする
      // スピーカーで音を鳴らさない stop
      speaker.stop();
    } else if (state === "left") {
      speaker.play(1318.51); // ミ
    }
  }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

JavaScriptの浮動小数点数を符号なし64ビット整数で表現したい

はじめに

JavaScriptにおける数値は、倍精度浮動小数点数しかないわけですが、倍精度浮動小数点数を構成するビット列を符号なし64ビット整数で表現した値が欲しいという場面がありました。このやり方について調べたので、記事にしてみました。

変換方法

function DoubleToBigInt(f)
{
  var buf = new ArrayBuffer(8);
  (new Float64Array(buf))[0] = f;

  lower = BigInt((new Uint32Array(buf))[0]);
  upper = BigInt((new Uint32Array(buf))[1]);

  return (upper << BigInt(32)) | lower;
}

上記のコードでは、ArrayBufferを経由して、倍精度浮動小数点数から2つの符号なし32ビット整数に変換しています。upperは倍精度浮動小数点数の上位32ビット、lowerは倍精度浮動小数点数の下位32ビットです。残念ながら、Uint64Arrayなんてものはありませんから、こうするしかないですね…

2つの符号なし32ビット整数に変換した後は、これらの値をBitIntに変換して符号なし64ビット整数の値を表現しています。

この関数の戻り値を確認してみましょう。

var f = 0.1234567;
console.log(DoubleToBigInt(f)); // 4593560413433027186n

4593560413433027186という値になりましたが、これが正しいのかC言語のコードで検証してみます。

#include <stdio.h>

union {
  double d;
  unsigned long l;
} val;

int main(void) {
  val.d = 0.1234567;
  printf("%ld\n", val.l);
  return 0;
}

上記のコードをコンパイルして、実行してみます。

$ gcc test.c
$ ./a.out
4593560413433027186

JavaScriptコード側の出力と一致しました。

おわりに

これをやろうと思ったのは、ももテクの記事を参考にJavaScriptのMath.random関数(XorShift128+)の内部状態を復元しようとしたためです。浮動小数点数表現で出力されるよりは、整数値で出力された方が解析しやすいかなと思いました。

なにはともあれ、上記のコードで浮動小数点数 -> 符号なし64ビット整数への変換ができたので、一件落着です。

追記 (2020/04/05 15:38)

@uhyo さんからBigUint64Arrayの存在を教えていただきました。ありがとうございます!BigUint64Arrayを使えば、DoubleToBigInt関数を以下のようにもっと簡潔に書けます。

function DoubleToBigInt(f)
{
  var buf = new ArrayBuffer(8);
  (new Float64Array(buf))[0] = f;

  return (new BigUint64Array(buf))[0];
}

参考サイト

https://stackoverflow.com/questions/2003493/javascript-float-from-to-bits
https://qiita.com/uhyo/items/f9abb94bcc0374d7ed23

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

(小ネタ)Node.jsのWebアプリでclusterを使いながら定期的に子プロセスを再起動させる

Node.jsでサーバサイドWebアプリを開発中、なぜかメモリリークがあるライブラリに遭遇してしまったので、ワークアラウンドとして、定期的にプロセスを再起動させて、メモリリークの問題を緩和したいと思いました。

サーバサイド

http://0.0.0.0:10080 をリスンするプロセスが4つ立ち上がり、5秒未満で子プロセスを停止させ、その後に再起動します。(実用上は、もっと長い時間でプロセスを殺すべきです。)

src/index.js
const cluster = require("cluster");
const http = require("http");

const sleep = time => new Promise(done => setTimeout(done, time));

const clusterCount = 4;
const portNumber = 10080;

if (cluster.isMaster) {
  const spawnProcess = () => {
    // プロセスを終了させるまでの時間: 0 〜 5000 msec
    const ttl = ~~(5000 * Math.random());
    const child = cluster.fork();
    let timeout;

    child.on("listening", () => {
      // 指定時間で終了(Graceful kill)させる
      console.log(`誕生! 死まで ${ttl} msec.`);
      timeout = setTimeout(() => {
        console.log(`死: ${child.id}`);
        child.kill();
      }, ttl);
    });
    child.on("disconnect", () => {
      // 別の理由で死んだ場合はkillをキャンセル
      if (timeout) {
        clearTimeout(timeout);
      }
    });
    child.on("exit", () => {
      // 子プロセスが終了したら代わりのものを1つ起動する
      spawnProcess();
    });
  };

  // 子プロセスを複数起動する
  for (let i = 0; i < clusterCount; i++) {
    spawnProcess();
  }
}
if (cluster.isWorker) {
  // Express や Koa など好きに使いましょう
  http
    .createServer(async (req, res) => {
      // リクエスト終了までやや時間がかかる設定
      await sleep(1000);
      res.writeHead(200);
      res.end("Request done\n");
    })
    .listen(portNumber);
}

起動すると、下記のようなログを吐きながら、プロセスの終了と生成を延々と繰り返します。

誕生! 死まで 2712 msec.
誕生! 死まで 3984 msec.
誕生! 死まで 4297 msec.
誕生! 死まで 1547 msec.
死: 4
誕生! 死まで 4276 msec.
死: 2
:
:

テスト

ab コマンドできちんとリクエストが中断されずにいるか、テストしてみます、

$ ab -c 20 -n 500 http://localhost:10080/
This is ApacheBench, Version 2.3 <$Revision: 1826891 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking localhost (be patient)
Completed 100 requests
Completed 200 requests
Completed 300 requests
Completed 400 requests
Completed 500 requests
Finished 500 requests


Server Software:        
Server Hostname:        localhost
Server Port:            10080

Document Path:          /
Document Length:        13 bytes

Concurrency Level:      20
Time taken for tests:   26.226 seconds
Complete requests:      500
Failed requests:        0
Total transferred:      44000 bytes
HTML transferred:       6500 bytes
Requests per second:    19.07 [#/sec] (mean)
Time per request:       1049.029 [ms] (mean)
Time per request:       52.451 [ms] (mean, across all concurrent requests)
Transfer rate:          1.64 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    1   1.0      0       7
Processing:  1000 1008   5.4   1007    1025
Waiting:     1000 1007   4.7   1006    1023
Total:       1000 1008   5.5   1007    1025

Percentage of the requests served within a certain time (ms)
  50%   1007
  66%   1010
  75%   1012
  80%   1013
  90%   1016
  95%   1019
  98%   1020
  99%   1022
 100%   1025 (longest request)

特に何も問題なくリクエスト処理は完了しているようです。

Complete requests:      500
Failed requests:        0

まとめ

  • cluster でマルチプロセス化できるし、定期的にプロセスを再起動して、健全性を保つことができるはず
    • こういうゴミ掃除はマルチスレッドモデルだとできなさそう
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

puppeteer 画像保存 (fs, path)

puppeteerで画像を保存する

結論

app.js
const puppeteer = require('puppeteer');
const fs = require('fs');
const path = require('path');

(async () => {
  const browser = await puppeteer.launch({headless: false});
  const page = await browser.newPage();
  await page.goto('目的の画像サイトのURL');
  await page.waitFor(5000);

  let images = await page.evaluate(() => 
    Array.from(document.querySelectorAll('.pict')).map(a => a.src)
  )
//↓これ絶対冗長だからなおしたい
  let imagesPop = await page.evaluate(() => 
    Array.from(document.querySelectorAll('.pict')).map(a => a.src.split('/').pop())
  )

  for(let i = 0; i < images.length; i ++){
    let localfilefullpath = path.join(__dirname, imagesPop[i]);

    const viewSource = await page.goto(images[i]);
    fs.writeFile(localfilefullpath, await viewSource.buffer(), (error) => {
      if (error) {
        console.log(error);
        return;
      }
      console.log('保存できたよ!');
    });
    await page.waitFor(1000);
  }
  await browser.close();
})();

概略

evaluateで要素を2種類取得

①images = https~から始まるやつ
②imagesPop = 一番最後の/から~.jpgとかで終わるやつ

画像の保存

①ローカルファイルのフルパスとimagesPopを連結する
②ローカルファイルに直接保存させちゃう(本当はfs.mkdirSyncでフォルダ作った方がいいと思う)

誤解があったらご指摘ください

解説

インストールするnpmは
puppeteer fs pathの3つ

ブラウザで確認できるように
const browser = await puppeteer.launch({headless: false});に

目的のページにたどり着いてからローディングのぐるぐるが一旦止まるくらい待つ
await page.waitFor(5000);
セレクタで待つ方法、Xpathで待つ方法、ページ遷移で待つ方法、など様々あるが、簡単なのでこの方法。バンされちゃったらいやなので。安全に。

複数の要素を取得
let images = await page.evaluate(() =>
Array.from(document.querySelectorAll('.pict')).map(a => a.src)
)

このevaluate, Array.from, querrySelectorAll, mapの流れはめっちゃ使う
ほかにも$xで指定する方法もあるがいつも混乱してしまうから個人的にはあまり好まない

ローカルファイルのフルパスと画像URLの最後の部分を連結
let localfilefullpath = path.join(__dirname, imagesPop[i]);

バイナリデータを扱うときはbuffer()を使うんかなおそらく
fs.writeFile(localfilefullpath, await viewSource.buffer())
第一引数で保存場所、第二で保存したい画像URLに.buffer()を追加

参考文献

https://qiita.com/Ancient_Scapes/items/5f63bcfc2b5108313f77
https://qiita.com/KawamotoShuji/items/878ae659a5c6e540343e
http://info-i.net/fs-writefile
https://wa3.i-3-i.info/word1146.html

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

FIDOデバイスエミュレータを作成してみた(これが最後)

何回か続けてきたFIDOデバイスエミュレータはこれが最後です。

・WebAuthnを使ったFIDOサーバを立ててみた
 本物のFIDOデバイスをお持ちであれば、この投稿だけでも参考になります。
・FIDOデバイスエミュレータを作成してみた。。。が
 仮想的なFIDOデバイスをサーバに配置しようという野望です。
・FIDOデバイスエミュレータを作成してみた(だけどもうちょっと。)
 Arduinoを使って、ブラウザとFIDOデバイスエミュレータの橋渡しをしました。

上記の投稿ですでに編集済みですが、結局のところ、x509証明書が、FIDO Allianceに登録されていないためと、最終的に判断しました。

試行錯誤しながらも、ある程度の完成度まで仕上げられました。
FIDOのプロトコルだけでなく、PIN認証付きBLEペリフェラル、x509証明書など、多岐にわたるノウハウを獲得できました。今回の主要な目的の一つでしたので良かったと思っています。

最後の最後で行った修正を記録しておきます。

x509v3証明書にする

x509証明書の作成には以下を使っていました。

・Junkurihara/js-x509-utils
 https://github.com/junkurihara/js-x509-utils

しかしながら、v3には対応していないため、急遽以下のnpmモジュールに変更しました。

・jsrsasign
 https://kjur.github.io/jsrsasign/

これでv3にはなりました。fido2-libのコメントアウトも解除できました。

ですが、Chromeはエラーを吐いていましたので、さらに、る本物のFIDOデバイスから抜き出したx509証明書との違いを探してみました。以下の2つで差分がありました。

・サブジェクトキー識別子
・FIDO U2F certificate transports extension

サブジェクトキー識別子が何なのかがわかりませんでした。一般にHashらしく、それっぽく入れてみたのですがだめでした。コード上ではコメントアウトしています。

FIDO U2F certificate transports extension は、以下の仕様書に記載がありました。
 FIDO U2F Authenticator Transports Extension

結局x509証明書作成の部分は、以下のようなコードになりました。
(有効期限が固定値だったりと、力尽きてる感がありますが。。。)

index.js
  // X.509証明書の作成
  var tbsc = new rs.KJUR.asn1.x509.TBSCertificate();

  tbsc.setSerialNumberByParam({'int': 1234});
  tbsc.setSignatureAlgByParam({'name': 'SHA256withECDSA'});
  tbsc.setIssuerByParam({'str': "/CN=FT FIDO 0200"});  
  tbsc.setNotBeforeByParam({'str': "190511235959Z"});
  tbsc.setNotAfterByParam({'str': "340511235959Z"});
  tbsc.setSubjectByParam({'str': "/CN=FT FIDO P2000000000000"});  
  tbsc.setSubjectPublicKey(kp.pubKeyObj);

/*
  //サブジェクトキー識別子
  var extSKI = new rs.KJUR.asn1.x509.Extension();
  extSKI.oid = '2.5.29.14';
  const ski = rs.KJUR.crypto.Util.hashHex(kp_cert.pubKeyObj.pubKeyHex, 'sha1');
  const derSKI = new rs.KJUR.asn1.DEROctetString({ hex: ski });
  extSKI.getExtnValueHex = () => {return derSKI.getEncodedHex() };
  tbsc.appendExtension(extSKI);
*/

  // FIDO U2F certificate transports extension
  var extSKI2 = new rs.KJUR.asn1.x509.Extension();
  extSKI2.oid = '1.3.6.1.4.1.45724.2.1.1';
  extSKI2.getExtnValueHex = () => { return "03020640" };
  tbsc.appendExtension(extSKI2);

  var cert = new rs.KJUR.asn1.x509.Certificate({'tbscertobj': tbsc, 'prvkeyobj': kp_cert.prvKeyObj });
  cert.sign();
  var attestationCert = Buffer.from(cert.hTLV, 'hex');

FIDO仮想デバイスの通信バッファサイズを変更

X509証明書をv3にしたからなのか、通信サイズが少し大きくなって、バッファオーバーフローが発生しました。
Arduinoのソースコードで、通信バッファサイズを2倍にしました。

結果

だめでした。状況変わらずです。
以下のブラウザ画面です。何回見させられたことか。。。。

image.png

最後に

ここまでご参照いただきありがとうございました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Vue.js 画像URLから幅と高さを取得しようとしてはまった

こちらの記事どうり簡単に取得できるものだと思っていたけど、

var img = new Image();
img.src = 'http://image.src.com/image.jpg';

var width  = img.width;  // 幅
var height = img.height; // 高さ

vue.jsでやってみた時にはimg.widthやimg.heightが0だった。

調べているとstackoverflowなどでreturnが0だ!なんで!みたいな記事を見つけたがそもそもjavascriptの知識が乏しいのとjQueryを使ってどうにかしようとしている解決法をvue.jsでどうかくかわからなかった

知人に聞きこちらの記事を発見
https://www.raymondcamden.com/2019/06/13/reading-image-sizes-and-dimensions-with-vuejs

img.onloadという関数を使ってvar img = new Image();でできたimgを読み込まなきゃ画像の幅、高さを取得できないそうだ。

かつ、スコープの関係でonload内で使った変数を他で使うことはできないため、vue.jsのdataにプロパティを設定して、そこにセットする形になるそう。

vue.js
  export default {
    data () {
      return {
        new_image: []
      }
    },
    methods: {
      getImageSize (obj) {
        var img = new Image();
        img.onload = () => {
          this.new_image.push({width: img.width, height: img.height});
        }
        img.src = obj;
      }
    }
  }

// objには画像urlが入ります。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[rails,payjp]カード登録の際に登録画面をリロードしないとトークンを作成できない時の解決例

1.原因

今回の筆者の問題はturbolinksが効いていたからでした。

2.なぜ起こった?

理由は、turbolinksの機能が影響範囲ページの遷移を非同期で行う(影響範囲の全てのviewページが合わさって1ページとして扱う)ためでした。
他画面を経由し複数で情報を持った状態でクレジットカードの登録をしようとすると、悪意のある情報を持っている危険性があり安全性を保てないと見なされてしまいpayjpからの返答を受けることができなくなっていました。

3.解決方法

クレジットカード登録画面のみturbolinks(非同期通信)を切ってページ遷移するように記述することで筆者は解決しました。

具体的にはapp/views/layouts/apprication.html.haml(全体のbodyの記述部分)とapp/views/cards/index(ページ遷移元)を下記のように修正しました

app/views/layouts/apprication.html.haml
〜前略(header部分の記述は省略します)〜

%body
    - if content_for?(:body_attributes)
      = yield(:body_attributes)
    = render 'layouts/notification'
    = yield
app/views/cards/index
〜前略〜
= link_to "/cards/new", class: 'method-of-payment__body__button__icon',"data-turbolinks": false do
#app/views/cards/newに遷移する記述の部分に "data-turbolinks": false をつけました
〜後略〜

4.参考記事

https://gist.github.com/saboyutaka/8727377
https://qiita.com/Cheekyfunkymonkey/items/216bf7426493e6213927
https://qiita.com/keitah/items/05f02efe4e11ab2473e8

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

JavaScript【よく使う表現集】

概要

JavaScriptでよく個人的に使用頻度の高い表現をまとめた備忘録。
随時更新予定。

1から99の数値間で整数の乱数を生成する

Math.floor(Math.random() * (100 - 1)) + 1

範囲を変更したい場合は最大値(上記の場合だと100)と最小値(上記の場合だと1)の値を変更する。

数値を任意の倍数に整える

Math.ceil(num / trim) * trim) //切り上げ
Math.floor(num / trim) * trim) //切り下げ

numには元の数値、trimには整えたい倍数の数値をそれぞれに入力。
下記は配列で渡された数値を5の倍数(切り上げ)で整えた例。

function multipleTrim(num) {
  return num.map(a => Math.ceil(a / 5) * 5)
}
console.log(multipleTrim([73, 67, 38, 33])); //=> [ 75, 70, 40, 35 ]
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Chromium系ブラウザのdevコンソールで画像やリンクのURL一覧を一気にコピーする

クイックスタート

F12->consoleに以下をコピペして実行。
selectorとattrの中身は好きなものに書き換える

var selector = "a";
var attr = "href";
copy(Array.from(document.querySelectorAll(selector)).map(x => x.getAttribute(attr)).join("\n"));

クリップボードの中にURL(などのattrで指定した要素)の一覧がコピーされる

やっていること

  • Array.from()の中にquerySelectorAllを入れることでエレメントを全て配列化
  • 配列をmapしてその中でgetAttribute(attr)することで"href"など指定したプロパティ一覧の配列にする
  • join("\n")で改行コードを挟んで結合した文字列にする
  • copy()はコンソール限定のビルトイン関数で、その引数がクリップボードに入る
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

常に動くLINEBOTにお引っ越し(now編)(未解決版)

前回つまって諦めたnowに再挑戦した記録。

ngrokで作った時の記事:
WikipediaのAPIを使ってLINEbotに調べてもらう

はじめに

前回同様、課題感とテーマとしては「ngrokだと使いたいときに使えない!だから常時使えるようにしたい!」です。

目次としては、

  • now でだいぶハマる
  • さらに now にハマる
  • 対応したこと

です。

nowでだいぶハマる

nowを正しく動かすところでまずハマりました。
いくつかハマった気がして、だいたいはいじったりnow自体を消してやり直したりしたらできた印象だが、うっかりでハマったのがこの記事参照。
https://qiita.com/shima-07/items/64c051c9982ac0899b21

さらにnowにハマる

無事、ngrokを卒業して、nowを使ってLINE botができました。
が、、
なんか挙動がおかしい。ソースコードはngrokの時と何も変えてないのに。
botなのになんだか頭が悪い人間っぽさが出てきた・・・。

具体的には、

1. 初めて検索するキーワードにはwikipediaからの返り値を返してくれない。 (「初めてなので・・・」とかウブさはbotに求めてない)

image.png
(2回同じこと聞くと返してくれる・・・)

2. 前回の回答を返してくる。(質問をちゃんと聞いて)

今回は「カラス?」と聞いているのに、「くま」について説明し始めた、、

3. 自動応答したいreplayMessageとWikipedia APIのpushMessageの順番がバラバラ(落ち着いてほしい…)

本当は「カラスの説明:」が上にきて、その下にwikipediaからの返答をのせたいのだけど、ずれる時がある。

image.png

nowにしたことで、何やら処理の順番とかが変わるようだ。。

対応したこと

https://qiita.com/n0bisuke/items/fb19ed3cd0138135ae69
この記事を見て、

asyncとawaitを使い、replyとpushの場所も変えてみた

変更前↓

before.js
.
省略
.
.
.

function handleEvent(event) {
  if (event.type !== 'message' || event.message.type !== 'text') {
    return Promise.resolve(null);
  }

  let mes = ''
// console.log(event.message.text);
    if(event.message.text.indexOf('?') > -1){
        // ?を含んでいる場合にはwikiで検索したものを出して、含んでない場合はurlを返す
    var str = event.message.text;
    var result = str.split( '?' ).join( '' ); //?を取り除く処理
    mes = result + 'の説明:'; //wikiのbodyの前の一言
    getBody(event.source.userId,result); //wiki APIで取得できたらプッシュメッセージ

  }else{
    var result = event.message.text;
    mes = result + 'のURL:'; //wikiのurlの前の一言
    getUrl(event.source.userId,result); //wiki APIで取得できたらプッシュメッセージ
  }


  return client.replyMessage(event.replyToken, {
    type: 'text',
    // text: event.message.text //実際に返信の言葉を入れる箇所
    text : mes 
});
}

const getBody = async (userId,word) => {
    const res = await axios.get('http://wikipedia.simpleapi.net/api?keyword='+ encodeURIComponent(word) + '&output=json');
    const item = res.data;
    // console.log(item); 

    await client.pushMessage(userId, {
        type: 'text',
        text: item[0].body,
    });
}

const getUrl = async (userId,word) => {
    const res = await axios.get('http://wikipedia.simpleapi.net/api?keyword='+ encodeURIComponent(word) + '&output=json');
    const item = res.data;
    // console.log(item); 
    await client.pushMessage(userId, {
        type: 'text',
        text: item[0].url,
    });
}


(process.env.NOW_REGION) ? module.exports = app : app.listen(PORT);
console.log(`Server running at ${PORT}`);

変更後↓(replyMessageをasyncとawaitを使って書き換え、場所も変えてみた。)
※?がある場合の分岐の抜粋。

after1.js
async function handleEvent(event) {
  if (event.type !== 'message' || event.message.type !== 'text') {
    return Promise.resolve(null);
  }

  let mes = ''
// console.log(event.message.text);
    if(event.message.text.indexOf('?') > -1){
        // ?を含んでいる場合にはwikiで検索したものを出して、含んでない場合はurlを返す
    var str = event.message.text;
    var result = str.split( '?' ).join( '' ); //?を取り除く処理
    mes = result + 'の説明:'; //wikiのbodyの前の一言

    await client.replyMessage(event.replyToken, {
      type: 'text',
      // text: event.message.text //実際に返信の言葉を入れる箇所
      text : mes 
    });

    getBody(event.source.userId,result,mes); //wiki APIで取得できたらプッシュメッセージ

  }

こういう対応ではダメだった?
これにしても直らず。
上記3つのおとぼけbotさんのままでした。。

次に、

いっそwikiの内容をとってくる関数の中にreplyMessage入れたらいいんじゃない?

と思い、やってみた。(完全に素人の悪あがき感。。)

getBody.js
const getBody = async (userId,word,message) => {
  await client.replyMessage(event.replyToken, {
    type: 'text',
    // text: event.message.text //実際に返信の言葉を入れる箇所
    text : message 
  });

    const res = await axios.get('http://wikipedia.simpleapi.net/api?keyword='+ encodeURIComponent(word) + '&output=json');
    const item = res.data;
    console.log(item); 

    await client.pushMessage(userId, {
        type: 'text',
        text: item[0].body,
    });
}

これでも、相変わらずおとぼけbotは治らず。。

今回も失敗…

前回の諦めポイントのnow自体からは進んだのですが、結果botがおとぼけさんになってしまった。
(もっと人間的な振る舞いをしたら可愛げがあって多少のミスも許せるが、、まだ十分な人間味もない。)

時間的にもきついので、しばらくはこれ以上深掘らず、いったんここまで。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【CSS】最低限の見た目にするためにコンテンツを画面の中心にしたいだけです。

1. 記事の目的

わたしはCSSが書けません。

しかし、htmlやjavascriptで作成した成果物やコンテンツを、最低限の見た目にしたいという思いはあります。
「最低限の見た目」=「画面サイズに依存せずに画面の中心に配置する」と考えた私は、「なら、成果物を画面の中心にもってくるCSSをいつでも使えるように用意しておこう。」と決意しQiitaに投稿しました。それだけです。

2. コード(コピペ用)

bodyタグに対して、以下のようにCSSを設定することで、コンテンツを中心に配置できます。

body {
    margin:0;
    display:flex;
    min-height:100vh;
    justify-content:center;
    align-items:center;
    /* flex-direction: column; */
}

一般公開するので、一通り解説をします。
詳細は参考リンクに譲ります。

3. CSS の解説

3-1. 解説用html

解説用のhtmlサンプルは以下の通りです。今回はbodyタグの下に見た目用のCSSを追記しています。あくまで解説用のため、本来は必要ないです。

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">

    <style>
        body {
            /* ここを個々に解説をします */
            /* margin: 0; */
            /* display: flex; */
            /* justify-content: center; */
            /* min-height: 100vh; */
            /* align-items: center; */
            /* flex-direction: column; */
        }     
    </style>
</head>

<body>
    <div class="item item1">アイテム1</div>
    <div class="item item2">アイテム2</div>
    <div class="item item3">アイテム3</div>    
</body>

<style>
    /* 枠線や背景色などの見た目用CSS */
    body {
        box-sizing:border-box;
        border: black 10px solid;
        padding: 10px;
        background-color: gainsboro;
    }
    .item{
        border: black 2px solid;
        padding: 3px;
        margin: 3px;
        background-color: #fff;
    }
</style>    
</html>

こんな感じの見た目になります。(通常なら見た目用CSSがないため、もっと味気ない感じになりますよね。)
1.PNG
図1. 何もない状態

3-2. margin:0;

まず、marginと呼ばれる枠(border)の外の設定です。(図1の黒太枠の外の余白です。)

デフォルトではmargin:8pxとなっていました。margin:0に設定することで枠の外の余白をなくします。
※margin:0のときはpxのような単位がなくても構いません。もちろん、単位があっても動作します。

body {
    margin: 0px;
    /* display: flex; */
    /* justify-content: center; */
    /* min-height: 100vh; */
    /* align-items: center; */
    /* flex-direction: column; */
}       

1.PNG
図2. margin:0 を設定

黒太枠(bodyのborder)の外側の余白(margin)がなくなりました。

3-3. display: flex;

flexコンテナと呼ばれるものを設定します。
以下のjustify-contentやalign-itemsなどを使用するために必要です。

body {
    margin: 0;
    display: flex;
    /* justify-content: center; */
    /* min-height: 100vh; */
    /* align-items: center; */
    /* flex-direction: column; */
}         

1.PNG
図3. display: flexの適用

display:flexを適応するとこんな感じです。黒太枠内がflexコンテナです。
flexコンテナを適用したタグの子要素(アイテムたちのことです!)が左に寄せられています。
ブロックレベル要素(タグごとに改行される要素)のアイテムたちが横並びになっています。(図2だと縦に並んでました)

3-4. justify-content:center;

flexコンテナ内の要素を、水平方向のどの位置に配置するかを指定します。
デフォルト(指定していない場合)はjustify-content:flex-start。左寄せになります。(図3 参照)
今回はjustify-content:centerを指定して、中央にします。

body {
    margin: 0px;
    display: flex;
    justify-content: center;
    /* min-height: 100vh; */
    /* align-items: center; */
    /* flex-direction: column; */
}     

1.PNG
図4. justify-content: center の適用

左に寄っていたアイテムたちが、真ん中に集合しました!

3.5. min-height:100vh;

flexコンテナの領域の最低の高さを100 vhに設定します。

vhはviewport heightと呼ばれる、viewport(表示領域)の高さに対する割合の単位です。
100vhはviewportに対して100%という意味です。
つまりmin-height:100vh=「bodyの高さを少なくとも、表示領域と同じにする」になります。

body {
    margin: 0px;
    display: flex;
    justify-content: center;
    min-height: 100vh;
    /* align-items: center; */
    /* flex-direction: column; */
}     

1.PNG
図5. min-height: 100vh の適用

黒太枠線(bodyのborder)が画面いっぱいに広がりました。
それにともない、アイテムたちが縦にみょーんとなっています。

3-6. align-items:center;

図5でアイテムたちが縦にみょーんとなってしまっているのは、align-itemsのためです。
align-itemsはflexコンテナ内の要素を垂直方向のどの位置に配置するかを指定します。
デフォルト(指定していない場合)はalign-items:stretch が指定されている状態です。親要素の高さになります。(図5参照)
今回は、中央揃えにしたいのでalign-items:centerを指定します。

body {
    margin: 0px;
    display: flex;
    justify-content: center;
    min-height: 100vh;
    align-items: center;
    /* flex-direction: column; */
}     

1.PNG
図6. align-items: center の適用

3-7. (追記)flex-direction: column;

3-3. display: flex;でアイテムが横配置になりました。
これはflex-direction: row;がデフォルトで反映されているからです。

flex-directionはflexコンテナ内のアイテムの向きを指定するプロパティです。
flex-direction: column;とすることで、縦配置のまま画面中心にコンテンツを移動することができます。

4. さいごに

コンテンツを画面の中心に持ってくることができました!

これで最低限の見た目は保証がされるはずです!(私は信じています!)
あとはいいコンテンツを作るだけですね!…

参考リンク

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

nowでアプリケーションエラーが出たとき対応したこと

nowでハマったメモ

状況

@n0bisuke のこの記事を参考にやっていたつもりがこんなエラーと遭遇。

image.png

An error occurred with this application.
This is an error with the application itself, not the platform.

「nowは問題ないよ。お前のアプリケーション(ソースコード)が問題なんだよ。」とのこと。

対応

このnow.json内で指定しているjsファイルの一部を直したら直った。
(記事の例でいくと、server.jsってファイルの文末)

直した場所

自分にとってわかりやすいと思って書き換えてた最後のserver running のあたり。

エラー出てたとき

エラー出てたとき.js
if(process.env.NOW_REGION){
  module.export = app;
}else{
  app.listen(PORT);
  console.log(`Server running at ${PORT}`);
}

これ、記事の通り下記に直したら直った。
(初めから記事の通りやっておけばよかった話)

エラー出なくなった.js
(process.env.NOW_REGION) ? module.exports = app : app.listen(PORT);
console.log(`Server running at ${PORT}`);

image.png
成功!

結論

自分でアレンジしたところが悪かった。記事の通りにやっておけばよかった。
でもなんでこの書き方だとダメだったんだろう。。。
あと、nowを起動させるときにこのjsファイルも見ているんだなあ。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

typescriptを使ってjavascriptのシンタックスシュガーを理解する

背景

typescriptを勉強していて、アロー演算子構文の読み方がわからない。ドキュメントを読んでもよくわからなかった、ということがありました。
そんな時、tscコマンドを使うことで難解なシンタックスシュガーへの理解のヒントになることに気が付きました。
それについて紹介します。

https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Functions/Arrow_functions

たとえば下記アロー演算子の構文がわかりませんでした。

['bbbbb', '3'].map(({ length }) => length)
仮引数に {} 使とは、、、?みたいな感じです。
出力内容から見ると、lengthプロパティを返していることを察することができましたが、いまいち腑に落ちませんでした。

tscコマンドの出番です

typescriptをjavascritに変更するコマンドがtscです。

arrow.ts
['bbbbb', '3'].map(({ length }) => length)

上記のようにをファイルに書き出して、tscコマンドを実行します。
tsc arrow.tsすると、arrow.jsというファイルが生成されます。中身を見るとシンタックスシュガーを使わずに実装されたjavascriptができています。

arrow.js
['bbbbb', '3'].map(function (_a) {
    var length = _a.length;
    return length;
});

arrow.jsを読むと、 ['bbbbb', '3'].map(({ length }) => length) がどのように実行されているのかがわかりました。
ローカル変数にlengthを格納しておいて、そのローカル変数をreturnしていたんですね。
すごいです。これは理解できます。

他の「初見殺し構文」を読んでみる

難解.ts
var f = ([a, b] = [1, 2], {x: c} = {x: a + b}) => a + b + c;
難解.js
var f = function (_a, _b) {
    var _c = _a === void 0 ? [1, 2] : _a, a = _c[0], b = _c[1];
    var c = (_b === void 0 ? { x: a + b } : _b).x;
    return a + b + c;
};

javascriptに変換したところで意味はわかりませんでしたが、頑張れば読めるような気がしていきました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

点と(直線 or 線分)の距離の2乗 JavaScriptでの実装

いろいろやり方はありますが、この記事では一次式の連立方程式を使って考えます。

点と直線の場合

o と p を通る直線に q からおろした垂線の距離の2乗を求めます。

o を p と q からあらかじめ引いておいてやると、計算が楽です。
image.png

以下のように求められます。

const
_Dist2 = ( px, py, qx, qy ) => {
    const num = px * qy - py * qx
    return num * num / ( px * px + py * py )
}
const
Dist2 = ( ox, oy, px, py, qx, qy ) => _Dist2( px - ox, py - oy, qx - ox, qy - oy )

注)o と p が同じ場合 0 / 0 になるので NaN を返します。

参考:点と直線の距離公式の3通りの証明@高校数学の美しい物語

点と線分の場合

交点(X,Y)を求めてそれが線分の内に入っているかどうかチェックします。
X,Y は以下のように求められます。

const den = px * px + py * py
const _ = ( py * qy + px * qx ) / den
const X = px * _
const Y = py * _ 

これの説明は文末につけておきます。

これの X が 0 から px, Y が 0 から py の中に入っていればよさそうです。
ただ、符号の判断が面倒になるので、計算の最初に p が第一象限にあるように鏡像を作ってから考えます。

const
_SegDist2 = ( px, py, qx, qy ) => {
    const den = px * px + py * py
    if ( den == 0 ) return NaN
    if ( px < 0 ) {
        px = -px
        qx = -qx
    }
    if ( py < 0 ) {
        py = -py
        qy = -qy
    }
    const _ = ( py * qy + px * qx ) / den
    const X = px * _
    if ( X < 0 || px < X ) return NaN
    const Y = py * _
    if ( Y < 0 || py < Y ) return NaN
    const num = px * qy - py * qx
    return num * num / den
}
const
SegDist2 = ( ox, oy, px, py, qx, qy ) => _SegDist2( px - ox, py - oy, qx - ox, qy - oy )

q から op に降ろせる点がない場合 NaN を返しているので、Number.isNaN() で判定してください。

交点の求め方

プログラムっぽく書いてみました。

    直線

        py  
    Y = -- X
        px  

    に直行する直線は

          px  
    Y = - -- X + c 
          py  

    これが、qx, qy を通るから

           px  
    qy = - -- qx + c 
           py  

    すなわち

          px          px  
    Y = - -- X + qy + -- qx
          py          py  

    なので交点は

    py       px          px  
    -- X = - -- X + qy + -- qx
    px       py          py  

        px * ( px * qx + py * qy )
    X = --------------------------
            px * px + py * py

        py * ( px * qx + py * qy )
    Y = --------------------------
            px * px + py * py
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【React + Material-UI】電卓アプリを作ろう!【2020年4月版】

はじめに

この記事は「Reactのチュートリアルを終えて、何かを作ってみたい」という読者を想定しています。
分からない部分が出た場合はReactの公式ドキュメントに立ち返りましょう。

material-uiの公式ドキュメントも要チェック!

作ったもの

React Calculator

スクリーンショット 2020-04-04 21.52.36.png

iPhoneに入っている電卓アプリのようなものを作りました。

Githubにソースコードを公開しています。

戦略

ReactコンポーネントにはMaterial-UIを使い、保持するstateをReactのuseStateで実装します。

component、containerの洗い出し

component

  • ボタン: 押したら何かしらの動作を起こすコンポーネント
    スクリーンショット 2020-04-05 0.20.00.png

  • ディスプレイ: 上部にある、数字を表示するためのコンポーネント

スクリーンショット 2020-04-05 0.20.57.png

container

  • calculator: ボタンとディスプレイを集結させたもの。今回はこれ自体がアプリとなる。

stateの洗い出し

iPhoneの電卓アプリをいじってみて、どんなstateが必要かを考えます。はじめにざっくりと書き出して、実際には実装しながら追加していくカタチになります。

  1. value1: 演算対象の数字の文字列
  2. value2: value1に変化を与える数字の文字列
  3. operator: add(加算), subtract(減算), divide(除算), multiply(乗算)。operatorが指定されていないときはfalseにする。

  4. isAllClear: clearボタンがAC / Cどちらかを判別するboolean。

  5. isDicimalInput: .ボタンが押されて小数点以下入力になっているか判別するboolean。

  6. isAnswerDisplay: =ボタンが押された直後には特殊な挙動をするので、判別するbooleanを用意しておく。

たとえば、 5 * 7 =
と入力した場合、value1は5, value2は7, operatorは"multiply"である。

インストール

create react app

npx create-react-app react-calculator
cd react-calculator

@material-ui/core

// npm を使う場合
npm install @material-ui/core

// yarn を使う場合
yarn add @material-ui/core

バージョン情報

  • react: 16.13.1
  • @material-ui/core: 4.9.8

ファイル構成

スクリーンショット 2020-04-04 23.04.52.png

コンポーネントの実装

Display

Display.js
import React from 'react';
import { makeStyles } from '@material-ui/core/styles';
import Box from '@material-ui/core/Box';

const useStyles = makeStyles((theme) => ({
    box: {
        fontSize: "60px",
        width: "100%",
        margin: "0 auto",
    },
  }));

export default function Display(props) {
  const classes = useStyles();

  return (
      <div>
       <Box className={classes.box}>
            {props.children}
       </Box>
      </div>
  );
}

ポイント

props.childrenとすることで、<Display></Display>ではさんだものを表示できます。

MyButton

MyButton.js
import React from 'react';
import { makeStyles } from '@material-ui/core/styles';
import Button from '@material-ui/core/Button';

export default function MyButton(props) {
  const useStyles = makeStyles((theme) => ({
    margin: {
      margin: theme.spacing(1),
      backgroundColor: props.backgroundColor
    },
  }));
  const classes = useStyles();


  return (
      <div>
        <Button className={classes.margin} onClick={props.onClick}>
            {props.children}
        </Button>
      </div>
  );
}

ポイント

ボタンコンポーネントは親から1. 背景色, 2. クリックしたときに実行する関数, 3. ボタンの文字を受け取ります。クリックしたときに実行する関数について、親コンポーネントで<MyButton onClick={somthing}>としても機能しないので、子のButtonのonClickに接続する必要があるんですね。

コンテナの実装

長いコードなのでコメントアウトで説明していきます。

Calculator.js
import React, {useState} from 'react';

//components
import MyButton from '../components/MyButton';
import Display from '../components/Display';

//material-ui
import Box from '@material-ui/core/Box';
import Typography from '@material-ui/core/Typography';

function Calculator(){
    //保持したいstates
    const [value1, setValue1] = useState("0");
    const [value2, setValue2] = useState("0");
    const [operator, setOperator] = useState(false);
    const [isAllClear, setIsAllClear] = useState(true);
    const [isDicimalInput, setIsDicimalInput] = useState(false);
    const [isAnswerDisplay, setIsAnswerDisplay] = useState(false);

    //数字ボタンを押したとき
    const handleNumberClick = (num) => {
        //=ボタンを押した直後に数字を押した場合
        if(isAnswerDisplay){
            setValue1(num);
            setValue2("0");
            setOperator(false);
            setIsAnswerDisplay(false);
            setIsDicimalInput(false);
        } else {
           //演算子が押されていないときはvalue1を更新する
           if(!operator){
                if(isDicimalInput){
                    setValue1(String(value1) + String(num))
                } else {
                    setValue1(String(Number(value1)*10+num));
                }
            //演算子が押されているときはvalue2を更新する
            } else {
                if(isDicimalInput){
                    setValue2(String(value2) + String(num))
                } else {
                    setValue2(String(Number(value2)*10+num));
                }
            }
        }
        //0以外が押されたときはACをCに変える
        if(num !== 0){
            setIsAllClear(false);
        }
    }

    //00ボタンを押したとき
    const handleDoubleZeroClick = () => {
        if(!operator){
            if(isDicimalInput){
                setValue1(String(value1) + "00")
            } else {
                setValue1(String(Number(value1)*100));
            }
        } else {
            if(isDicimalInput){
                setValue2(String(value2) + "00")
            } else {
                setValue2(String(Number(value2)*100));
            }
        }
    }

    //AC/Cボタンを押したとき
    const handleClearClick = () => {
        setIsDicimalInput(false);
        //ACのときはすべてリセット
        if(isAllClear){
            setValue1("0");
            setOperator(false);
            setIsAnswerDisplay(false)
        //Cのときはvalue2を"0"にする
        } else {
            setIsAllClear(true);
            if(isAnswerDisplay){
                setValue1("0")
            } else{
                if(!operator){
                    setValue1("0");
                } else {
                    setValue2("0");
                }
            }
        }
    }

    // -/+ボタンを押したとき
    const handleMinusPlusClick = () => {
        if(isAnswerDisplay){
            setValue1(-1 * Number(value1));
        } else {
            if(!operator){
                setValue1(-1 * Number(value1));
            } else {
                setValue2(-1 * Number(value2));
            }
        }

    }

    // %ボタンを押したとき
    const handlePercentageClick = () => {
        if(isAnswerDisplay){
            setValue1(Number(value1) * 0.01);
        } else {
            if(!operator){
                setValue1(Number(value1) * 0.01);
            } else {
                setValue2(Number(value2) * 0.01);
            }
        }
    }

    //演算子を押したとき
    //引数はadd subtract divide multiplyのどれか
    const handleOperatorClick = (a) => {
        if(isAnswerDisplay){
            setIsAnswerDisplay(false);
        } else {
            if(value2 !== "0"){
                if(operator === "add"){
                    setValue1(Number(value1) + Number(value2))
                } else if(operator === "subtract"){
                    setValue1(Number(value1) - Number(value2))
                } else if(operator === "divide"){
                    setValue1(Number(value1) / Number(value2))
                } else if(operator === "multiply"){
                    setValue1(Number(value1) * Number(value2))
                }
            }
        }
        setIsDicimalInput(false);
        setOperator(a);
        setValue2("0")
    }

    // .ボタンを押したとき
    const handleDicimalPointClick = () => {
        if(isDicimalInput) return;

        setIsDicimalInput(true);
        if(!operator){
            setValue1(String(value1) + '.')
        } else {
            setValue2(String(value2) + '.')
        }
    }

    // =ボタンを押したとき
    const handleAnswerClick = () => {
        setIsDicimalInput(false);
        setIsAnswerDisplay(true);
        if(operator === "add") {
            if(value2 === "0"){
                //演算子が指定されたあと、value2が入力されずに=が押された場合
                //例えば、5 + = と入力した場合、10と表示する。
                setValue2(value1)
                setValue1(Number(value1) + Number(value1))
            } else {
                setValue1(Number(value1) + Number(value2))
            }
        } else if(operator === "subtract") {
            if(value2 === "0"){
                setValue2(Number(value1))
                setValue1(Number(value1) - Number(value1))
            } else {
                setValue1(Number(value1) - Number(value2))
            }
        } else if(operator === "divide") {
            if(value2 === "0"){
                setValue2(Number(value1))
                setValue1(Number(value1) / Number(value1))
            } else {
                setValue1(Number(value1) / Number(value2))
            }
        } else if(operator === "multiply"){
            if(value2 === "0"){
                setValue2(Number(value1))
                setValue1(Number(value1) * Number(value1))
            } else {
                setValue1(Number(value1) * Number(value2))
            }
        }
    }

    //ディスプレイに表示する内容
    const displayMarkup = isAnswerDisplay ? (
        value1
    ) : (
        operator ? (
            value2
        ) : (
            value1
        )
    );

    // AC/Cボタンの表示
    const clearButtonMarkup = isAllClear ? (
        "AC"
    ) : (
        "C"
    )
    return(
        <div>
            <Typography variant="h5">
                React Calculator
            </Typography>
            <Display>
                {displayMarkup}
            </Display>
            //material-uiのBoxはflexを使って好きなように並べられる。公式ドキュメント参照。
            <Box display="flex" flexDirection="column">
                <Box display="flex" justifyContent="center">
                   //ボタンの中に表示したいものをMyButtonではさむ
                   //MyButtonのpropsに背景色、onClick関数を渡す
                    <MyButton backgroundColor="gray" onClick={handleClearClick}>
                        <Typography variant="h5">{clearButtonMarkup}</Typography>
                    </MyButton>
                    <MyButton backgroundColor="gray" onClick={handleMinusPlusClick}>
                        <Typography variant="h5">-/+</Typography>
                    </MyButton>
                    <MyButton backgroundColor="gray" onClick={handlePercentageClick}>
                        <Typography variant="h5">%</Typography>
                    </MyButton>
                    //onClickに引数を渡したい場合はこのように記述する
                    <MyButton backgroundColor="orange" onClick={() => handleOperatorClick("divide")}>
                        <Typography variant="h5" >÷</Typography>
                    </MyButton>
                </Box>
                <Box display="flex" justifyContent="center" >
                    <MyButton onClick={() => handleNumberClick(7)} backgroundColor="#e0e0e0">
                        <Typography variant="h5">7</Typography>
                    </MyButton>
                    <MyButton onClick={() => handleNumberClick(8)} backgroundColor="#e0e0e0">
                        <Typography variant="h5">8</Typography>
                    </MyButton>
                    <MyButton onClick={() => handleNumberClick(9)} backgroundColor="#e0e0e0">
                        <Typography variant="h5">9</Typography>
                    </MyButton>
                    <MyButton backgroundColor="orange" onClick={() => handleOperatorClick("multiply")}>
                        <Typography variant="h5" >×</Typography>
                    </MyButton>
                </Box>
                <Box display="flex" justifyContent="center">
                    <MyButton onClick={() => handleNumberClick(4)} backgroundColor="#e0e0e0">
                        <Typography variant="h5">4</Typography>
                    </MyButton>
                    <MyButton onClick={() => handleNumberClick(5)} backgroundColor="#e0e0e0">
                        <Typography variant="h5">5</Typography>
                    </MyButton>
                    <MyButton onClick={() => handleNumberClick(6)} backgroundColor="#e0e0e0">
                        <Typography variant="h5">6</Typography>
                    </MyButton>
                    <MyButton backgroundColor="orange" onClick={() => handleOperatorClick("subtract")}>
                        <Typography variant="h5" >-</Typography>
                    </MyButton>
                </Box>
                <Box display="flex" justifyContent="center">
                    <MyButton onClick={() => handleNumberClick(1)} backgroundColor="#e0e0e0">
                        <Typography variant="h5">1</Typography>
                    </MyButton>
                    <MyButton onClick={() => handleNumberClick(2)} backgroundColor="#e0e0e0">
                        <Typography variant="h5">2</Typography>
                    </MyButton>
                    <MyButton onClick={() => handleNumberClick(3)} backgroundColor="#e0e0e0">
                        <Typography variant="h5">3</Typography>
                    </MyButton>
                    <MyButton backgroundColor="orange" onClick={() => handleOperatorClick("add")}>
                        <Typography variant="h5" >+</Typography>
                    </MyButton>
                </Box>
                <Box display="flex" justifyContent="center">
                    <MyButton onClick={() => handleNumberClick(0)} backgroundColor="#e0e0e0">
                        <Typography variant="h5">0</Typography>
                    </MyButton>
                    <MyButton onClick={handleDoubleZeroClick} backgroundColor="#e0e0e0">
                        <Typography variant="h5">00</Typography>
                    </MyButton>
                    <MyButton onClick={handleDicimalPointClick} backgroundColor="#e0e0e0">
                        <Typography variant="h5" >.</Typography>
                    </MyButton>
                    <MyButton backgroundColor="orange" onClick={handleAnswerClick}>
                        <Typography variant="h5" >=</Typography>
                    </MyButton>
                </Box>
            </Box>
        </div>
    )
};

export default Calculator;

ポイント

  • handleNumberClickなどの引数がある関数は、onClick={() => somthing(引数)}のように記述します
  • stateはひとつにまとめても良いが、どこで何を使うかが決まってないうちは独立したstateにしておいて後からまとめよう。

App.js

App.js
import React from 'react';
import './App.css';

import Calculator from './containers/Calculator'

function App() {
  return (
    <div className="App">
      <div className="container">
        <Calculator />
      </div>
    </div>
  );
}

export default App;

App.css

App.css
.App {
  text-align: center;
}
.container {
  margin: 80px auto 80px auto;
  padding: 20px;
  max-width: 300px;
  background-color: #80deea;
}

以上で完成です!

おまけ

こちらの記事を参考に、gh-pagesでサイトを公開しました。
https://shintaro-hirose.github.io/react-calculator/

今後もReactで簡単なアプリを作っていきます!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む