20200403のJavaScriptに関する記事は23件です。

ももかん様への回答欄につけたかったHTMLコード

try1.html
<!DOCTYPE html>
<html>
<head><title>『項目の順番を保存して取得』(試行錯誤1)</title></head>
<body>
    <div id="app">
        <ul id="hoge">
            <li data-id="1">element 1</li>
            <li data-id="2">element 2</li>
            <li data-id="3">element 3</li>
        </ul>
        <p>get.sortlist1は... {{ sortlist1 }}</p>
        <p>set.sortlist2は... {{ sortlist2 }}</p>
    </div>
<!--
順番を保存するには、HTMLのdata-idで設定している順番を、storeのsetでlocalStorageに保存します。
保存した順番を取得するには、storeのgetを用い、getしたものをreturnさせてあげることで、リロードする前の項目順で表示されます。
-->
    <script src="./Sortable.js"></script>
    <script src="./vue.js"></script>
    <script>
        var app = new Vue
        (
            {
                el: "#app",
                data:
                {
                    sortlist1 : "",
                    sortlist2 : "",
                },
            }
        );

        Sortable.create
        (
            hoge,
            {
                group: "save",
                store:
                {
                    /**
                     * Get the order of elements. Called once during initialization.
                     * @param   {Sortable}  sortable
                     * @returns {Array}
                     */
                    get: function (sortable)
                    {
                        var order = localStorage.getItem(sortable.options.group.name);
                        //return order ? order.split('|') : [];
                        app.sortlist1=order.split('|');
                    },

                    /**
                     * Save the order of elements. Called onEnd (when the item is dropped).
                     * @param {Sortable}  sortable
                     */
                    set: function (sortable)
                    {
                        var order = sortable.toArray();
                        localStorage.setItem(sortable.options.group.name, order.join('|'));
                        app.sortlist2=order.split('|');
                    }
                },
                onEnd : function ()
                {
                    app.sortlist2=hoge.set();
                },
            }
        );
    </script>
</body>
</html>
try2.html
<!DOCTYPE html>
<html>
<head><title>『項目の順番を保存して取得』(試行錯誤2)</title></head>
<body>
    <div id="app">
        <ul id="hoge">
            <li data-id="1">element 1</li>
            <li data-id="2">element 2</li>
            <li data-id="3">element 3</li>
        </ul>
        <input type="button" value="並び順を取得" v-on:click="putsortable1" /></p>
        <p>get.sortlist1は・・・ {{ sortlist1 }}</p>
    </div>
<!--
順番を保存するには、HTMLのdata-idで設定している順番を、storeのsetでlocalStorageに保存します。
保存した順番を取得するには、storeのgetを用い、getしたものをreturnさせてあげることで、リロードする前の項目順で表示されます。
-->
    <script src="./Sortable.js"></script>
    <script src="./vue.js"></script>
    <script>
        var app = new Vue
        (
            {
                el: "#app",
                data:
                {
                    sortlist1 : "",
                    sortlist2 : "",
                },
                methods :
                {
                    putsortable1 : function (e)
                    {
                        this.sortlist = sortable4.toArray().join(',');
                    }
                }


            }
        );

        Sortable.create
        (
            hoge,
            {
                group: "save",
                store:
                {
                    /**
                     * Get the order of elements. Called once during initialization.
                     * @param   {Sortable}  sortable
                     * @returns {Array}
                     */
                    get: function (sortable)
                    {
                        var order = localStorage.getItem(sortable.options.group.name);
                        //return order ? order.split('|') : [];
                        app.sortlist1=order.split('|');
                    },

                    /**
                     * Save the order of elements. Called onEnd (when the item is dropped).
                     * @param {Sortable}  sortable
                     */
                    set: function (sortable)
                    {
                        var order = sortable.toArray();
                        localStorage.setItem(sortable.options.group.name, order.join('|'));
                        app.sortlist2=order.split('|');
                    }
                },
            }
        );
    </script>
</body>
</html>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

指定した年の日本の祝日を計算する

はじめに

東京オリンピック・パラリンピックが1年延期となりました。
今年はオリンピック開会式・閉会式に合わせて一部の祝日が移動していますが、来年はどうなるんでしょう?
もし同様に移動するのであれば、祝日判定も変わっちゃうし、
誰かの対応を祈って待つより自分で対応できるように今のうちに作っとけばいいんじゃない??

ということで日本の祝日を計算するnpmパッケージを作ってみました。

要件

機能

  • 西暦を与えるとその年の日本の祝日の一覧を返す
  • 日付を与えるとその日が祝日かどうかを判定する

対応年

  • 国民の祝日に関する法律が施行された1948年以降
    • 2021年以降は現行法が継続するものとして計算する

祝日とは

本記事内の「祝日」とは、国民の祝日に関する法律(祝日法)に定められている国民の祝日と、皇室慶弔行事に伴う法律によって定められる休日を指します。

さらに、実装する上でこれらの祝日を以下の5つに分けて考えていきたいと思います。

  • 固定日系
    • 日付が決められているもの(元日、建国記念の日等)
  • ハッピーマンデー系
    • m月第n月曜日と決められているもの(成人の日、海の日等)
  • 変動日系
    • 春分の日、秋分の日
  • 不定日系
    • 振替休日、国民の休日
  • 特定日系
    • 1年限りの休日(天皇の即位の日等)

祝日計算

祝日を計算するにあたって、祝日クラスを作っておきます。

model/holiday.js
module.exports = class Holiday {
  constructor(date, name, hasExtraHoliday) {
    this.date = date; // 祝日の日付
    this.name = name; // 祝日の名前
    this.hasExtraHoliday = hasExtraHoliday; // 振替休日を持つかどうか
  }
}

また、祝日を計算する用のクラスも作っておきます。

model/publicHoliday.js
const Holiday = require('./holiday');

module.exports = class PublicHoliday {
  constructor(year) {
    this.year = year;
  }
  // yearを元に祝日を計算していく
}

1. 固定日系

これは日付が決まっているので単純ですね。
(ちなみに、元日、建国記念の日、天皇誕生日、昭和の日、憲法記念日、みどりの日、こどもの日、山の日、文化の日、勤労感謝の日の10日が当てはまります)
ただし、日曜日と重なる場合は振替休日が発生するため、曜日の確認が必要です。

model/publicHoliday.js
_getNewYearsDay() {
  let date = new Date(this.year, 0, 1);
  let hasExtraHoliday = this._checkExtraHoliday(date);
  return new Holiday(date, '元日', hasExtraHoliday);
}

_checkExtraHoliday(date) {
  return date.getDay() === 0;
}

振替休日の確認は全ての祝日で必要なので、切り出して使い回すことにしました。
例として元日の場合を記載しましたが、残りの9日も同様になります。

2. ハッピーマンデー系

日付ではなく曜日で決められている祝日で、成人の日、海の日、敬老の日、スポーツの日が当てはまります。
例として成人の日(1月の第2月曜日)を挙げると、

  • 1月1日の曜日を求める
  • 1月1日の曜日から第1月曜日の日付を求める
  • 第1月曜日の日付に7を足す

という流れで第2月曜日を求められそうですが、曜日を求めるのと日付に7を足すのはともかく、1日(月初日)の曜日から第1月曜日の日付を求めるのは少し考える必要がありそうです。

javascriptのDate型では、曜日は以下のように表されます。

  • 曜日対応表
Sun. Mon. Tue. Wed. Thu. Fri. Sat.
0 1 2 3 4 5 6

ここで、便宜上翌週の日曜日を7、月曜日を8と置くと以下のようになります。

  • 曜日対応表(拡張版)
Sun. Mon. Tue. Wed. Thu. Fri. Sat. Sun. Mon.
0 1 2 3 4 5 6 7 8

これにより、月初日が日曜日または月曜日のときは「1」との差、火曜日から土曜日のときは「8」との差を求めれば、第1月曜日が求められることが分かります。

  • 月初日と第1月曜日の差
Sun. Mon. Tue. Wed. Thu. Fri. Sat. Sun. Mon.
1 0 6 5 4 3 2 - -

このまま場合分けをしても良いのですが、よく見るとこれは「8」との差を7で割った余りになっています。
ということで、余りを用いて求めていくことにします。

model/publicHoliday.js
_getComingOfAgeDay() {
  let date = this._calculateWeek(new Date(this.year, 0, 1), 1, 2);
  let hasExtraHoliday = this._checkExtraHoliday(date);
  return new Holiday(date, '成人の日', hasExtraHoliday);
}

_calculateWeek(date, day, weekNumber) {
  let diff = (7 + day- date.getDay()) % 7;
  date.setDate(date.getDate() + diff + (7 * (weekNumber - 1)));
  return date;
}

例によって日付計算部分は使い回せるように切り出しました。
また、今のところ予定は無いですが、月曜日以外でも対応できるようにしてみました。

3. 変動日系

春分の日と秋分の日は国立天文台によって算出される春分日、秋分日を基準にしています。
こちらの計算式については下記サイトのものをお借りしました。

春分の計算・秋分の計算

有効範囲は春分が1796年~2351年、秋分が1604年~2230年と十二分です。

model/publicHoliday.js
_getVernalEquinoxDay() {
  let date = new Date(this.year, 2, this._calculateEquinox('vernal'));
  let hasExtraHoliday = this._checkExtraHoliday(date);
  return new Holiday(date, '春分の日', hasExtraHoliday);
}

_getAutumnalEquinoxDay() {
  let date = new Date(this.year, 8, this._calculateEquinox('autumnal'));
  let hasExtraHoliday = this._checkExtraHoliday(date);
  return new Holiday(date, '秋分の日', hasExtraHoliday);
}

_calculateEquinox(season) {
  let a = 0;
  let b = 0;
  if (season === 'vernal') {
    a = 0.242385544201545;
    b = 20.9150411785049;
  }
  if (season === 'autumnal') {
    a = 0.242035499172366;
    b = 24.0227494548387;
  }
  return Math.floor(a * this.year - (Math.floor(this.year / 4) - Math.floor(this.year / 100) + Math.floor(this.year / 400)) + b);
}

ここまでで恒常的に存在する祝日については求められるようになりました。
続いて振替休日や国民の休日を求めていきたいのですが、この2つの祝日は祝日の曜日や祝日同士の関係によって求められるので、先に1年間の祝日を求められるようにします。

model/publicHoliday.js
getPublicHolidays() {
  let holidays = [
    this._getNewYearsDay(),
    this._getNationalFoundationDay(),
    this._getShowaDay(),
    this._getConstitutionMemorialDay(),
    this._getGreenryDay(),
    this._getChildrensDay(),
    this._getMountainDay(),
    this._getCultureDay(),
    this._getLabourThanksgivingDay(),
    this._getComingOfAgeDay(),
    this._getMarineDay(),
    this._getRespectForTheAgedDay(),
    this._getHealthAndSportsDay(),
    this._getTheEmperorsBirthday(),
    this._getVernalEquinoxDay(),
    this._getAutumnalEquinoxDay()
  ];
  return holidays;
}
model/holidayCollector.js
const Holiday = require('./holiday');
const PublicHoliday = require('./publicHoliday');

module.exports = class HolidayCollector {
  constructor() {
    this.holidays = [];
  }

  getHolidays(year) {
    this._getPublicHolidays(year);

    // 振替休日や国民の休日を求めてholidaysに追加する

    return this.holidays;
  }

  _getPublicHolidays(year) {
    let publicHoliday = new PublicHoliday(year);
    let publicHolidays = publicHoliday.getPublicHolidays();
    publicHolidays.forEach(holiday => {
      if (holiday) {
        this.holidays.push(holiday);
      }
    });
  }
}

4. 不定日系

振替休日については、各休日を求める際に振替休日を持つかどうかも確認しているため、リストの要素を一つずつ見ていけば問題ありません。
気を付ける必要があるのは、振替休日は必ずしも振替元の祝日の翌日(月曜日)になるとは限らないところです。

振替休日は、

「国民の祝日」が日曜日に当たるときは、その日後においてその日に最も近い「国民の祝日」でない日を休日とする。

と定められているため、既に国民の祝日となっている日が振替休日になることはありません。
また、上記の定義から振替休日がさらに振替休日を持つこともありません。

model/holidayCollector.js
_getSubstituteHolidays() {
  let substituteHolidays = [];
  this.holidays.forEach(holiday => {
    if (holiday.hasExtraHoliday) {
      let date = new Date(holiday.date.getTime());
      let isHoliday = true;
      // 振替元の日付から1日ずつ進めていき、祝日でも日曜でもない日を見つける
      while (isHoliday) {
        date.setDate(date.getDate() + 1);
        if (!this._findDate(date) && date.getDay()) {
          isHoliday = false;
        }
      }
      substituteHolidays.push(new Holiday(date, '振替休日', false));
    }
  });
  Array.prototype.push.apply(this.holidays, substituteHolidays);
}

// 与えられた日付が祝日リストに含まれているか検索する
_findDate(date) {
  let findedDate = this.holidays.find((holiday) => {
    return (holiday.date.getTime() === date.getTime());
  });
  return findedDate;
}

次に国民の休日ですが、これは、

その前日及び翌日が「国民の祝日」である日(「国民の祝日」でない日に限る。)は、休日とする。

と定められています。
つまり、祝日と祝日に挟まれた日も休日になるということですね。
ただし、挟まれた日が日曜日や振替休日の場合はそちらが優先されるため、国民の休日とはなりません。

model/holidayCollector.js
_getNationalHolidays() {
  let nationalHolidays = [];
  for (let i = 0; i < this.holidays.length - 1; i++) {
    // 日曜日を挟むパターンは考慮しない
    if (this.holidays[i].date.getDay() === 6) {
      continue;
    }
    if ((this.holidays[i + 1].date.getTime() - this.holidays[i].date.getTime()) / 86400000 === 2) {
      let date = new Date(this.holidays[i].date.getTime());
      date.setDate(date.getDate() + 1);
      nationalHolidays.push(new Holiday(date, '国民の休日', false));
    }
  }
  Array.prototype.push.apply(this.holidays, nationalHolidays);
}

また、上記のコードは祝日リストがソートされていることを前提に書いているため、ソートしてから実行するようにします。

model/holidayCollector.js
getHolidays(year) {
  this._getPublicHolidays(year);
  this._getSubstituteHolidays();
  this._sortHolidays();
  this._getNationalHolidays();
  this._sortHolidays();
  return this.holidays;
}

_sortHolidays() {
  this.holidays.sort(function(a, b) {
    if (a.date.getTime() < b.date.getTime()) return -1;
    if (a.date.getTime() > b.date.getTime()) return 1;
    return 0;
  });
}

5. 特定日系

ここまでで、祝日法にて定められた祝日については求められましたが、皇室慶弔行事に伴ってその年限りで定められる休日が残っています。
対応が必要なのは下記5つの法律によって定められた6日になります。

  • 昭和34年法律第16号 皇太子明仁親王の結婚の儀の行われる日を休日とする法律
    • 1959年4月10日
  • 平成元年法律第4号 昭和天皇の大喪の礼の行われる日を休日とする法律
    • 1989年2月24日
  • 平成2年法律第24号 即位礼正殿の儀の行われる日を休日とする法律
    • 1990年11月12日
  • 平成5年法律第32号 皇太子徳仁親王の結婚の儀の行われる日を休日とする法律
    • 1993年6月9日
  • 平成30年法律第99号 天皇の即位の日及び即位礼正殿の儀の行われる日を休日とする法律
    • 2019年5月1日
    • 2019年10月22日
model/holidayCollector.js
_getSpecificHolidays(year) {
  if (year === 1959) {
    this.holidays.push(new Holiday(new Date(1959, 3, 10), '皇太子明仁親王の結婚の儀の行われる日', false));
  }
  if (year === 1989) {
    this.holidays.push(new Holiday(new Date(1989, 1, 24), '昭和天皇の大喪の礼の行われる日', false));
  }
  if (year === 1990) {
    this.holidays.push(new Holiday(new Date(1990, 10, 12), '即位礼正殿の儀の行われる日', false));
  }
  if (year === 1993) {
    this.holidays.push(new Holiday(new Date(1993, 5, 9), '皇太子徳仁親王の結婚の儀の行われる日', false));
  }
  if (year === 2019) {
    let days = [
      new Holiday(new Date(2019, 4, 1), '天皇の即位の日', false),
      new Holiday(new Date(2019, 9, 22), '即位礼正殿の儀の行われる日', false)
    ];
    days.forEach(day => {
      this.holidays.push(day);
    });
  }
}

これらの休日は、祝日法で定められた祝日と同等のものとして扱われるため、先程求めた振替休日や国民の休日の計算対象になります。
(わざわざ日曜日に休日を定めた例は無いため、振替休日が発生したことはありませんが...)

したがって、祝日を求める際は、

  1. 祝日法に定められた恒常的な祝日
  2. その年限りの休日
  3. 振替休日
  4. 国民の休日

の順に求めていく必要があります(1と2は順不同です)。

model/holidayCollector.js
getHolidays(year) {
  this._getPublicHolidays(year);
  this._getSpecificHolidays(year);
  this._getSubstituteHolidays();
  this._sortHolidays();
  this._getNationalHolidays();
  this._sortHolidays();
  return this.holidays;
}

祝日の移動について

現行の祝日法に則った祝日については、これで対応が完了しました。
しかし、祝日法は何度か改正されており、過去の祝日を正しく求めるには祝日法の変遷を反映する必要があります。
例えば、「スポーツの日」の場合、

  • 1966年から「体育の日」として適用(10月10日)
  • 2000年から10月の第2月曜日に移動
  • 2020年から名称を「スポーツの日」に変更
  • 2020年のみ7月24日に移動

といった変更点があるので、これらを取り込みます。

model/publicHoliday.js
_getHealthAndSportsDay() {
  if (this.year < 1966) {
    return false;
  }
  let date = this._calculateWeek(new Date(this.year, 9, 1), 1, 2);
  let name = 'スポーツの日';
  if (this.year < 2020) {
    name = '体育の日';
  }
  if (this.year < 2000) {
    date = new Date(this.year, 9, 10);
  }
  if (this.year === 2020) {
    date = new Date(this.year, 6, 24);
  }
  let hasExtraHoliday = this._checkExtraHoliday(date);
  return new Holiday(date, name, hasExtraHoliday);
}

どの祝日も変更内容自体は難しいものではないので、適用年に気を付けるくらいです。

  • 祝日の変遷
1948 1949 - 1965 1966 1967 - 1973/4/11 1973/4/12 - 1985 1986 - 1988 1989 - 1995 1996 - 1999 2000 - 2002 2003 - 2006 2007 - 2015 2016 - 2018 2019 2020 2021 -
元日 - 1/1 1/1 1/1 1/1 1/1 1/1 1/1 1/1 1/1 1/1 1/1 1/1 1/1 1/1
成人の日 - 1/15 1/15 1/15 1/15 1/15 1/15 1/15 1月第2月曜日 1月第2月曜日 1月第2月曜日 1月第2月曜日 1月第2月曜日 1月第2月曜日 1月第2月曜日
建国記念の日 - - - 2/11 2/11 2/11 2/11 2/11 2/11 2/11 2/11 2/11 2/11 2/11 2/11
天皇誕生日 - 4/29 4/29 4/29 4/29 4/29 12/23 12/23 12/23 12/23 12/23 12/23 - 2/23 2/23
春分の日 - 春分日 春分日 春分日 春分日 春分日 春分日 春分日 春分日 春分日 春分日 春分日 春分日 春分日 春分日
昭和の日 - - - - - - - - - - 4/29 4/29 4/29 4/29 4/29
憲法記念日 - 5/3 5/3 5/3 5/3 5/3 5/3 5/3 5/3 5/3 5/3 5/3 5/3 5/3 5/3
みどりの日 - - - - - - 4/29 4/29 4/29 4/29 5/4 5/4 5/4 5/4 5/4
こどもの日 - 5/5 5/5 5/5 5/5 5/5 5/5 5/5 5/5 5/5 5/5 5/5 5/5 5/5 5/5
海の日 - - - - - - - 7/20 7/20 7月第3月曜日 7月第3月曜日 7月第3月曜日 7月第3月曜日 7/23 7月第3月曜日
山の日 - - - - - - - - - - - 8/11 8/11 8/10 8/11
敬老の日 - - 9/15 9/15 9/15 9/15 9/15 9/15 9/15 9月第3月曜日 9月第3月曜日 9月第3月曜日 9月第3月曜日 9月第3月曜日 9月第3月曜日
秋分の日 秋分日 秋分日 秋分日 秋分日 秋分日 秋分日 秋分日 秋分日 秋分日 秋分日 秋分日 秋分日 秋分日 秋分日 秋分日
体育の日 - - 10/10 10/10 10/10 10/10 10/10 10/10 10月第2月曜日 10月第2月曜日 10月第2月曜日 10月第2月曜日 10月第2月曜日 - -
スポーツの日 - - - - - - - - - - - - - 7/24 10月第2月曜日
文化の日 11/3 11/3 11/3 11/3 11/3 11/3 11/3 11/3 11/3 11/3 11/3 11/3 11/3 11/3 11/3
勤労感謝の日 11/23 11/23 11/23 11/23 11/23 11/23 11/23 11/23 11/23 11/23 11/23 11/23 11/23 11/23 11/23
振替休日 - - - -
国民の休日 - - - - -

特に、振替休日に関する改正は1973年4月12日に施行されているため、

  • 1973年の建国記念の日は日曜日ですが、振替休日はなし
  • 1973年の天皇誕生日は日曜日で、振替休日あり

と1年の中で差が出ています。

パッケージ公開

これで全ての対応が完了しました。
あとは必要な機能に合わせて関数を用意して完成です!

index.js
const HolidayCollector = require('./model/holidayCollector');

module.exports = {
  getHolidaysInYear: function (year) {
    let holidays = new HolidayCollector().getHolidays(year);
    return holidays;
  },

  isHoliday: function (date) {
    // 与えられた日付の年の祝日リストを取得し、その中に与えられた日付があるか判定する
    let targetDate = new Date(date.getFullYear(), date.getMonth(), date.getDate());
    let holidays = new HolidayCollector().getHolidays(targetDate.getFullYear());
    let target = holidays.find((holiday) => {
      return (holiday.date.getTime() === targetDate.getTime());
    })
    if (target) {
      return true;
    }
    return false;
  }
}

パッケージの公開にあたっては、下記の記事を参考にさせていただきました。ありがとうございます。

初めてのnpm パッケージ公開

また、実際に公開したものが以下になります。

public-holidays-jp

おわりに

この記事を書いている間にこんなニュース記事が出ていました。

来年も五輪開閉会式に合わせ祝日移動へ - 産経ニュース

改正案がいつ通るのかは分かりませんが、やはり対応が必要そうです。

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

配列って難しい。

let fileIndex = [1,2,3,4,5,6,7,8,9,10];
fileIndex.shift();
//中身
fileIndex = 9[2,3,4,5,6,7,8,9,10]
fileIndex.push(fileIndex[fileIndex.length - 1] + 1)
//計算の順番に並べてます
//[9-1]
//fileIndex[8]
//2,3,4,5,6,7,8,9,10
//0,1,2,3,4,5,6,7, 8 この順番なので
//↓そのために
//10
//10+1=11
//となるので
console.log(fileIndex);
//10[2,3,4,5,6,7,8,9,10,11]

fileIndex.push(fileIndex[fileIndex.length - 1] + 1)
この一文でここまで理解するとは思いませんでした。

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

動的な配列って何?

プログラミングスクールでチーム開発をする際に、

参考にした記事のコードでこの一文で結構悩まされたので

メモに書いときます。

let fileIndex = [1,2,3,4,5,6,7,8,9,10];
fileIndex.shift();

console.log(fileIndex);
//9[2,3,4,5,6,7,8,9,10]
fileIndex.push(fileIndex[fileIndex.length - 1] + 1)
//計算の順番に並べてます
//fileIndex.push(fileIndex[9 - 1] + 1)
//fileIndex.push(fileIndex[8] + 1)
//2,3,4,5,6,7,8,9,10 ←この数字を取り出す
//0,1,2,3,4,5,6,7,[8番目] 配列の順番は下の文になります。
//↓そのために
//10
//fileIndex.push(10 + 1)
//fileIndex.push(11)
consol.log(fileIndex);
10[2,3,4,5,6,7,8,9,10,11]

fileIndex.push(fileIndex[fileIndex.length - 1] + 1)

一文だけにこんなに時間を使うとはは思いませんでした。

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

Vue.js用カレンダーライブラリ「v-calendar」の最新バージョンがimport出来ない件

プロジェクトにおいてRailsのWebpackerでVue.jsを使っていて、カレンダー機能を実装するために色々調べているとv-calendarというライブラリがあることを知った。
早速使ってみようと思って公式ドキュメントの通りインストールして設定下が、謎のエラーが発生したので記事にすることにした。
結論から言うと、エラーの原因はよく分からず、ただ後述するようにバージョンを下げてインストールするとうまく動いたので、解決策として記載する。

Version

Vue.js 2.6.11
Ruby on Rails 6.0.0

インストール

v-calendar公式ドキュメントに従ってyarnを用いてv-calendarをインストールする。

yarn add v-calendar@next

するとv-calendarの1.0.0-beta.23がインストールされた。

package.json
{
  "name": "...",
  "private": true,
  "dependencies": {
    "@coreui/coreui": "2.1.6",
    "@rails/actioncable": "^6.0.0-alpha",
    "@rails/activestorage": "^6.0.0-alpha",
    "@rails/ujs": "^6.0.0-alpha",
    "@rails/webpacker": "^4.0.7",
     // 中略
    "turbolinks": "^5.2.0",
    "v-calendar": "^1.0.0-beta.23",
    "vue": "^2.6.11",
    "vue-loader": "^15.9.1",
    "vue-template-compiler": "^2.6.11",
    "waypoints": "^4.0.1"
  },
  "version": "0.1.0",
  "devDependencies": {
    "webpack-dev-server": "^3.10.3"
  }
}

そしてwebpackerで使用するためにimportしてプラグインとして登録する。

app/javascript/packs/application.js
import Vue from 'vue'
import App from '../app.vue'
import VCalendar from 'v-calendar'
Vue.use(VCalendar)

document.addEventListener('DOMContentLoaded', () => {
  new Vue({
    el: '#vue',
    render: h => h(App)
  })
})

公式ドキュメントに従ってここまで設定してローカルサーバを起動したところ、コンソール上で"export 'default' (imported as 'VCalendar') was not found in 'v-calendar'と怒られてる。
ブラウザでも、Uncaught TypeError: Cannot read property 'mixin' of undefinedと怒られている。
公式通りにやってるのに何でや?と思って色々調べてみたがよく分からず。。。詰みかけていたところでこんなIssueを発見した。
"export 'default' (imported as 'Calendar') was not found in 'v-calendar/lib/components/calendar.umd'

わしの詰まってるエラーとほぼ同じや!!!きっとここに解決策が書いてあるはず!
と思って読んでいくと、肝心の開発者からの返答がない。
他のメンバーからも、はよ返事しろや。と怒られている。
(原文はAny responses to this?)

解決策

詰んだ………と思ってぼーっとIssueを眺めていたら、質問者のv-calendarのバージョンがv1.0.1 and 1.0.0-beta.23であることに気づく。
これもしかしたらバージョン下げたらいけんじゃね?と思い下記を実行。

yarn add v-calendar@1.0.0-beta.22

エラー発生せず解決した…!!!

結論

v-calendarを使う際はバージョンを1.0.0-beta.22に指定しよう。

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

Kinx ライブラリ - XML

XML

News!

Kinx - 3rd Preview Release!

これまでの修正も含め、3rd Preview Release を行いました。ここで触れている Fiber の修正 も含まれてます。

しかし未だ プレビュー。もし宜しければバグ報告等頂けると大変助かります。特に今回の XML はあまりテストできていない感満載。もうちょっとテストできてから紹介しようかとも思ったものの、せっかくのプレビュー版で使い方が分からないのもどうかと思い公開することにしました。実装自体はしてあり、サンプルは動くことを確認済です。

はじめに

「見た目は JavaScript、頭脳(中身)は Ruby、(安定感は AC/DC)」 でお届けしているスクリプト言語 Kinx。言語はライブラリが命。ということでライブラリの使い方編。

今回は XML です。

XML もよく利用するのでスクリプト言語でサクッと扱いたい要素の一つ。

XML

DOMパース

Xml.parseFile() または Xml.parseString() を使って DOM ツリーを構築する。ファイルを読み込む場合は以下の通り。

var doc = Xml.parseFile("xmlfile.xml");

下記は文字列を直接パースする例。

var doc = Xml.parseString(%{
<?xml version="1.0" encoding="UTF-8" ?>
<artists>
  <artist country="US" id="1">
    <name>BON JOVI</name>
    <price>2400</price>
    <img file="bonjovi.jpg"/>
  </artist>
  <artist country="US" id="2">
    <name>GUNS N ROSES</name>
    <price>21000</price>
    <img file="GNR.jpg"/>
  </artist>
  <artist country="DE" id="3">
    <name>Helloween</name>
    <price>2400</price>
    <img file="helloween.jpg"/>
  </artist>
</artists>
});

返されたドキュメント・オブジェクトは以下のメソッドを持つ。

メソッド 内容
documentElement() ルートドキュメントを取得
createElement(tagname) Element ノードを作成する
createTextNode(text) Text ノードを作成する
createComment(comment) Comment ノードを作成する
createCdataSection(content) CDATA Section ノードを作成する
createProcessingInstruction(target, data) Processing Instruction ノードを作成する
createEntityReference(name) Entity Reference ノードを作成する
createElementNS(nsUri, qname) Element ノードを名前空間を指定して作成する
getElementById(id) id を指定してノードを検索する
getElementsByTagName(tagName) tagName のノードを配列にして返す
xpath(expr) expr の XPATH を評価し、結果を配列にして返す

ルートノード

ルートノードは documentElement() メソッドを使用して以下のように取得する。

var root = doc.documentElement();

XMLノード

ルートノードを含む XML ノードは以下のプロパティとメソッドを持つ。

プロパティ
プロパティ 内容
type ノード種別
name QName
tagName タグ名
localName ローカル名
namespaceURI 名前空間 URI
prefix プレフィックス
メソッド
メソッド 内容
attributes() 属性一覧を配列で返す。
setAttribute(qname, value) 属性を設定する。
setAttributeNS(nsUri, qname, value) 名前空間を指定して属性を設定する。
removeAttribute(qname) 属性を削除する。
removeAttributeNS(nsUri, localName) 名前空間を指定して属性を削除する。
parentNode() 親ノードを返す。
children() 子ノードを配列で返す。
firstChild() 最初の子ノードを返す。
lastChild() 最後の子ノードを返す。
nextSibling() 次のノードを返す。
previousSibling() 前のノードを返す。
appendChild(node) 子ノードにノードを追加する。
removeChild(node) 子ノードからノードを削除する。
replaceChild(node1, node2) 子ノードのノードを置き換える。
replaceNode(node) 自分自身のノードを別のノードでを置き換える。
insertBefore(node) 前のノードとしてノードを追加する。
insertAfter(node) 次のノードとしてノードを追加する。
remove() ノードを削除する。
textContent() テキストを取得する。
innerText() テキストを取得する。
hasChildren() 子ノードが存在すれば 1 を返す。
hasAttributes() 属性があれば 1 を返す。
getElementById(id) id を指定してノードを検索する
getElementsByTagName(tagName) tagName のノードを配列にして返す
xpath(expr) expr の XPATH を評価し、結果を配列にして返す

XPath

XPath は XPATH 式にマッチしたノードをノードセット(配列)の形で返す。ノードセットにも xpath() メソッドがあり、絞り込んだノード群に対し XPATH をチェインして使うことができる。

先ほどのサンプル XML のドキュメントに対して実行。

var nodes = doc.xpath("//artist")
               .xpath("price")
               .map(&(p) => p.innerText());

nodes.each(&(text) => {
    System.println(text);
});

結果。

2400
21000
2400

サンプル・ソース

同梱しているサンプルソースを載せておきます。説明していない Xml.Writer とかありますが、こんな感じの DOM パースができる例ということで、参考になると思い。

function displayXml(doc, node, indent) {
    System.print("  " * indent);
    if (node.type == Xml.ELEMENT_NODE) {
        System.print("ELEM %s" % node.name);
    } else if (node.type == Xml.TEXT_NODE) {
        System.print("TEXT %s" % node.value.trim());
    }

    var attr = node.attributes();
    for (var i = 0, len = attr.length(); i < len; ++i) {
        System.print("[%s=%s]" % attr[i].name % attr[i].value);
    }
    System.println("");

    var child = node.firstChild();
    while (child) {
        displayXml(doc, child, indent + 1);
        child = child.nextSibling();
    }
}

var doc = Xml.parseString(%{
<?xml version="1.0" encoding="UTF-8" ?>
<artists>
  <artist country="US" id="1">
    <name>BON JOVI</name>
    <price>2400</price>
    <img file="bonjovi.jpg"/>
  </artist>
  <artist country="US" id="2">
    <name>GUNS N ROSES</name>
    <price>21000</price>
    <img file="GNR.jpg"/>
  </artist>
  <artist country="DE" id="3">
    <name>Helloween</name>
    <price>2400</price>
    <img file="helloween.jpg"/>
  </artist>
</artists>
});
var root = doc.documentElement();
displayXml(doc, root);

var el = root.getElementById("3");
if (el) {
    el.remove();
}

System.println("");
System.println("getElementByTagName:");
var els = root.getElementsByTagName("img");
if (els.isArray) {
    els.each(&(el) => displayXml(doc, el));
}

System.println("");
System.println("XPath:");
var nodes = doc.xpath("//artist").xpath("price");
if (nodes.isArray) {
    nodes.each(&(el) => displayXml(doc, el));
}

var xmlWriter = new Xml.Writer(System);
xmlWriter.write(doc);
xmlWriter.write(root);

実行結果。

ELEM artists
  TEXT
  ELEM artist[country=US][id=1]
    TEXT
    ELEM name
      TEXT BON JOVI
    TEXT
    ELEM price
      TEXT 2400
    TEXT
    ELEM img[file=bonjovi.jpg]
    TEXT
  TEXT
  ELEM artist[country=US][id=2]
    TEXT
    ELEM name
      TEXT GUNS N ROSES
    TEXT
    ELEM price
      TEXT 21000
    TEXT
    ELEM img[file=GNR.jpg]
    TEXT
  TEXT
  ELEM artist[country=DE][id=3]
    TEXT
    ELEM name
      TEXT Helloween
    TEXT
    ELEM price
      TEXT 2400
    TEXT
    ELEM img[file=helloween.jpg]
    TEXT
  TEXT

getElementByTagName:
ELEM img[file=bonjovi.jpg]
ELEM img[file=GNR.jpg]

XPath:
ELEM price
  TEXT 2400
ELEM price
  TEXT 21000
<artists>
        <artist country="US" id="1">
                <name>BON JOVI</name>
                <price>2400</price>
                <img file="bonjovi.jpg" />
        </artist>
        <artist country="US" id="2">
                <name>GUNS N ROSES</name>
                <price>21000</price>
                <img file="GNR.jpg" />
        </artist>
</artists>
<artists>
        <artist country="US" id="1">
                <name>BON JOVI</name>
                <price>2400</price>
                <img file="bonjovi.jpg" />
        </artist>
        <artist country="US" id="2">
                <name>GUNS N ROSES</name>
                <price>21000</price>
                <img file="GNR.jpg" />
        </artist>
</artists>

おわりに

XPath が使えると便利だ。

そして、XML と Zip(以前の記事)を組み合わせると、実は Xlsx ファイル(Excel ファイル)の読み書きができたりします。Xlsx ファイルは Office Open XML という名前で標準仕様化されており(色々問題もあるが)、XML ファイルを Zip で固めたものになってるので読めたりするといった具合。

ただ、実際問題として Office Open XML の全てをサポートするってのは相当量のコードになるので、すぐにできそうなのは簡易的な読み書きくらいですね。時間があればチャレンジしよう。

ではまた次回。

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

高階関数

 何かと話題になる「高階関数」ですが、自分なりのまとめです。
 関数型言語だと使う目的が違ったりすると思うので、ECMAScriptといった言語が中心です。

高階関数とは

 Wikipediaによると言葉の定義としては以下の通り。

高階関数とは、第一級関数をサポートしているプログラミング言語において少なくとも以下のうち1つを満たす関数である
 ・ 関数(手続き)を引数に取る
 ・ 関数を返す

 定義としては実にシンプルで、どのようなときに使えるかを記事では書いていきます。

関数を引数に取る関数

 ECMAScriptを触っている人なら”コールバック関数”という単語を目にしたことも多いと思います。このコールバック関数を受け取ってる関数が高階関数です。

どんなとき使う?

 一言で表すなら「引数の関数に一部処理を委譲することで、アルゴリズムを抽象化(共通化)できる場合」です。

具体的な例

 例えば、ソートを考えてみます。ソートにはクイックソート、マージソート、バブルソートなど様々なアルゴリズムがありますが、ソートは大雑把に書けば以下を繰り返して、すべての要素に対して順序を決めることです。

  1. 集合から何らかのルールに沿って二つの要素を取り出す
  2. 取り出した二つの要素を比較して大小関係を決める
  3. 決まった大小関係に従って、集合内の要素を入れ替える

 ソートを実装する上で厄介なのが2番目の処理です。アプリケーションの世界では、比較対象は数値もあれば、文字列、またはユーザー定義型など様々な形式があり、比較方法もアプリケーションの要件によって異なります。そのため、ソート処理を汎用化するには工夫が必要になります。

 この工夫が”関数を引数に取る関数”です。

 ソートアルゴリズムのうち「二つの要素を比較して大小関係を決める」処理を外部から受け取るようにします。こうすることでソートアルゴリズム自体は汎用的に実装することができます。

  1. 集合から何らかのルールに沿って二つの要素を取り出す
  2. 取り出した二つの要素を外部から受け取った比較関数を使って大小関係を決める
  3. 決まった大小関係に従って、集合内の要素を入れ替える

 疑似的なコードで書けば次のようになります(実際に動くサンプルではありません)。

ソート実装側
// ここでは集合がどのような実装になっているかは触れません

// ソート処理実装側
// comparatorは比較関数
const sort = comparator => {
  // すべての要素の順序が確定するまで、繰り返す

  // 集合から何らかのルールに沿って二つの要素を取り出す
  const elmA, elmB = ...

  // 外部から受け取った比較関数を使って大小関係を決める
  //(例えば、elmAが大きい場合は正の値、大小関係が同じなら0、elmAが小さい場合は負の値)
  const order = comparator(elmA, elmB)

  // 決まった大小関係order従って、集合内の要素elmAとelmBを入れ替える
}

 一方、ソートを呼び出す側は次のようになります。

アプリケーション実装側
// 大小関係を決める比較関数
const myComparator = (lho, rho) => {
  // 本来なら引数に対して大小関係を決めて値を返す
  return 0 // 0 固定なので並び変わらない!
}

// ソート処理を呼び出す
myCollection.sort(myComparator)

// アカウントの集合に対して年齢でソートする
accounts.sort((a1, a2) => a1.age - a2.age)

 このように比較処理(関数)を呼び出し元から渡してもらうことで、ソート処理の共通化ができます。当記事最後の方にも書いた複雑な比較を行うソートであっても、ソート処理の修正は不要です。

関数を引数に取る関数のまとめ

 処理の一部を引数として渡される関数に委譲することで、アルゴリズムの抽象化(共通化)ができ、再利用性を高めることができる。

関数を返す関数

 知ると便利なのですが、案外難しいのが関数を返す関数。

どんなとき使う?

 一言で表すと「関数を作りたい場合」です。

 説明になってませんね。
 高階関数で関数を作りたい理由は色々あり、幾つかの例を挙げます。

  • 状態を持った関数(クロージャー)を作りたい(今回メインで紹介する内容)
  • カリー化したい(当記事の最後に紹介します)
  • 定型的な関数を楽に作りたい(当記事最後の方にデコレーターや比較関数を高階関数で作る例を紹介しています)

 繰り返しになりますが、ここでは1点目を中心に紹介します。

具体的な例

 よくある、カウンタープログラムで考えてみます。
 まず高階関数を使わない場合。

高階関数を使わない例
// 分かりやすく、酷い実装です
let state = 0
const countup = () => ++state

console.log(countup()) // 1
console.log(countup()) // 2
console.log(countup()) // 3

// このようなこともできてしまう!
state = 100 // 100

 このコードの酷いところは、stateの値をcountup関数以外でも更新できてしまうことです。
 この問題を解決するための手段は色々ありますが、そのうちの一つ、関数を返す関数で考えてみます。

高階関数を使った例
// createCounterが関数を返す関数(高階関数)
const createCounter = () => {
  let state = 0
  return () => ++state
}

const countup = createCounter() // 0

// countupは関数のため()を付けて実行できます。
console.log(countup()) // 1
console.log(countup()) // 2
console.log(countup()) // 3

// countup()内のstateは外部から操作できない
// state = 100

 次のような実装も可能ですが、こちらは関数ではないオブジェクトを返しているので、高階関数ではありません。

高階関数ではない例
const createCounter = () => {
  let state = 0
  return { countup: () => ++state }
}

const counter = createCounter() // 0

counter.countup() // 1
counter.countup() // 2
counter.countup() // 3

// こちらのメリットは複数の操作関数を定義できることです
// createCounter()呼び出しのたびにオブジェクト内に関数が作られることが
// 問題になるような場合はクラス化も検討

 話は戻って、もう少し高階関数の例を示します。
 今度は初期値とカウントアップする量を指定できるように拡張します。

高階関数版の拡張
const createCounter = (initialVale = 0) => {
  let state = initialVale
  return (steps = 1) => state += steps
}

const countup = createCounter(5) // 5

console.log(countup())  // 6
console.log(countup(2)) // 8
console.log(countup(3)) // 11

// 次のように実行もできますが、結果は異なります。なぜでしょうか?
createCounter(5)()
createCounter(5)(2)
createCounter(5)(3)
// 高階関数の実装方法と実行方法次第で結果が変わることもあるので、注意が必要です

 外側の関数(高階関数)は当然ながら引数を受け取ることができ、初期状態のセットアップを行うことができます。そして、高階関数が返す関数も引数を受け取ることができるため、セットアップ済みの状態をもとに処理が行えます。

 このメリットは、コストの掛かる処理を一度だけ実行し、その結果をもとに引数の値を変えながら処理を何度も実行する、といったことが簡単に実現できる点にあります。

 例えば、指定したURLからHTMLをダウンロードして、HTMLで使われているタグをカウントする処理を考えます。高階関数を用いることでHTMLのダウンロードは一度だけ行い、タグのカウント時はHTMLを再ダウンロードしない、といったことが簡単に実現できます(いったんHTML取得の非同期処理云々は無視します)。

const tagCounter = url => {
  // urlで指定されたサイトをダウンロードする処理
  const html = getContents(url)

  // html内に指定されたタグが何回出現するか数える関数を返す
  return tagName => countTag(html, tagName)
}

const yNews = 'https://どこかのニュースサイト'
// 1回だけhtml取得が行われ、yNews専用のカウンター関数になる
const yNewsTagCounter = tagCounter(yNews)

yNewsTagCounter('div')  // キャッシュしたhtmlをつかう
yNewsTagCounter('span') // キャッシュしたhtmlをつかう
yNewsTagCounter('li')   // キャッシュしたhtmlをつかう

 返される関数内でダウンロードするように実装すれば、htmlのダウンロード実行を遅らせることもできます(その分、返される関数側がやや複雑になる)。

const tagCounter = url => {
  let html

  // html内に指定されたタグが何回出現するか数える関数を返す
  return tagName => {
      // ダウンロードができていない場合、一回だけダウンロードする
      if (!html) html = getContents(url)
      return countTag(html, tagName)
  }
}

const yNews = 'https://どこかのニュースサイト'
// yNews専用のカウンター関数になる。この時点ではhtmlはダウンロードが行われない
const yNewsTagCounter = tagCounter(yNews)

yNewsTagCounter('div')  // ここで初めてhtmlのダウンロードが行われる
yNewsTagCounter('span') // キャッシュしたhtmlをつかう
yNewsTagCounter('li')   // キャッシュしたhtmlをつかう

 補足:後述のカリー化目的の場合は別ですが、そもそも上記例のように、前の処理の結果で引数が変わらない場合や連続して実行できる場合、高階関数化せずにタグ名を配列で受け取れるようにすればよいので、なんでも高階関数化は避けた方が良いでしょう。

関数を返す関数のまとめ

 返される関数に与える引数を変えながら、関数内に保持した状態を使って(何度も)処理できる関数が作れる。

 関数を引数にとる関数とは異なり代替手段もいくつかあるため、本当に必要か見極める必要があります。
 また、コードサンプルで示した通り、実行方法(部分適用有無)によって結果が異なるような実装の場合、注意する必要があります。

 なお初めに挙げた他の目的(カリー化や定型的な関数作成の簡略化)については補足で紹介します。

いったん高階関数まとめ

 高階関数は利用する立場だと案外分かり易いのですが、設計する(高階関数を提供する)側になるととたんに難しくなると思います。
 正しく設計できると非常に強力なものですが、「これわざわざ高階関数にする必要ないじゃん」みたいなものは、保守性が下がったり、想定外の動作をしたりするので注意が必要です。

高階関数の補足

 ここからは高階関数に絡みそうな雑多な内容です。

デコレーター

 言語によって異なりますが、いわゆる「デコレーター」は「関数を引数にとる関数」と「関数を返す関数」を組み合わせることで実現することができます。

 例えば、ある関数を呼び出す際にデコレーターを使うと、呼び出し先の関数を修正することなく、機能を追加できます。

  • 関数の実行前後でログを出力したい
  • 関数の実行時間を計測したい
  • 共通的な例外処理をしたい

疑似的なコードは次のようになります。

独自デコレーターを作る
const myDecorator = func => (...args) => {
  console.log('開始');
  const result = func.apply(this, args);
  console.log('終了');
  return result
}

// yNewsTagCounter は前に出てきた関数
const decoratedYNewsTagCounter = myDecorator(yNewsTagCounter)
decoratedYNewsTagCounter('div')  // コンソールに'開始''終了'が出力される
decoratedYNewsTagCounter('span') // コンソールに'開始''終了'が出力される
decoratedYNewsTagCounter('li')   // コンソールに'開始''終了'が出力される

 アノテーションベースのデコレーター(Python、TypeScriptなど)が使えると、デコレーターの適用が楽になります。

コールバック関数にクロージャーを使う

 コールバック関数も工夫次第ではとても強力な武器になるという話です。
 Wikipediaのクロージャーには次のような説明があります。

ライブラリの設計者は、関数(コールバック関数)を引数として受け取る関数(高階関数)を定義することで、利用者が挙動をカスタマイズできる汎用的なライブラリ関数を提供することができる。その際、クロージャを高階関数の引数として渡すことで、記述の簡素化や高階関数の外側の状態の参照が可能となる。例えばコレクションのソートを行う関数は、比較関数を引数に渡すことで、利用者が定義した基準でソートできるようになるが、クロージャを使うことでさらに自由度の高い比較処理を簡潔に記述することができるようになる。

 クロージャーの詳細は割愛しますが、関数を返す関数で示したような”状態を持った関数”だと思ってください。
 例えば人の名前で並び変えるとき、同姓の場合は名で並び変えるようなことがあります。

  • 姓の昇順
  • 名の昇順

 これをクロージャーを使えば簡潔に記述できる、と言っていると私は理解し、それっぽいサンプルを書いてみました。
 細かい説明は省略しますが、関数を返す関数も例として実装しています。

複雑な比較関数を作ってみる:共通関数部分
// せっかくなので、今まで出てきた内容も盛り込んでいます

// あまりECMASript詳しくないので間違ってるかも
// とりあえずNode.jsで動作する比較関数に関するユーティリティクラス
// あまりきちんとクラス設計はしていません
const ComparatorUtils = class {
  constructor(comparator) {
    this.comparators = [comparator]
  }

  // 一番初めに使用する比較関数を登録する
  static comparing (keyExtractor, comparator = ComparatorUtils.#defaultComparator) {
    return new ComparatorUtils(ComparatorUtils.keyExtractorComparator(keyExtractor, comparator))
  }

  // 要素から値を取得する方法をコールバック関数で渡すと比較関数を作ってくれる関数
  // 関数を返す関数(高階関数)を使う目的の一つ
  static keyExtractorComparator =  (keyExtractor, comparator = ComparatorUtils.#defaultComparator) =>
    (e1, e2) => comparator(keyExtractor(e1), keyExtractor(e2))

  // 雑なデフォルト比較関数
  static #defaultComparator = (v1, v2) => {
    if (v1 > v2) return 1
    if (v1 < v2) return -1
    return 0
  }

  // 比較関数の判定結果を逆転させるデコレーター
  static #reversedDecorator = comparator => (e1, e2) => -1 * comparator(e1, e2)

  // ひとつ前の比較関数で大小関係が確定しなかった場合に実行する比較関数を登録する
  thenComparing (keyExtractor, comparator = ComparatorUtils.#defaultComparator) {
    this.comparators.push(ComparatorUtils.keyExtractorComparator(keyExtractor, comparator))
    return this
  }

  // スタック最後の比較関数の判定結果を逆転させる
  reversed () {
    this.comparators.push(ComparatorUtils.#reversedDecorator(this.comparators.pop()))
    return this
  }

  // これも関数を返す関数(高階関数)の例です
  // 複数条件を組み合わせるような比較関数も高階関数で作成することができます

  // 最初の比較関数で大小関係が決定できなかった場合、次の比較関数を呼び出す比較関数を作成
  // reduce便利!
  comparator = () =>
    this.comparators.reduce((first, next) => (e1, e2) => first(e1, e2) || next(e1, e2))
}
アプリケーション部分
// personComparatorはクロージャーとなっており、thenComparing関数で登録された比較関数を保持している
// このサンプルの場合、姓(昇順) -> 名(昇順) -> 年齢(降順)の順にソートできる
const personComparator = ComparatorUtils.comparing(person => person.lastname)
                           .thenComparing(person => person.firstname)
                           .thenComparing(person => person.age).reversed() // 年齢比較結果を逆転させる
                           .comparator()

// 後の使い方は今まで通り
const persons = [
  { lastname: 'bbb', firstname: 'ttt', age: 10},
  { lastname: 'bbb', firstname: 'ttt', age: 20},
  { lastname: 'ccc', firstname: 'sss', age: 30},
  { lastname: 'bbb', firstname: 'sss', age: 40},
  { lastname: 'aaa', firstname: 'uuu', age: 50},
]
persons.sort(personComparator)

// ソート結果
// [
//   { lastname: 'aaa', firstname: 'uuu', age: 50 }, // 姓の昇順
//   { lastname: 'bbb', firstname: 'sss', age: 40 }, // 同姓の場合、名の昇順
//   { lastname: 'bbb', firstname: 'ttt', age: 20 }, // 同姓同名の場合、年齢の降順
//   { lastname: 'bbb', firstname: 'ttt', age: 10 },
//   { lastname: 'ccc', firstname: 'sss', age: 30 } 
// ]

 今回は共通関数部分もコードを掲載したので分量が多いですが、アプリケーション部分は確かにすっきり書けていると思います。
 姓と名くらいなら一つの比較関数でべたに書いた方が実装量は減りますが、上記のような仕組みだけ作ってしまえばあとは自由にカスタマイズできますね。
 なおJavaならAPIとしてこのような仕組みは組み込まれています。ECMAScriptだと外部のライブラリーを探せば同様のものはあるのではないでしょうか。

カリー化?

 関数を返す関数(高階関数)を使うとカリー化することができます。
 Wikipediaによるとカリー化とは次の通りです。

複数の引数をとる関数を、引数が「もとの関数の最初の引数」で戻り値が「もとの関数の残りの引数を取り結果を返す関数」であるような関数にすること(あるいはその関数のこと)である。

 先述の例でHTML内のタグをカウントする例を挙げました。HTMLダウンロードのコストを無視すれば、次のような関数定義でも問題ありません(この場合、高階関数化はしない)。

カリー化前
const tagCounter = (url, tagNmae) => { /* ダウンロードとタグのカウント処理 */ }

const yNews = 'https://どこかのニュースサイト'
tagCounter(yNews, 'div')
tagCounter(yNews, 'span')
tagCounter(yNews, 'li')

 これを次のように書けるようにするのがカリー化です。ここで”関数を返す関数”が使われています。

カリー化
const curriedTagCounter = url => tagName => { /* ダウンロードとタグのカウント処理 */ }
// 分かり易くするため、少しだけ変形
//const curriedTagCounter = url => {
//  return tagName => { /* ダウンロードとタグのカウント処理 */ }
//}

// curriedTagCounter(yNews)が関数を返すので、それに引数を与えて実行している
const yNews = 'https://どこかのニュースサイト'
curriedTagCounter(yNews)('div')
curriedTagCounter(yNews)('span')
curriedTagCounter(yNews)('li')

 カリー化した上記サンプルは配列のmap関数などが簡単に使えるようになります。

const tags = ['div', 'span', 'li']

// カリー化したタグをカウントする関数(map関数が使える)
// curriedTagCounterの戻り値となる関数自体は、単一の文字列引数を受け取り、単一の数値を返すだけでよい
const result1 = tags.map(curriedTagCounter(yNews))

// curriedTagCounter(yNews)に関しては次のようにすることもできる
// 高階関数の実装次第では、result1と結果は異なる
const yNewsCurriedTagCounter = curriedTagCounter(yNews)
const result2 = tags.map(yNewsCurriedTagCounter)

// 参考

// タグの配列を引数に受け取ってタグをカウントする関数
// arrayTagCounter関数内で引数の配列に対する繰り返し処理や、結果を配列にpushするなどの処理が必要になる
const result3 = arrayTagCounter(yNews, tags)

// カリー化しなくてもmap関数は使えるが……部分適用した関数を作る必要がある
// いわゆる部分適用を行う
const yNewsTagCounter = tagName => {
  const yNews = 'https://どこかのニュースサイト'
  return tagCounter(yNews, tagName)
}
const result4 = tags.map(yNewsTagCounter)

 サンプルのcurriedTagCounter関数のようにカリー化する場合、最後の引数部分を高階関数側で設定してもらうようにすると、map関数やfilter関数などの高階関数に適用できて便利ですね。

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

Expo SDK37のBare WorkflowでNotificationsを使ってみる

はじめに

4月1日にExpoのSDK37がリリースされました。詳しい変更内容は下記のURLから見ることができます。

https://dev.to/expo/expo-sdk-37-is-now-available-69g
https://github.com/expo/expo/blob/master/CHANGELOG.md#3700

大きな変化として、まずはEject後の選択肢がExpoKitからBare Workflowへ完全に移行した点が挙げられます。
そして単にリプレイスされただけという訳ではなく、これまでManaged Workflowのみ対応していたNotifications APIがBare Workflowでも使用できるようになり、APIの内容も変化しています。

この記事ではSDK37のNotifications APIをBare Workflowで使用するための導入方法や検証結果をまとめてみます。
ちなみに途中でエラー解決のためにソースをいじっていますが、私自身がAPIへの理解を深めることを目的とした其の場凌ぎの感があるので、あくまでそれら全てをお勧めしているわけではありません。

プロジェクトを作成

既存のプロジェクトをアップデートするのではなく、プロジェクトを作成するところからやってみます。
まずは(していなければ)expo-cliをアップデート。

$ npm update -g expo-cli

expo initしてBare workflowのテンプレートからプロジェクトを作成します。

$ expo init BareNotification
? Choose a template:
❯ minimal               bare and minimal, just the essentials to get you started

作成されたプロジェクトディレクトリを開き、package.jsonを見ると見慣れないexpo-updatesというパッケージがデフォルトでインストールされているのがわかります。後述しますがこのパッケージは今のところリリースビルドする時にエラーの原因になってしまいます。

バンドル/パッケージ名に注意

ここで注意したいのが、Expoの以前のバージョンでもそうですが、最初からBare Workflowのテンプレートでプロジェクトを作成した場合はiOSのbundleIdentifierやAndroidのパッケージ名がプロジェクト名を元に勝手に命名されてしまうという点です。変更したい場合は一括置換、およびAndroidのjavaディレクトリの修正などを適切に行ってください。
(なので、実際の開発ではManaged Workflowでプロジェクトを作成しapp.jsonのバンドル・パッケージ名、アイコン、スプラッシュ等を編集してからEjectというのがスマートです。)

パッケージをインストール

画面の実装の方は後述するとして設定を先に行いますが、最初に使用するパッケージをインストールしておきます。

$ npm install expo-notifications expo-web-browser \
@react-navigation/native @react-navigation/stack \
react-native-reanimated react-native-gesture-handler \
react-native-screens react-native-safe-area-context \
@react-native-community/masked-view

多く見えますが、react-navigationの依存パッケージが結構あります。

Notificationsの設定

公式のREADMEに沿って設定を行います。所々リンクが通っていないので、察しながらやっていきます。
https://github.com/expo/expo/tree/master/packages/expo-notifications#installation-in-bare-react-native-projects

iOSの設定

ライブラリをインストール

通常のプロセスですが、上記のnpmパッケージをインストールした上で、CocoaPodsでライブラリをインストールします。

ライブラリをインストール
$ cd ios
$ pod install
$ cd ..

Capabilityを追加

Xcodeでプロジェクト(.xcworkspace)を開き、Signing & CapabilitiesタブでPush NotificationsのCapabilityを追加します。
BareNotifications_xcodeproj.png

Team、Bundle IdentifierなどのSigningの設定も適宜行ってください。

プロジェクトにAPNSキーを紐付ける

Managed WorkflowではExpoが勝手にやってくれていた部分を手動で行います。(といっても、これもコマンドを叩けばいいだけです。)

$ expo credentials:manager
? What do you want to do?
  ---- Current project actions ----
❯ Use existing Push Notifications Key in current project
  Use existing Distribution Certificate in current project
  ---- Account level actions ----
  Remove Provisioning Profile
  Add new Push Notifications Key
  Remove Push Notification credentials
  Update Push Notifications Key
  Add new Distribution Certificate
  Remove Distribution Certificate
  Update Distribution Certificate

色々と情報が出たあとに何をしたいか聞かれるので、Expoを使ってすでにPush Notifications Keyが生成されている場合はUse existing Push Notifications Key in current projectを選択し、プロジェクトにキーを紐付けます。
Push Notifications Keyはデベロッパーごとに2つまでと決まっているので基本的にはプロジェクトを跨って使い回すことになります。まだ作成していない場合はAdd new Push Notifications Keyを選択したうえで生成されたキーをプロジェクトに紐付けてください。

Androidの設定

AndroidではFCM(Firebase Cloud Messaging)を使った実装になっているため、Firebaseプロジェクトが必要になります。ここでは、すでにFirebaseプロジェクトを作成しているという前提で進めます。

AndroidプロジェクトとFirebaseプロジェクトの紐付け

設定項目は具体的にはFirebase Android 構成ファイル(google-services.json)の設置とGradleファイルの編集などですが、アップデートが速いので、下記の公式のドキュメントに沿って行ってください。

Android プロジェクトに Firebase を追加する
https://firebase.google.com/docs/android/setup?hl=ja

FCMサーバーキーのアップロード

Expoから通知を送るためには、Expo側にFCMサーバーキーをアップロードする必要があります。
Firebase Consoleの設定 > 「クラウド メッセージング」タブ内のサーバーキー(下画像)をコピーし、
Test_Project_–_Firebase_console.png
下記のコマンドでこれをアップロードします。

$ expo push:android:upload --api-key <サーバーキー>

[参考]Uploading Server Credentials
https://docs.expo.io/versions/latest/guides/using-fcm/#uploading-server-credentials

画面の作成

さて、設定が完了したので確認のための画面を作りますが、ここでは以前下記の記事で作成した画面遷移のパターンを試してみることにします。
https://qiita.com/mildsummer/items/6e13800c04a91dd3c2d6
react-navigationがv4系からv5系で割と大きく変化しているようなので、それも修正します。ついでにHooksに書き直した結果、最終的なソースがこちらです。

App.js
import "react-native-gesture-handler";
import React, { useEffect, useState, createRef } from "react";
import { Text, View, Alert, Platform } from "react-native";
import { NavigationContainer, StackActions } from "@react-navigation/native";
import { createStackNavigator } from "@react-navigation/stack";
import * as Notifications from "expo-notifications";
import * as WebBrowser from "expo-web-browser";

function Top() {
  const [token, setToken] = useState(null);

  useEffect(() => {
    init().catch(console.error);
  }, []);

  async function init() {
    const settings = await Notifications.requestPermissionsAsync();
    if (settings.granted || settings.status === 1) {
      const { data: token } = await Notifications.getExpoPushTokenAsync({
        experienceId: "@<ユーザー名>/<プロジェクト名>"
      });
      console.log("[EXPO PUSH TOKEN]", token);
      console.log(
        `Try running the command "node pushNotification.js ${
          token.match(/\[(.+)]/)[1]
          } [SCREEN NAME(A|B|C) or URL] [ID]"`
      );
      console.log(
        `ex) $ node pushNotification.js ${token.match(/\[(.+)]/)[1]} A 123`
      );
      setToken(token);
    } else {
      console.log("permission denied");
    }
  }

  return (
    <View
      style={{
        flex: 1,
        width: "100%",
        justifyContent: "center",
        alignItems: "center"
      }}
    >
      <Text>{token || "getting token..."}</Text>
      <Text>Please look your console</Text>
    </View>
  );
}

const Stack = createStackNavigator();
const navigationRef = createRef();
const stackScreens = [
  { name: "A", color: "#6200EE" },
  { name: "B", color: "#03DAC6" },
  { name: "C", color: "#B00020" }
];
let currentNavigationState = null;

export default function App() {
  let subscription = null;

  useEffect(() => {
    Notifications.setNotificationHandler({
      handleNotification: async (notification) => {
        // console.log(notification);
        return {
          shouldShowAlert: true,
          shouldPlaySound: true,
          shouldSetBadge: true,
        };
      }
    });
    Notifications.addNotificationResponseReceivedListener(response => {
      console.log("response", response);
      respond(response.notification);
      Alert.alert("通知が届きました", JSON.stringify(response.notification.request.content.data));
    });
    subscription = Notifications.addNotificationReceivedListener((notification) => {
      console.log("received notification");
      console.log(notification);
      Alert.alert("通知が届きました", JSON.stringify(notification.request.content.data), [
        {
          text: "キャンセル",
          style: "cancel"
        },
        {
          text: "移動する",
          onPress: () => {
            respond(notification);
          }
        }
      ]);
    });
  }, []);

  function respond(notification) {
    const data = Platform.OS === 'ios' ? notification.request.content.data?.body : notification.request.content.data;
    if (data && data.url) {
      WebBrowser.openBrowserAsync(data.url).catch(console.error);
    } else if (data && navigationRef.current) {
      const routeName = data.routeName || "top";
      const params = data.params;
      if (currentNavigationState && currentNavigationState.routes.length > 1) {
        // 同階層の場合はスタック追加せず差し替える
        navigationRef.current.dispatch(StackActions.replace(routeName, params));
      } else {
        navigationRef.current.navigate(routeName, params);
      }
    }
  }

  return (
    <NavigationContainer
      ref={navigationRef}
      onStateChange={(currentState) => {
        currentNavigationState = currentState;
      }}
    >
      <Stack.Navigator initialRouteName="top">
        <Stack.Screen name="top" options={{ headerShown: false }} component={Top} />
        {stackScreens.map((screen) => (
          <Stack.Screen
            key={screen.name}
            name={screen.name}
            options={({ route }) => {
              headerTitle: `${screen.name} ID:${route.params.id}`
            }}
          >
            {({ route }) => (
              <View
                style={{
                  flex: 1,
                  width: "100%",
                  justifyContent: "center",
                  alignItems: "center",
                  backgroundColor: screen.color
                }}
              >
                <Text style={{ color: "#FFF", fontSize: 16 }}>screen name</Text>
                <Text style={{ color: "#FFF", fontSize: 24 }}>{screen.name}</Text>
                <Text style={{ color: "#FFF", fontSize: 16, marginTop: 24 }}>ID</Text>
                <Text style={{ color: "#FFF", fontSize: 24 }}>
                  {route.params.id || "-"}
                </Text>
              </View>
            )}
          </Stack.Screen>
        ))}
      </Stack.Navigator>
    </NavigationContainer>
  );
}

挙動は変わりませんが、v5ではReactの要素として(JSXの中に)スクリーンを定義するようになっています。
expoパッケージ内のNotificationsとexpo-notificationsでAPIが結構変わっているのがわかるでしょうか。詳しい内容はREADMEを参照してください。
また、後述する検証の結果、iOSとAndroidでnotificationオブジェクト内のdataの入り方が違っていたので、Platform.OSを見て処理を分けています。
getExpoPushTokenAsyncしている箇所では、オプションのexperienceId@<ユーザー名>/<プロジェクト名>の形式で入れてください。プロジェクト名は正確にはapp.json内のexpo.slugの値(のはず)です。

ExpoのpushAPIを叩く処理

上述した以前の記事と同じように、便宜的にAPIを叩く処理を用意しておきます。

pushNotification.js
const request = require("request");

const [, , token, routeName, id] = process.argv;

const isWebView = /http/.test(routeName);

request(
  {
    url: "https://expo.io/--/api/v2/push/send",
    method: "POST",
    json: {
      to: `ExponentPushToken[${token}]`,
      title: "通知サンプル",
      body: "ここに説明文が入ります",
      data: isWebView
        ? {
            url: routeName,
            params: { id }
          }
        : {
            routeName,
            params: { id }
          },
      _displayInForeground: true
    }
  },
  function(error, response, body) {
    if (error) {
      console.log(error);
    } else {
      console.log(body);
    }
  }
);

実機で検証してみる

Expo Client

READMEには、Expo Clientではまだ使用できないと書いてあります。
https://github.com/expo/expo/tree/master/packages/expo-notifications#installation-in-managed-expo-projects

実際に確認すると、expo-notificationsをインポートしている時点でNative module cannot be null.となり、使用できません。
PNGイメージ 48.png

Xcode + iPhoneでデバッグビルドを確認

iOSアプリをXcodeで動かしてみます。端末を繋ぎ、Product→Runします。
トークンが表示され、
PNGイメージ_48.png

先ほどのスクリプトで通知を送ってみます。

$ node pushNotification.js <トークン文字列部分> A 123

PNGイメージ_50.png
PNGイメージ 51.png

アプリのforeground/bacground状態共に問題なく通知が届き、通知に含まれるデータを取得することでページ遷移の実装も正常に動いています。

私の場合は最初は上手くいかなかったのですが、上述したexpo credentials:managerコマンドによって既存のPush Notifications Keyを削除し、新しく生成しなおすと成功しました。

Androidでデバッグビルドを確認

USBデバッグが有効になっている端末をつなぎadb devicesでデバイスが検知できているのを確認したうえで、react-nativeコマンドを叩いて起動します。

$ npx react-native run-android

PNGイメージ_50.png
FirebaseとFCMとの連携が上手くいっていれば、Androidでも問題なくトークンが表示されます。

しかし実際に通知を送ってみると、アプリがクラッシュしてしまいました。
adb logcatで探ってみると、このようなエラーが発生しています。

java.lang.UnsupportedOperationException: Could not put a value of class android.os.Bundle to bundle.

どうやらこのエラーは、通知のパラメータにtitlebodyだけでなくdataを入れると発生することがわかりました。
環境依存などではなく純粋にネイティブモジュールのバグのようなので、モジュール自体に手を入れる以外解決方法はありません。
余裕があればPRを送るとして、一時的な解決方法としてnode_modules/expo-notificationsの中を編集してみます。

dataエラー(Could not put a value of class android.os.Bundle to bundle.)回避

dataパラメータを指定したい場合、まずはunimodules内のMapArgumentsを新しいファイルで置き換えます。

node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/MapArguments.java
package expo.modules.notifications.notifications;

import android.os.Bundle;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.unimodules.core.arguments.ReadableArguments;

public class MapArguments implements ReadableArguments {
  private Map<String, Object> mMap;

  public MapArguments() {
    mMap = new HashMap<>();
  }

  public MapArguments(Map<String, Object> map) {
    mMap = map;
  }

  @Override
  public Collection<String> keys() {
    return mMap.keySet();
  }

  @Override
  public boolean containsKey(String key) {
    return mMap.containsKey(key);
  }

  @Override
  public Object get(String key) {
    return mMap.get(key);
  }

  @Override
  public boolean getBoolean(String key) {
    return getBoolean(key, false);
  }

  @Override
  public boolean getBoolean(String key, boolean defaultValue) {
    Object value = mMap.get(key);
    if (value instanceof Boolean) {
      return (Boolean) value;
    }
    return defaultValue;
  }

  @Override
  public double getDouble(String key) {
    return getDouble(key, 0);
  }

  @Override
  public double getDouble(String key, double defaultValue) {
    Object value = mMap.get(key);
    if (value instanceof Number) {
      return ((Number) value).doubleValue();
    }
    return defaultValue;
  }

  @Override
  public int getInt(String key) {
    return getInt(key, 0);
  }

  @Override
  public int getInt(String key, int defaultValue) {
    Object value = mMap.get(key);
    if (value instanceof Number) {
      return ((Number) value).intValue();
    }
    return defaultValue;
  }

  @Override
  public String getString(String key) {
    return getString(key, null);
  }

  @Override
  public String getString(String key, String defaultValue) {
    Object value = mMap.get(key);
    if (value instanceof String) {
      return (String) value;
    }
    return defaultValue;
  }

  @Override
  public List getList(String key) {
    return getList(key, null);
  }

  @Override
  public List getList(String key, List defaultValue) {
    Object value = mMap.get(key);
    if (value instanceof List) {
      return (List) value;
    }
    return defaultValue;
  }

  @Override
  public Map getMap(String key) {
    return getMap(key, null);
  }

  @Override
  public Map getMap(String key, Map defaultValue) {
    Object value = mMap.get(key);
    if (value instanceof Map) {
      return (Map) value;
    }
    return defaultValue;
  }

  @Override
  public boolean isEmpty() {
    return mMap.isEmpty();
  }

  @Override
  public int size() {
    return mMap.size();
  }

  @Override
  public ReadableArguments getArguments(String key) {
    Map value = getMap(key);
    if (value != null) {
      return new MapArguments(value);
    }
    return null;
  }

  @Override
  public Bundle toBundle() {
    Bundle bundle = new Bundle();
    for (String key : mMap.keySet()) {
      Object value = mMap.get(key);
      if (value instanceof String) {
        bundle.putString(key, (String) value);
      } else if (value instanceof Integer) {
        bundle.putInt(key, (Integer) value);
      } else if (value instanceof Double) {
        bundle.putDouble(key, (Double) value);
      } else if (value instanceof Long) {
        bundle.putLong(key, (Long) value);
      } else if (value instanceof Boolean) {
        bundle.putBoolean(key, (Boolean) value);
      } else if (value instanceof ArrayList) {
        bundle.putParcelableArrayList(key, (ArrayList) value);
      } else if (value instanceof Map) {
        bundle.putBundle(key, new MapArguments((Map) value).toBundle());
      } else if (value instanceof Bundle) { // Bundleクラスインスタンスをセットする場合を追加
        bundle.putBundle(key, (Bundle) value);
      } else {
        throw new UnsupportedOperationException("Could not put a value of " + value.getClass() + " to bundle.");
      }
    }
    return bundle;
  }
}

続いてnode_modules/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/NotificationSerializer.javaを編集します。

node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/NotificationSerializer.java
package expo.modules.notifications.notifications;

import android.os.Bundle;
import android.util.Log;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
- import org.unimodules.core.arguments.MapArguments; // この行を削除

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import androidx.annotation.Nullable;
+ import expo.modules.notifications.notifications.MapArguments; // この行を追加
import expo.modules.notifications.notifications.interfaces.NotificationTrigger;
import expo.modules.notifications.notifications.model.Notification;
import expo.modules.notifications.notifications.model.NotificationContent;
import expo.modules.notifications.notifications.model.NotificationRequest;
import expo.modules.notifications.notifications.model.NotificationResponse;
import expo.modules.notifications.notifications.model.triggers.FirebaseNotificationTrigger;
import expo.modules.notifications.notifications.triggers.DateTrigger;
import expo.modules.notifications.notifications.triggers.TimeIntervalTrigger;

public class NotificationSerializer {
  // 省略
}

こちらはimportしているモジュールを置き換えているだけです。

再度確認してみる

ネイティブモジュールを修正した後、再度確認してみると通知を処理できるようになりました。
この際、リスナー関数の引数になるNotificationオブジェクトの構造がiOSと違ったので、上述したように処理を分けています。
Screenshot_20200403-165400
Screenshot_20200403-165400

Androidにおいてはアプリが終了している状態で届いた通知をタップしたときに、リスナー関数が呼ばれないなどiOSと少し挙動が異なります。これについては別記事で改めて詳しく調べようと思います。

Xcode + TestFlight + iPhoneでリリースビルドを確認

プロジェクトをArchiveしApp Storeにアップロード、TestFlightで確認というプロセスでiOSのリリースビルドを確認します。
この時点でまずはApp Store Connectで同じバンドル名の新規Appを追加しておく必要があります。

さて、このまま普通にProduct→Archiveしてすんなりビルドできればいいのですが、
もしnvmでNode.jsのバージョン管理をしている場合は最後の方でこのようなエラーが発生してしまいます。

No specify using Node.js version.
Command PhaseScriptExecution failed with a nonzero exit code

これ自体はTARGET設定のBuild Phasesの中にあるBundle Expo Assetsというフェーズの行頭に、nvmへのPATHを通している.bashrcなり.bash_profileなりへの参照を追加すれば解決します。
BareNotifications_xcodeproj.png

しかしこれで一旦はビルドが通りTestFlightで確認しようと思っても、アプリを起動した瞬間にクラッシュしてしまうと思います。
Expo SDK37以降、Bare Workflowではデフォルトでexpo-updatesパッケージがインストールされており(これはインストールの時にも書きましたが)、この設定を正しく行わないと起動時にエラーが発生してしまうからです。

expo-updates自体はBare WorkflowでOTAアップデートができるようになるありがたいパッケージです。ただ、今現在expo-updatesREADMEを見ても設定方法がいまいちよくわからない(特に、app.manifestの書き方など)のと、今回はNotificationsの検証がしたいだけなので、expo-updatesを使用している部分を削除することで検証を進めたいと思います。

expo-updates使用部分を削除

npmパッケージやPodsをアンインストールするかどうかはお任せするとして、コード内のexpo-updates使用部分を削除します。基本的にはREADMEに書いてあるInstallationの逆のことをすればよいはずです。

AppDelegate.mを修正

expo-updatesが導入される前の状態に戻します。
Githubでみると2019年4月のこの辺りです(わりと最近まで変化無いようです)。コピペして、モジュール名がHelloWorldになっている部分をmainに直します。
https://github.com/expo/expo/blob/c9802eba34e4b88335aac24e76f8e24d990d2bd1/templates/expo-template-bare-minimum/ios/HelloWorld/AppDelegate.m

AppDelegate.hを修正

こちらも同様。
https://github.com/expo/expo/blob/c9802eba34e4b88335aac24e76f8e24d990d2bd1/templates/expo-template-bare-minimum/ios/HelloWorld/AppDelegate.h

Build Phasesを修正

Bundle Expo Assetsのコードを置き換えます。

export NODE_BINARY=node
../node_modules/react-native/scripts/react-native-xcode.sh

BareNotifications_xcodeproj.png

これでコード内にあるexpo-updatesの使用箇所がなくなりました。
その他、Expo.plistapp.manifestなどは不要になるので削除して大丈夫です。

再度確認

Archive、アップロードが完了し、TestFlightで確認してみると、無事通知を送ることができました。挙動はデバッグビルドと変わりありません。
PNGイメージ_52.png

Androidでリリースビルドを確認

Androidの場合も同じく、そのままリリースビルドするとクラッシュしてしまうので、expo-updates部分を削除します。
こちらもConfigure for Androidの逆をすればいいんですが、iOSと違って行の削除・フラグの変更だけなので簡単だと思います。

編集するファイルはapp/build.gradleMainApplication.javaAndroidManifest.xmlです。(MainApplication.javaの一部をコメントアウトするだけでも大丈夫です。)

Androidのリリースビルドの方法は人によって色々ありそうなので割愛します。
私の場合はAndroid Studioではnvmの関係でビルドできなかったりしたので、署名を含めてコマンドラインでやっています。

Androidでもリリースビルドの挙動が確認できました。
PNGイメージ_52.png

まとめ

  • expoパッケージ内のNotificationsAPIと今回新たに追加されたexpo-notificationsでは実装方法が異なる
  • AndroidではFCMを使用するのでFirebaseプロジェクトが必須
  • Expo Clientでは使用できない
  • iOSのデバッグビルドは正常に動作
  • Androidではネイティブモジュールの不具合でdataパラメータを使用できない
    • モジュールを上書きすることで無理やり対処することは可能
  • iOS、Android共にデフォルトでインストールされているexpo-updatesの影響でそのままではリリースビルドがクラッシュする。
    • expo-updatesの設定が必要だが、よくわからない場合はこれを使用しないという選択肢もある
  • Androidではそのほか、background/foregroundの挙動やNotification Iconの設定方法などよくわからない部分が残っている
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ファルコン・パンチの表示順をランダムにする

作成にあたって

友人がpythonを用いてターミナル上動作するこれを作っていたので、僕はhtmlとjs使って作成してみました。
動けばいいだけだったので1つのファイルにまとめて書いています。

コード

index.html
<!doctype html>
<html>
<head>
<title>ファルコン・パンチ</title>

<script type="text/javascript">
    function exchangeText() {
        var text = document.forms.id_form.id_textBox.value.split("");
        target = document.getElementById("output");
        if(exchange(text)){
            target.innerText = exchange(text);
        }else{
            target.innerText = "";
        }
    }
    // 文字の順番を入れ替える
    function exchange(text) {
        for(i=0; i<text.length; i++){
            var rand = Math.floor(Math.random()*text.length);
            var tmp = text[i];
            text[i] = text[rand];
            text[rand] = tmp;
        }
        var output;
        for(i=0; i<text.length; i++){
            if(output){
                output = output + text[i];
            }else{
                output = text[i];
            }
        }
        return output;
    }
</script>
</head>

<body>
    <p><b>ファルコンのうんちぶり</b></p>
    <form name="form" id="id_form" action="">
        <input name="textBox" id="id_textBox" type="text" value="ファルコン・パンチ" />
        <input type="button" value="入れ替え" onclick="exchangeText()" />
    </form>
    <div id="output"></div>

</body>
</html>

おわりに

実務経験が少ないため、どうかけば可読性が向上するのか等コメント頂ければ幸いです。

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

Yarn workspaces から Lerna に移行した

Yarn workspaces から Lerna に移行した時の知見です。
やや書きかけ項目です。

前提

とあるサービスを提供しており、モノレポで運用している。使用スタックは主に API の Ruby on Rails で GraphQL の API サーバーを構築し、そのフロントエンドを React/ReactNative で作成している。

もともとは別々のリポジトリだったが、提供サービスが増えてくるに従い、下記の問題点が出てきた。

  • CI/CD パイプラインを一々作成する必要があり、反映にも手間がかかる
  • GraphQL の型定義やユーティリティ、細かいところでは Git の Hook など、再利用したいものがある
  • GitHub への招待などが手間になる

そこで、この記事を書く半年前くらいにリポジトリを統合し、モノレポになった。

Rails と それ以外 (TypeScript) でフォルダを分割し、 TypeScript 側は手軽にモノレポをマネジメントできそうな Yarn workspaces を選択した。その時はこれで苦しむことを知らない。

当初のディレクトリ構成は、下記。

.
|-- api/                    # Rails API
|-- docker-compose.yml
|-- package.json            # 開発用のパッケージ (prettier など)
|-- packages                # TypeScript clients
|-- packages/
|    |-- app                # アプリ (React Native)
|    |    |-- src/
|    |    `-- package.json
|    |-- console            # 管理画面 (React Native Web)
|    `-- components         # 共有コンポーネント
|-- prettier.config.js
|-- private/                 # AWS のトークンなど、秘匿ファイル。git-crypt で暗号化
|-- tsconfig.json           # 使いまわしている
|-- tslint.json
`-- yarn.lock

まとめると、

  • サーバーサイドは主に Ruby on Rails
  • iOS/Android アプリ 及びそれに付随する Web サービス(管理画面など)を提供している
    • iOS/Android アプリは React Native を使用
    • Web サービスには React Native Web を使用
    • React Native で作成されたコンポーネントを、一部 Web でも使用している

が、上記の構成では限界を感じたため、 Lerna に移行することにした。

Lerna 移行の動機

React Native で Yarn workspaces は、 Yarn の hoist (巻き上げ)機能により、非常に使いにくいことが分かってきた。

致命的なのは、 react-native-cli は、 React Native における index.jsAppRegistry.registerComponent する場所) の 相対パス./node_modules/react-native/cli.js を見に行くため、巻き上げた先のパスを設定する必要がある。

巻き上げられると、 Root の node_modules を見に行くため、ビルド時に ../../node_modules/react-native/cli.js に設定する必要がある。つまり、 Xcode をいじる必要が出てくる。

また、 Root から実行する際は下記のスクリプトが必要になってくる。

// @see https://github.com/facebook/react-native/issues/25822#issuecomment-531009417

process.chdir('./packages/nupp1-fit')

var cli = require('@react-native-community/cli')
cli.run()

ここまでは、まだ設定すれば問題無いが、 場合によって 巻き上げられないパッケージがあるので、他のパッケージを更新した際に、 React Native がビルドされるかを確認する必要がある。

特に React Native 0.60.0 からの autolink 機能は、 Cocoapod でインストールした時に、暗黙的に ./node_modules 以下を探索するので、../../node_modules に巻き上げられると使えなくなってしまう。

では Yarn の nohoist 機能を使えばよいではないか、という意見がありそうだが、それもうまくいくとは限らない。調べた限りでは、下記のような動作をする。

  • nohoist 機能は、動作しないことがある。
  • 何が巻き上げられるかが不明瞭。多分ロジックを追えば分かるが、各ライブラリの依存関係を調べる必要があり、果てしない。
  • 同一のパッケージ/バージョンがあると巻き上げられるらしい。
    • バージョンが異なる場合、各パッケージの node_modules に保存される。
  • キャッシュが効かないことがある → CIの低速化
  • Symbolic link (.bin/**) が壊れることがある。 (semver など)
    • 多分、多数のパッケージが依存しているパッケージで、バージョンによって bin が違う場合、発生する。
  • 各パッケージでインストールした後、 Root でインストールすると、依存関係を変更してないのに、再度インストールが走る。

この問題を調査していたところ、 Lerna はデフォルトで巻き上げをしない(正確にはするが、 Root の package.json にある場合のみ)ので、これを使って解決できそうな気がした。

実作業・困ったこと

基本的には、こちらのリポジトリを参考にしながら、作業を進めていった。

https://github.com/erickjth/react-native-web-monorepo-lerna

コマンドが複雑化する

Lerna を入れると、 yarn add 等は 一切できなくなる

また cross-env や webpack-dev-server など、開発時のみ必要になる一部の devDependencies は Root の package.json のみに記述していたので(これは要改善)、各パッケージ配下では実行できない。

毎回 yarn lerna exec --ignore @my/types --scope @my/package tsc などするのも面倒だったので、 Makefile を作成し、その中に npm scripts に相当するスクリプトを記述することにした。

抜粋するとこんな感じ。

SHELL := /bin/bash
LERNA_OPTION := --stream --parallel --no-bail --ignore @my/types --ignore @my/js-project
NODE_BIN := ./node_modules/.bin

export PATH := $(NODE_BIN):$(PATH)

tsc:                                           # Execute `tsc` in each scripts
    lerna exec $(LERNA_OPTION) tsc

参考: https://qiita.com/Hoishin/items/0e9b4ebee45e3f8cdc29

React Native CLI

React Natvie CLI に Lerna が生成する Symbolic Link を追わせるためには、 metro.config.js を少々いじる必要がある。
React Native 0.62.0-RC3 で利用するときは、下記のような設定が必要になる。

resolver.watchFoldersextraNodeModules の設定がポイント。

参考

const path = require('path')
const { getDefaultConfig } = require('metro-config')

function getProjectModuleDir(m) {
  return path.resolve(__dirname, `node_modules/${m}`)
}

// To allow importing peerDependency from other packages
const modulesResolvedInProject = [
  '@babel/runtime',
  '@react-native-firebase/app',
  '@react-native-firebase/analytics',
  '@react-native-community/push-notification-ios',
  '@react-native-community/async-storage',
  'react',
  'react-is',
  'react-native',
  'react-native-appsflyer',
  'react-native-device-info',
  'react-native-picker',
  'react-native-keyboard-aware-scroll-view',
  'react-native-google-places-autocomplete',
  'react-native-keyboard-spacer',
  'react-native-linear-gradient',
  'react-native-paper',
  'react-native-svg',
  'react-native-vector-icons',
  'react-redux',
  'react-native-repro',
]

const extraNodeModules = modulesResolvedInProject.reduce((acc, m) => {
  return { ...acc, [m]: path.resolve(__dirname, `node_modules/${m}`) }
}, {})

module.exports = (async () => {
  const {
    resolver: { sourceExts, assetExts },
  } = await getDefaultConfig()
  return {
    transformer: {
      babelTransformerPath: require.resolve('react-native-svg-transformer'),
    },
    watchFolders: [
      // To allow finding files outside mobile
      path.resolve(__dirname, '..'),
    ],
    resolver: {
      assetExts: assetExts.filter(ext => ext !== 'svg'),
      sourceExts: [...sourceExts, 'svg'],
      extraNodeModules,
    },
    maxWorkers: 2,
  }
})()

総評

良かったこと

  • きちんと依存関係を各リポジトリの package.json に記述しないと、 Symlink が設定されない。
    • Yarn workspaces は何となくでも簡単に動いてしまったので、これは逆に良かった。
  • Lerna bootstrap の方が yarn install より速い(体感)
  • 今後、 Docker Image の作成をする時に、苦労しなそう
    • 依存関係は、各パッケージでちゃんとインストールしないといけないため
  • 各パッケージ以下でコマンドを並列実行できるようになった
    • それ以前は、 wsrun というものを使っていた

手間取ったこと

  • セットアップ手順が複雑化。 yarn install && yarn lerna bootstrap && yarn postinstall
    • 上記の Makefile で記述したため、解決
  • CI の書き換えなど
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Nuxt.js】firebase導入編(RDB版):データの追加 取得をしよう

前置き

便利なfirebase!
シンプルに導入の仕方を解説していきます?
簡単なデータの書き込みと取得をしてみます?

❓そもそもfirebaseとは
簡単に言うと
オンラインにデータを保存できて
取得もできる優れものです!
自分で1から作るとなると大変ですが
firebaseを使えば簡単ですね?
https://firebase.google.com/docs/database

❓どんな時に使うか
例えば、チャットアプリ!
リアルタイムにデータを保存・同期できるので
メッセージを送り合うことが簡単になります?

例えば、フリマサイト!??
会員情報とその会員が出品した商品が
DBに保存されていきます。
それを会員はいつでも編集でき、
反映したらすぐ表示が変わるわけです?
会員自身でデータが作れて編集もできる!
といった感じです?

❓Referenceの読み方
基本的にfirebaseのreferenceは
英語の状態で表示し、
自分でgoogle翻訳で翻訳しましょう??

言語を日本語にすると
古いバージョンだったりするので、
最新の英語を翻訳していくのがベスト!⭕️
ただいきなり全部英語だと
欲しい情報がどこにあるか分からないので
最初は日本語で表示させて
ある程度検討をつけてから英語にしてます?
(英語も理解できるように頑張ろう…?)

DBの種類と違い

firebaseのDatabaseは2種類あります。
・Realtime Database
・Cloud Firestore
  (Realtime Databaseの拡張版?)
動作や料金が変わってきます?
基本は無料で使えます!?
https://firebase.google.com/docs/database/rtdb-vs-firestore

基本はCloud Firestoreがオススメです?
具体的な違いはここが参考になります!
https://techblog.kayac.com/rtdb-vs-firestore

Step1: firebaseのDBを作成

https://firebase.google.com/?hl=ja

まずはここから
1. 「使ってみる」を押す
ログインしていない場合はログイン
そもそもアカウントがない場合は
Googleアカウントを作成してください?

step1.png

2. 「プロジェクトを追加」を押す

step2.png

3. プロジェクト名を入力
下に表示されているsample-6a560が
プロジェクトIDになります。

step3.png

4. Google アナリティクスの有無を選択
なしの場合は続行でプロジェクト完成
ありの場合は続行で次のアカウント選択

step4.png

5. アカウントを選択
 Default Account for Firebaseを選択

step5.png

これでプロジェクトが完成?✨
プロジェクト概要画面へ移行します?

6. サイドメニューのDatabaseを押す

3.png

7. DBの種類を選択、作成
 オススメはCloud Firestoreですが、
 今回はシンプルにしたいので
 firebase Realtime DBです?

2.png

8. セキュリティルールを選択
 テストモードで読み取りと書き込みを
 許可しておきましょう!

これでDatabaseができました??

Step2: Appの作成

ウェブアプリにfirebaseを追加していきます!

1. ダッシュボードから</>ウェブを選択

app1.png

2. アプリのニックネームを入力
 Firebase Hostingの設定は今回はなしで⭕️

app2.png

3.「コンソールに進む」を押す
 コードはいつでも確認できるので飛ばしてOK

これでダッシュボードから
アプリが確認できるようになりました?

app4.png

Step3: firebaseのインストール

兎にも角にもまずはインストール!!!?
https://firebase.google.com/docs/web/setup

referenceのnode.jsタブを見ていきます?
Nuxtには初めからpakage.jsonがあるので
$npm initは不要ですね!

ターミナル
$ npm install --save firebase

これでpakage.jsonのdependenciesに
firebaseのバージョンが追加されていますね!

pakage.json
{
 "dependencies": {
   "firebase": "^7.12.0",
   "nuxt": "^2.0.0",
   "vue": "^2.6.11",
   "vue-template-compiler": "^2.6.11"
 },
}

Step4: firebaseと連携する

plugin/firebase.jsを作ります?

【plugin/firebase.js】
Step3で見たreferenceの続きです。
・firebaseをimport
・firebase.initializeApp()
 アプリでfirebaseの初期化
・その中にFirebase SDK snippetを貼り付け
 (Step2で作成したアプリのコード)

plugin/firebase.js
import firebase from "firebase/app"

if (!firebase.apps.length) {
 firebase.initializeApp({
   apiKey: "貼り付け",
   authDomain: "貼り付け",
   databaseURL: "貼り付け",
   projectId: "貼り付け",
   storageBucket: "貼り付け",
   messagingSenderId: "貼り付け",
   appId: "貼り付け",
   measurementId: "貼り付け"
 })
}

export default firebase

apiKeyなどの見方は
firebaseプロジェクトダッシュボードの
ここの歯車を押して…

5.png

この画面の下にある…

6.png

ここです!
7.png

これで準備は整いました?

Step4: DBにinputでデータを追加してみる

現在のDBには何もありません。
8.png

inputでこのデータを送信してみましょう?

9.png

index.vue
<template>
 <div class="page">
   <label>
     <span>
       お名前:
     </span>
     <input
       type="text"
       v-model="user.name"
     >
   </label>
   <label>
     <span>
       email:
     </span>
     <input
       type="text"
       v-model="user.email"
     >
   </label>
   <button
     type="button"
     @click="submit"
   >
     Submit
   </button>
 </div>
</template>

<script>
import firebase from '@/plugins/firebase'

export default {
 data () {
   return {
     user: {
       name: "",
       email: ""
     },
   }
 },
 methods: {
   submit () {
     let Ref = firebase.database().ref()
     Ref.push({ name: this.user.name, body: this.user.email })
     .then(response => {
       console.log(response, this.user)
     })
   },
 },
}
</script>

【結果】

Databaseに反映されていますね?
consoleに表示されたkeyとも
バッチリ合っています?

10.png

【解説】
・plugin/firebase.jsをimportする

・firebase.database().ref()
 firebase.database()でデータが取り出せます。
 ref()でdatabaseのURL指定をします。
 引数が空=firebase.jsで貼り付けたURLです?
https://firebase.google.com/docs/reference/js/firebase.database
https://firebase.google.com/docs/reference/js/firebase.database.Database#ref

・push
 変数にしたURLにデータをpushします?
 データの書き込みmethodには
 set, push, update, transactionがあります。
 setはまるごと上書き
 pushはkeyでまとまったデータを追加
https://firebase.google.com/docs/reference/js/firebase.database.Reference#set

・.then
 通信が成功した時の処理を書きます。

【上手くいかないぞ?】
テストモードではなく
ロックモードになっている可能性があります。
その場合はDatabaseのルールを
trueにしてみてください〜!

11.png

Step5: DBからデータを取得してみる

先ほどのデータを取得してみましょう!?
keyでまとまったデータ全てではなく、
このkeyのnameだけを取得してみます?

10.png

Databaseのkey(赤枠の部分)を押してみましょう!
下の階層になったことでURLが変わりましたね!
ちなみにURLはブラウザではなくここです?

スクリーンショット 2020-03-28 15.36.32.png

【index.vue】
methodsのfetchDataを追加し、
template内でdivで表示させます。

index.vue
<template>
 <div class="page">
   <label>
     <span>
       お名前:
     </span>
     <input
       type="text"
       v-model="user.name"
     >
   </label>
   <label>
     <span>
       email:
     </span>
     <input
       type="text"
       v-model="user.email"
     >
   </label>
   <button
     type="button"
     @click="submit"
   >
     Submit
   </button>
   <button
     type="button"
     @click="fetchData"
   >
     fetchData
   </button>
   <div id="user.name" />
 </div>
</template>

<script>
import firebase from '@/plugins/firebase'

export default {
 data () {
   return {
     user: {
       name: "",
       email: ""
     },
   }
 },
 methods: {
   submit () {
     let Ref = firebase.database().ref()
     Ref.push({ name: this.user.name, body: this.user.email })
     .then(response => {
       console.log(response, this.user)
     })
   },
   fetchData () {
     let Ref = firebase.database().ref('/-M3Ub7gJeswkN3I7BSYk')
     Ref.on('value', function(snapshot){
       document.getElementById("user.name").innerHTML = snapshot.child("name").val()
     })
   },
 },
}
</script>

【結果】

fetch.gif

【解説】
・firebase.database().ref()
 下階層のURLに変更します!
 firebase.database().ref('/{ key名 }')

・ Ref.on('value', function(snapshot){ 処理 })
 データの取得をします?
https://firebase.google.com/docs/database/admin/retrieve-data

・document.getElementById〜
 div id="user.name"部分に表示させます

・snapshot.child("name").val();
 snapshot内(database内)で指定したURLの子
 つまり今回でいうと-M3Ub7gJeswkN3I7BSYkの下の
 nameの値だけを取得しています?

記事が公開したときにわかる様に、
note・Twitterフォローをお願いします?

https://twitter.com/aLizlab

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

Happy Number

20200403
Write an algorithm to determine if a number is "happy".

A happy number is a number defined by the following process: Starting with any positive integer, replace the number by the sum of the squares of its digits, and repeat the process until the number equals 1 (where it will stay), or it loops endlessly in a cycle which does not include 1. Those numbers for which this process ends in 1 are happy numbers.

Example:

Input: 19
Output: true
Explanation: 
12 + 92 = 82
82 + 22 = 68
62 + 82 = 100
12 + 02 + 02 = 1

Javascript:

/**
 * @param {number} n
 * @return {boolean}
 */
var isHappy = function(n, counter=0) {
    result = false;
    if(counter < 20){
        //切割n并算出各个位置上数字平方后存入array数组
        const array = n.toString().split("").map(n => n*n);
        //数组单位加和,参数:初始值,最终值,初始Index:0
        const sum = array.reduce((a, b)=> a + b, 0);

        if (sum ===1 ){
            result = true;
        } else {
            counter++;
            isHappy(sum, counter);
        }

        return result;

    }
};

sample 44 ms submission
Setを活用したバージョン

/**
 * @param {number} n
 * @return {boolean}
 */
var isHappy = function(n) {
  let seen = new Set();
  let current = n;

  while(true) {
    current = String(current).split('').reduce((acc, number) => (acc + Number(number)**2), 0)
    if(current === 1) {
      return true;
    } else if(seen.has(current)) {
      return false;
    } else {
      seen.add(current)
    }
  }
};
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

GatsbyでFeatherIconsを使う方法

Gatsbyでアイコンフォントを使うときのメモ。

使い方

アイコンフォントをインストールする。

npm install feather-icons --save

yarnの場合

yarn add feather-icons

コンポーネント内でインポート。

{feather.icons.facebook.toSvg()}だけだとHTMLがエスケープされて表示されるので注意。

// src/components/header.js
import React from "react"
import { Link } from "gatsby"
import feather from 'feather-icons'

const Header = () => {
  return (
<header id="header">
  <div className="container">
    <h1 className="logo">サイトタイトル</h1>
    <nav className="header-menu">
      <ul className="nav">
        <li><Link to="/about">メニュー</Link></li>
        <li>
          <Link to="/facebook-url">
            <div dangerouslySetInnerHTML={
              { __html: feather.icons.facebook.toSvg({width: 15,height: 13}) }
            } />
          </Link>
        </li>
      </ul>
    </nav>
  </div>
</header>
  )
}

export default Header

utilを使う場合

utilにコード入れて関数呼び出しの方が便利

// src/utils/feather.js
import React from 'react';
import feather from 'feather-icons';

export default (name, sizeArray) => {
  const featherString = feather.icons[name].toSvg({
    width: sizeArray[0],
    height: sizeArray[1]
  });
  return <div dangerouslySetInnerHTML={{ __html: featherString }} />;
};

コンポーネントで呼び出し

// src/components/header.js
import React from "react"
import feather from '../utils/feather';

const Header = () => {
  return (
<header id="header">
  {feather('facebook', ['30', '30'])}
</header>
  )
}
export default Header
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

puppeteerでユーザーによるクリックを待つ

ユーザーによるクリックなどのイベントををpuppeteerで待ちたい

闇が深い場面な気はしますがpuppeteerを使ってクリックや入力などのイベントを受け取りたいことがあったとします。

そんな時にイベントを検知するスクリプトに苦戦したので関数として書き残しておきます。Promiseに対してresolveするだけの関数を作りpage.exposeFunctionで関数を公開し、evaluateで公開した関数をaddEventListenerを使って登録します。
半自動でパスワードなどは入力させたい時なんかに使ってください。
引数のmyEventNameは複数回実行する時に被らなければ何でも構いません。

script.js
async function waitEvent(page,myEventName,eventType,elementId){
    return new Promise(async resolve=>{
        await page.exposeFunction(myEventName,()=>{resolve(myEventName);});
        await page.evaluate((elementId,eventType,myEventName)=>{
            document.getElementById(elementId).addEventListener(eventType,()=>{
                eval('window.'+myEventName+'();');
            });
        },elementId,eventType,myEventName);
    });
}

使い方

使用例です。
クリック以外のイベントにも使えます。
要素のイベントでは無い物が欲しければobserve等使えば行けるとは思います。

example.js
var result_input = await waitEvent(page,'myclickEvent','click','buttonId');

jQueryのval()などからの変更はプログラム側から意図的にイベントが発生させない限り反応できないらしい。

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

Node.js Worker Threads: TypeScriptのワーカーを起動する方法 〜ts-node、ts-node-devに対応する方法〜

Node.jsのWorker Threadsは、本物のスレッドプログラミングができます。ワーカーの処理を記述したJavaScriptを与えて、ワーカーを起動するわけですが、TypeScriptのファイルを指定するにはどしたらいいのでしょうか?

本稿でわかること

  • ts-nodeとWorker Threadsを組み合わせて、TypeScriptのワーカーを起動する方法
  • ts-node-devでTypeScriptのワーカーを起動する方法

前提知識

本稿を理解するにあたっては、下記の技術についての基礎的な知識が必要です。

  • Worker Threads
  • ts-node
    • TypeScriptのコンパイルとJavaScriptの実行をコマンド一つでできるツール。
      • tsc && node dist/main.jsを一発でできるようにしたツール。
    • nodeコマンドの感覚でTypeScriptを実行できる。 例: ts-node src/main.ts
  • ts-node-dev
    • ts-nodeとnode-devを組み合わせた開発ツール。
    • TypeScriptのコードに変更が加わると自動的にコンパイルし、プログラムを再起動してくれる。

解決したい課題: WorkerにTypeScriptを指定することはできない

JavaScriptでWorker Threadsを実行する方法は以下のような手順になります。

まず、ワーカー側の処理を実装したJavaScriptファイルを作ります:

worker.js
console.log('Hello from worker')

次に、ワーカーを起動する処理を書くのですが、Workerコンストラクタでワーカーのファイル名を指定する必要があります:

main.js
const {Worker} = require('worker_threads')
const worker = new Worker('./worker.js')

このmain.jsを実行すると、ワーカーが起動することが確認できます:

$ node main.js
Hello from worker

このコードをTypeScriptで書き直し、ts-nodeで同じように実行するとどうなるでしょうか? やってみましょう。

まず、ワーカー側の実装をTSに移植します。内容はworker.jsと全く同じです:

worker.ts
console.log('Hello from worker')

次に、main.jsをTSに移植します。大きな変更は、worker.jsではなくworker.tsを起動するように変える点です:

main.ts
import {Worker} from 'worker_threads'

new Worker(__dirname + '/worker.ts') // tsファイルを指定

このコードを、ts-nodeで起動してみます。すると、次のようなエラーが発生し、ワーカーが起動できないことが分かります。エラー内容は、「ワーカースクリプトの拡張子はjs, mjs, cjsじゃないとダメだよ」というものです。

$ ts-node src/main.ts
The worker script extension must be ".js", ".mjs", or ".cjs". Received ".ts"

このことから、Worker Threadsでは直接TypeScriptファイルが指定できないことが分かったと思います。

解決策: いったんJavaScriptファイルを経由するようにする

Worker Threadsで起動できるコードはJavaScriptのみという制約があるので、直接TypeScriptのワーカーを起動するのはあきらめます。迂回手段として、まずJavaScriptのワーカーを起動し、その中でTypeScriptコードをrequireするようにします。

先述した失敗作TypeScriptコードを手直ししていきましょう。

まず、main.tsはworker.tsではなく、worker.jsを起動するように直します:

main.ts
import {Worker} from 'worker_threads'

new Worker(__dirname + '/worker.js') // jsを起動するように直す

次に、最終的な目的地である、TypeScriptワーカーのファイルを作ります。名前はworker.jsと区別できるようtsWorker.tsにしておきます:

tsWorker.ts
console.log('tsWorker.ts started')

最後に、main.tsとtsWorker.tsを橋渡しする、worker.jsを実装します。worker.jsの重要な役割は、ts-nodeをregisterすることです。これにより、以降のコードではtsファイルをrequireして実行できるようになります:

worker.js
console.log('worker.js started')
require('ts-node').register() // 重要
require(__dirname + '/tsWorker.ts')

このコードを実行してみましょう。

$ ts-node src/main.ts
worker.js started
tsWorker.ts started

出力結果から、まずworker.jsが実行され、次にtsWoekr.tsが読み込まれ実行されたことがわかると思います。

ts-node-devでは、execArgvを空っぽにしてWorkerを起動する

これまでts-nodeでTypeScriptワーカーを起動する方法を説明してきましたが、類似のツールであるts-node-devでも同じ方法で対応できるのでしょうか? 結論を言うと、そのままでは対応できません。

上のmain.tsをts-node-devで実行してみると分かりますが、worker.jsは起動するものの、worker.js内のrequireが動作せず、スレッドが終了してしまいます:

$ ts-node-dev src/main.ts
Using ts-node version 8.8.1, typescript version 3.8.3
worker.js started

worker.jsでexecArgvを確認すると、ワーカー側では不要なts-node-devのフックが渡ってきているのがわかります:

worker.js
console.log('worker.js started')
console.log(process.execArgv)
// require('ts-node').register()
// require(__dirname + '/tsWorker.ts')
実行結果
$ ts-node-dev src/main.ts
Using ts-node version 8.8.1, typescript version 3.8.3
worker.js started
[
  '-r',
  '/var/folders/4l/mrmcxh3x40lbcpwxyz29ppcw0000gn/T/ts-node-dev-hook-1629690677650566.js'
]

解決策としては、main.tsのnew WorkerのオプションでexecArgvをカラにすることです:

main.ts
import {Worker} from 'worker_threads'

new Worker(__dirname + '/worker.js', {execArgv: []})

こうしておくと、まずはts-node-devでもTypeScriptのワーカーが起動できるようになります。

$ ts-node-dev src/main.ts
Using ts-node version 8.8.1, typescript version 3.8.3
worker.js started
tsWorker.ts started

しかし、この方法に問題がないわけではありません。ts-node-devの醍醐味としては、TypeScriptのコードを書き直したら、自動的に再コンパイルして、プロセスを起動しなおしてくれることです。しかし、この対処法では、スレッド側でrequireされたファイルをいくら修正しても、自動再コンパイル&再起動はされません。

この課題の解決策については、また時間を見つけて調べてみたいと思います。

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

dayjsで存在しない日付をバリデーションする

moment.jsをお使いの皆さんにはisValid関数があるじゃんとお思いでしょう。
dayjsはmoment.jsのAPIと高い互換性を持ちますがこの関数に関しては挙動が異なるみたいです。

dayjs('2020/02/31', 'YYYY/MM/DD').isValid() // true
moment('2020/02/31', 'YYYY/MM/DD').isValid() // false

dayjsではフォーマットが可能かどうかだけを判断してるみたいで、日付が存在するかは判定してない。
なので以下のようなメソッドを作ることで日付の存在をチェックできます。

const validate = (date, format) => {
  return dayjs(date, format).format(format) === date;
}
validate('2020/02/31', 'YYYY/MM/DD') // false

なぜこれで判定ができるのかというと、dayjsはフォーマットを行うと2020/02/312020/03/02に変換されます。
なので、もとの値と比較してズレていると存在しない日付という判定が行えるということでした。

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

Remove Duplicates from Sorted Array

20200403
第二問
做此题之前需要理解的时间空间复杂度
时空复杂度:
o(1), o(n), o(logn), o(nlogn)是算法的时空复杂度的表示。它不仅仅可以用于表示时间复杂度,也用于表示空间复杂度。
O后面的括号中有一个函数f(n)表示为(O(f(n))),它指明某个算法的耗时/耗空间与数据增长量之间的关系。其中的n代表输入数据的量。
时间复杂度O(1):
是最低的时空复杂度,也就是耗时/耗空间与输入数据大小无关,无论输入数据增大多少倍,耗时/耗空间都不变。 哈希算法就是典型的O(1)时间复杂度,无论数据规模多大,都可以在一次计算后找到目标(注释:在不考虑发生冲突的情况下)
时间复杂度为O(n):
就代表数据量增大几倍,耗时也增大几倍。例如常见的遍历算法。
时间复杂度O(n^2):
就代表数据量增大n倍时,耗时增大n的平方倍,这是比线性更高的时间复杂度。比如冒泡排序,就是典型的O(n^2)的算法,对n个数排序,需要扫描n×n次。
时间复杂度O(logn):
当数据增大n倍时,耗时增大logn倍(这里的log是以2为底的,比如,当数据增大256倍时,耗时只增大8倍,是比线性还要低的时间复杂度)。二分查找就是O(logn)的算法,每找一次排除一半的可能,256个数据中查找只要找8次就可以找到目标。
时间复杂度O(nlogn):
就是n乘以logn,当数据增大256倍时,耗时增大256*8=2048倍。这个复杂度高于线性低于平方。归并排序就是O(nlogn)的时间复杂度。
原文链接:https://blog.csdn.net/BIackMamba/java/article/details/90741470

テーマ:

Given a sorted array nums, remove the duplicates in-place such that each element appear only once and return the new length.

Do not allocate extra space for another array, you must do this by modifying the input array in-place with O(1) extra memory.

Example 1:

Given nums = [1,1,2],

Your function should return length = 2, with the first two elements of nums being 1 and 2 respectively.

It doesn't matter what you leave beyond the returned length.
Example 2:

Given nums = [0,0,1,1,1,2,2,3,3,4],

Your function should return length = 5, with the first five elements of nums being modified to 0, 1, 2, 3, and 4 respectively.

It doesn't matter what values are set beyond the returned length.
Clarification:

Confused why the returned value is an integer but your answer is an array?

Note that the input array is passed in by reference, which means modification to the input array will be known to the caller as well.

Internally you can think of this:

    // nums is passed in by reference. (i.e., without making a copy)
    int len = removeDuplicates(nums);

    // any modification to nums in your function would be known by the caller.
    // using the length returned by your function, it prints the first len elements.
     for (int i = 0; i < len; i++) {
            print(nums[i]);
     }

思路:

使用快慢指针来记录遍历的坐标。

开始时这两个指针都指向第一个数字

如果两个指针指的数字相同,则快指针向前走一步

如果不同,则两个指针都向前走一步

当快指针走完整个数组后,慢指针当前的坐标加 1 就是数组中不同数字的个数

答え:
Javescript

/**
 * @param {number[]} nums
 * @return {number}
 */
    var removeDuplicates = function(nums) {
    const numsLength = nums.length;
    if(numsLength == 0) return 0;
    //被动指针
    let slowP = 0;
    for(let fastP = 0; fastP < nums.length; fastP++){
        if(nums[fastP] != nums[slowP]){
            slowP++;
            //重构数组(用下一个指针的值覆盖重复元素)
            nums[slowP] = nums[fastP];
        }
    }
    //返回值:最后数组的长度
    return slowP + 1;
};
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

配列で同一のプロパティ値を持つオブジェクトを除外したい

やりたいこと

type Item = {
  id: number
  value: string
};

const items: Array<Item> = [
  { id: 1, value: 'foo' },
  { id: 2, value: 'bar' },
  { id: 3, value: 'bar' },
  { id: 4, value: 'baz' },
  { id: 5, value: 'baz' }
];

// 配列itemsからidの値は気にせず、valueの値が同じものを消したい!
const expect: Array<Item> =[
  { id: 1, value: 'foo' }, 
  { id: 2, value: 'bar' },
  { id: 4, value: 'baz' }
];

方法

const result = items.filter((item, index, self) => self.map(e => e.value).indexOf(item.value) === index);

result.forEach(e => console.log('id:' + e.id + ',' + 'value:' + e.value));
// id:1,value:foo 
// id:2,value:bar 
// id:4,value:baz 
  • filter()やmap()の第3引数ってどういう時に使うんだろう:thinking:と思っていましたが、こういう用途があるんですね。勉強になりました。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

オブジェクトの配列で同一のプロパティ値を持つ要素を除外したい

やりたいこと

type Item = {
  id: number
  value: string
};

const items: Array<Item> = [
  { id: 1, value: 'foo' },
  { id: 2, value: 'bar' },
  { id: 3, value: 'bar' },
  { id: 4, value: 'baz' },
  { id: 5, value: 'baz' }
];

// 配列itemsからidの値は気にせず、valueの値が同じものを消したい!
const expect: Array<Item> = [
  { id: 1, value: 'foo' }, 
  { id: 2, value: 'bar' },
  { id: 4, value: 'baz' }
];

方法

const result = items.filter((item, index, self) =>
  self.map(e => e.value).indexOf(item.value) === index
);

result.forEach(e => console.log('id:' + e.id + ',' + 'value:' + e.value));
// id:1,value:foo 
// id:2,value:bar 
// id:4,value:baz 
  • filter()やmap()の第3引数ってどういう時に使うんだろう:thinking:と思っていましたが、こういう用途があるんですね。勉強になりました。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

VBA(Excel)でWebページをPDF化,PNG化,JPG化する方法(Selenium Basic)

VBA(Excel)でWebページをPDF化,PNG化,JPG化する方法

-概要-
業務でよく使うことがあったので作りました。

Seleniumは、簡単にブラウザ操作を自動化するようなツールです。
よく聞くのは、PythonやJavaScriptでSeleniumをつかう方法ですが、実はVisualBasicで動かすことができます。

特に、日本ではExcelへの依存度が高い企業が多いと思うので、GUIとしてExcelを用いることは心理的抵抗がないのではないでしょうか?(いろいろ言いたいことはあると思いますが...)

-手順-
1.Selenium Releaseから、SeleniumBasicをダウンロードする。
2.上のファイルをインストールする。
3.必要なブラウザにあったドライバをダウンロードする。(今回は、Chromeにします)
Chrome Driver
4.C:\Users[ユーザ名]\AppData\Local\SeleniumBasicのchromedriver.exeを3でダウンロードしたchromedriver.exeで更新する。(ファイル上書きするだけ)
5.vbaを書く

-詳説-
1.2.URL等は変わるかもしれません。各自調べてください。デフォルトでインストールすれば大丈夫です。
3.ここも、URLは変わるかもしれません。各自調べてください。Chromeの場合は、ご自身の使っているChromeのバージョンと同じバージョンをダウンロードしてきてください。
4.置き換えるだけです。
5.

4まで行うと、Excel 開発環境でツール->参照設定でSelenium Type Libraryが出てくるのでチェックしましょう。これをしないとSeleniumが使えないです。あと、Microsoft Scripting Runtime等の参照設定もチェックしてください。

以下,例です。
to~ (保存先ディレクトリ、保存ファイル名、WebページURL、シートの行番号(確認用にチェックとか保存ディレクトリの書き込みする用に)、保存できたか管理するシート)

PDF用

Option Explicit

Sub toPDF(ByVal directory, ByVal filename, ByVal url, ByVal i, ByVal sheetn As String)
  On Error GoTo myerror:
    Dim sheet1 As Worksheet
    Set sheet1 = Sheets(sheetn)
    Dim sPath As String, WSH As Variant
    Set WSH = CreateObject("WScript.Shell")
    sPath = WSH.SpecialFolders("Desktop") & "\"
    If (directory = "") Then directory = sPath
    If (Right(directory, 1) <> "\") Then directory = directory & "\"
    If (filename = "") Then filename = Format(Now(), "yyyy-mm-dd-hh-mm-ss")
    Dim savepath As String
    savepath = directory & filename & ".pdf"
    Dim driver As New Selenium.ChromeDriver
    driver.SetPreference "download.default_directory", directory
    driver.SetPreference "download.directory_upgrade", True
    driver.SetPreference "download.prompt_for_download", False
    driver.SetPreference "safebrowsing.enabled", True
    driver.SetPreference "plugins.plugins_disabled", Array("Chrome PDF Viewer")
    driver.AddArgument "headless"
    driver.AddArgument "disable-gpu"
    driver.AddArgument "hide-scrollbars"
    Dim w As Long
    Dim h As Long

    driver.Start
    driver.Get url

    w = driver.ExecuteScript("return document.body.scrollWidth")
    h = driver.ExecuteScript("return document.body.scrollHeight")

    Dim pdf As Object

    driver.Window.SetSize w, h

    Set pdf = CreateObject("Selenium.PdfFile")
    pdf.SetPageSize 210, 297, "mm"
    pdf.AddImage driver.TakeScreenshot, True
    pdf.SaveAs savepath
    sheet1.Cells(i, 5).Value = 1
    sheet1.Cells(i, 6).Value = savepath
    driver.Quit
    Exit Sub
myerror:
    MsgBox "no"
    sheet1.Cells(i, 5).Value = 0
End Sub
Sub dopdf()
  Dim sPath As String, WSH As Variant
    Set WSH = CreateObject("WScript.Shell")
    sPath = WSH.SpecialFolders("Desktop") & "\"
    Dim directory As String
    Dim filename As String
    directory = sPath
    If (Right(Len(directory), 1) <> "\") Then directory = directory & "\"

  Dim sheet1 As String
  Dim i As Long

  sheet1 = "pdf用"

  Dim sheetn As Worksheet
  Set sheetn = Sheets(sheet1)
  Dim r As Long
  r = sheetn.Cells(Rows.Count, 4).End(xlUp).Row
  For i = 2 To r
    If (sheetn.Cells(i, 4).Value = "") Then
      GoTo a1:
    End If
    filename = sheetn.Cells(i, 3).Text
    If (filename = "") Then filename = Format(Now(), "yyyy-mm-dd-hh-mm-ss")
    Dim result
    result = Dir(sheetn.Cells(i, 2).Text, vbDirectory)
    If (directory <> "" Or result <> True) Then sPath = sheetn.Cells(i, 2).Text
    Call toPDF(sPath, filename, sheetn.Cells(i, 4).Text, i, sheet1)
a1:
  Next i
End Sub

JPG用

Option Explicit

Sub toJPG(ByVal directory, ByVal filename, ByVal url, ByVal i, ByVal sheetn As String)
  On Error GoTo myerror:
    Dim sheet1 As Worksheet
    Set sheet1 = Sheets(sheetn)
    Dim sPath As String, WSH As Variant
    Set WSH = CreateObject("WScript.Shell")
    sPath = WSH.SpecialFolders("Desktop") & "\"
    If (directory = "") Then directory = sPath
    If (Right(directory, 1) <> "\") Then directory = directory & "\"
    If (filename = "") Then filename = Format(Now(), "yyyy-mm-dd-hh-mm-ss")
    Dim savepath As String
    savepath = directory & filename & ".jpg"
    Dim driver As New Selenium.ChromeDriver
    driver.SetPreference "download.default_directory", directory
    driver.SetPreference "download.directory_upgrade", True
    driver.SetPreference "download.prompt_for_download", False
    driver.SetPreference "safebrowsing.enabled", True
    driver.SetPreference "plugins.plugins_disabled", Array("Chrome PDF Viewer")
    driver.AddArgument "headless"
    driver.AddArgument "disable-gpu"
    driver.AddArgument "hide-scrollbars"
    Dim w As Long
    Dim h As Long

    driver.Start
    driver.Get url
    driver.FindElementByClass("tab02").Click
    driver.ExecuteScript ("this.document.getElementById('tab01').setAttribute('class','tabContent01');")
    driver.ExecuteScript ("this.document.getElementById('tab03').setAttribute('class','tabContent03');")
    w = driver.ExecuteScript("return document.body.scrollWidth")
    h = driver.ExecuteScript("return document.body.scrollHeight")
    driver.Window.SetSize w, h
    driver.TakeScreenshot.SaveAs savepath
    sheet1.Cells(i, 5).Value = 1
    sheet1.Cells(i, 6).Value = savepath
    driver.Quit
    Exit Sub
myerror:

    sheet1.Cells(i, 5).Value = 0
End Sub
Sub dojpg()
  Dim sPath As String, WSH As Variant
    Set WSH = CreateObject("WScript.Shell")
    sPath = WSH.SpecialFolders("Desktop") & "\"
    Dim directory As String
    Dim filename As String
    directory = sPath
    If (Right(Len(directory), 1) <> "\") Then directory = directory & "\"

  Dim sheet1 As String
  Dim i As Long

  sheet1 = "jpg用"

  Dim sheetn As Worksheet
  Set sheetn = Sheets(sheet1)
  Dim r As Long
  r = sheetn.Cells(Rows.Count, 4).End(xlUp).Row
  For i = 2 To r
    If (sheetn.Cells(i, 4).Value = "") Then
      GoTo a1:
    End If
    filename = sheetn.Cells(i, 3).Text
    If (filename = "") Then filename = Format(Now(), "yyyy-mm-dd-hh-mm-ss")
    Dim result
    result = Dir(sheetn.Cells(i, 2).Text, vbDirectory)
    If (directory <> "" Or result <> True) Then sPath = sheetn.Cells(i, 2).Text
    Call toJPG(sPath, filename, sheetn.Cells(i, 4).Text, i, sheet1)
a1:
  Next i
End Sub

PNG用

Option Explicit

Sub toPNG(ByVal directory, ByVal filename, ByVal url, ByVal i, ByVal sheetn As String)
  On Error GoTo myerror:
    Dim sheet1 As Worksheet
    Set sheet1 = Sheets(sheetn)
    Dim sPath As String, WSH As Variant
    Set WSH = CreateObject("WScript.Shell")
    sPath = WSH.SpecialFolders("Desktop") & "\"
    If (directory = "") Then directory = sPath
    If (Right(directory, 1) <> "\") Then directory = directory & "\"
    If (filename = "") Then filename = Format(Now(), "yyyy-mm-dd-hh-mm-ss")
    Dim savepath As String
    savepath = directory & filename & ".png"
    Dim driver As New Selenium.ChromeDriver
    driver.SetPreference "download.default_directory", directory
    driver.SetPreference "download.directory_upgrade", True
    driver.SetPreference "download.prompt_for_download", False
    driver.SetPreference "safebrowsing.enabled", True
    driver.SetPreference "plugins.plugins_disabled", Array("Chrome PDF Viewer")
    driver.AddArgument "headless"
    driver.AddArgument "disable-gpu"
    driver.AddArgument "hide-scrollbars"
    Dim w As Long
    Dim h As Long

    driver.Start
    driver.Get url
    driver.FindElementByClass("tab02").Click
    driver.ExecuteScript ("this.document.getElementById('tab01').setAttribute('class','tabContent01');")
    driver.ExecuteScript ("this.document.getElementById('tab03').setAttribute('class','tabContent03');")
    w = driver.ExecuteScript("return document.body.scrollWidth")
    h = driver.ExecuteScript("return document.body.scrollHeight")
    driver.Window.SetSize w, h
    driver.TakeScreenshot.SaveAs savepath
    sheet1.Cells(i, 5).Value = 1
    sheet1.Cells(i, 6).Value = savepath
    driver.Quit
    Exit Sub
myerror:

    sheet1.Cells(i, 5).Value = 0
End Sub
Sub dopng()
  Dim sPath As String, WSH As Variant
    Set WSH = CreateObject("WScript.Shell")
    sPath = WSH.SpecialFolders("Desktop") & "\"
    Dim directory As String
    Dim filename As String
    directory = sPath
    If (Right(Len(directory), 1) <> "\") Then directory = directory & "\"

  Dim sheet1 As String
  Dim i As Long

  sheet1 = "png用"

  Dim sheetn As Worksheet
  Set sheetn = Sheets(sheet1)
  Dim r As Long
  r = sheetn.Cells(Rows.Count, 4).End(xlUp).Row
  For i = 2 To r
    If (sheetn.Cells(i, 4).Value = "") Then
      GoTo a1:
    End If
    filename = sheetn.Cells(i, 3).Text
    If (filename = "") Then filename = Format(Now(), "yyyy-mm-dd-hh-mm-ss")
    Dim result
    result = Dir(sheetn.Cells(i, 2).Text, vbDirectory)
    If (directory <> "" Or result <> True) Then sPath = sheetn.Cells(i, 2).Text
    Call toPNG(sPath, filename, sheetn.Cells(i, 4).Text, i, sheet1)
a1:
  Next i
End Sub

ブック構成は、"pdf用","png用","jpg用"シート3つです。

B,C,D列2行目以降に
保存先、保存名,URLを入れて
マクロを動かすと保存されます。

ちなみに、driver.executescirpts等で、DOM操作もできるので、WebページをいじってからそのWebページをキャプチャすることができます。

眠い。。。
また、写真とか後からつけます。

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

VBA(Excel)でWebページをPDF化,PNG化,JPG化する方法(Slenenium Basic)

VBA(Excel)でWebページをPDF化,PNG化,JPG化する方法

-概要-
業務でよく使うことがあったので作りました。

Seleniumは、簡単にブラウザ操作を自動化するようなツールです。
よく聞くのは、PythonやJavaScriptでSeleniumをつかう方法ですが、実はVisualBasicで動かすことができます。

特に、日本ではExcelへの依存度が高い企業が多いと思うので、GUIとしてExcelを用いることは心理的抵抗がないのではないでしょうか?(いろいろ言いたいことはあると思いますが...)

-手順-
1.Selenium Realeaseから、SeleniumBasicをダウンロードする。
2.上のファイルをインストールする。
3.必要なブラウザにあったドライバをダウンロードする。(今回は、Chromeにします)
Chrome Driver
4.C:\Users[ユーザ名]\AppData\Local\SeleniumBasicのchromedriver.exeを3でダウンロードしたchromedriver.exeで更新する。(ファイル上書きするだけ)
5.vbaを書く

-詳説-
1.2.URL等は変わるかもしれません。各自調べてください。デフォルトでインストールすれば大丈夫です。
3.ここも、URLは変わるかもしれません。各自調べてください。Chromeの場合は、ご自身の使っているChromeのバージョンと同じバージョンをダウンロードしてきてください。
4.置き換えるだけです。
5.

4まで行うと、Excel 開発環境でツール->参照設定でSelenium Type Libraryが出てくるのでチェックしましょう。これをしないとSeleniumが使えないです。あと、Microsoft Scripting Runtime等の参照設定もチェックしてください。

以下,例です。
to~ (保存先ディレクトリ、保存ファイル名、WebページURL、シートの行番号(確認用にチェックとか保存ディレクトリの書き込みする用に)、保存できたか管理するシート)

PDF用

Option Explicit

Sub toPDF(ByVal directory, ByVal filename, ByVal url, ByVal i, ByVal sheetn As String)
  On Error GoTo myerror:
    Dim sheet1 As Worksheet
    Set sheet1 = Sheets(sheetn)
    Dim sPath As String, WSH As Variant
    Set WSH = CreateObject("WScript.Shell")
    sPath = WSH.SpecialFolders("Desktop") & "\"
    If (directory = "") Then directory = sPath
    If (Right(directory, 1) <> "\") Then directory = directory & "\"
    If (filename = "") Then filename = Format(Now(), "yyyy-mm-dd-hh-mm-ss")
    Dim savepath As String
    savepath = directory & filename & ".pdf"
    Dim driver As New Selenium.ChromeDriver
    driver.SetPreference "download.default_directory", directory
    driver.SetPreference "download.directory_upgrade", True
    driver.SetPreference "download.prompt_for_download", False
    driver.SetPreference "safebrowsing.enabled", True
    driver.SetPreference "plugins.plugins_disabled", Array("Chrome PDF Viewer")
    driver.AddArgument "headless"
    driver.AddArgument "disable-gpu"
    driver.AddArgument "hide-scrollbars"
    Dim w As Long
    Dim h As Long

    driver.Start
    driver.Get url

    w = driver.ExecuteScript("return document.body.scrollWidth")
    h = driver.ExecuteScript("return document.body.scrollHeight")

    Dim pdf As Object

    driver.Window.SetSize w, h

    Set pdf = CreateObject("Selenium.PdfFile")
    pdf.SetPageSize 210, 297, "mm"
    pdf.AddImage driver.TakeScreenshot, True
    pdf.SaveAs savepath
    sheet1.Cells(i, 5).Value = 1
    sheet1.Cells(i, 6).Value = savepath
    driver.Quit
    Exit Sub
myerror:
    MsgBox "no"
    sheet1.Cells(i, 5).Value = 0
End Sub
Sub dopdf()
  Dim sPath As String, WSH As Variant
    Set WSH = CreateObject("WScript.Shell")
    sPath = WSH.SpecialFolders("Desktop") & "\"
    Dim directory As String
    Dim filename As String
    directory = sPath
    If (Right(Len(directory), 1) <> "\") Then directory = directory & "\"

  Dim sheet1 As String
  Dim i As Long

  sheet1 = "pdf用"

  Dim sheetn As Worksheet
  Set sheetn = Sheets(sheet1)
  Dim r As Long
  r = sheetn.Cells(Rows.Count, 4).End(xlUp).Row
  For i = 2 To r
    If (sheetn.Cells(i, 4).Value = "") Then
      GoTo a1:
    End If
    filename = sheetn.Cells(i, 3).Text
    If (filename = "") Then filename = Format(Now(), "yyyy-mm-dd-hh-mm-ss")
    Dim result
    result = Dir(sheetn.Cells(i, 2).Text, vbDirectory)
    If (directory <> "" Or result <> True) Then sPath = sheetn.Cells(i, 2).Text
    Call toPDF(sPath, filename, sheetn.Cells(i, 4).Text, i, sheet1)
a1:
  Next i
End Sub

JPG用

Option Explicit

Sub toJPG(ByVal directory, ByVal filename, ByVal url, ByVal i, ByVal sheetn As String)
  On Error GoTo myerror:
    Dim sheet1 As Worksheet
    Set sheet1 = Sheets(sheetn)
    Dim sPath As String, WSH As Variant
    Set WSH = CreateObject("WScript.Shell")
    sPath = WSH.SpecialFolders("Desktop") & "\"
    If (directory = "") Then directory = sPath
    If (Right(directory, 1) <> "\") Then directory = directory & "\"
    If (filename = "") Then filename = Format(Now(), "yyyy-mm-dd-hh-mm-ss")
    Dim savepath As String
    savepath = directory & filename & ".jpg"
    Dim driver As New Selenium.ChromeDriver
    driver.SetPreference "download.default_directory", directory
    driver.SetPreference "download.directory_upgrade", True
    driver.SetPreference "download.prompt_for_download", False
    driver.SetPreference "safebrowsing.enabled", True
    driver.SetPreference "plugins.plugins_disabled", Array("Chrome PDF Viewer")
    driver.AddArgument "headless"
    driver.AddArgument "disable-gpu"
    driver.AddArgument "hide-scrollbars"
    Dim w As Long
    Dim h As Long

    driver.Start
    driver.Get url
    driver.FindElementByClass("tab02").Click
    driver.ExecuteScript ("this.document.getElementById('tab01').setAttribute('class','tabContent01');")
    driver.ExecuteScript ("this.document.getElementById('tab03').setAttribute('class','tabContent03');")
    w = driver.ExecuteScript("return document.body.scrollWidth")
    h = driver.ExecuteScript("return document.body.scrollHeight")
    driver.Window.SetSize w, h
    driver.TakeScreenshot.SaveAs savepath
    sheet1.Cells(i, 5).Value = 1
    sheet1.Cells(i, 6).Value = savepath
    driver.Quit
    Exit Sub
myerror:

    sheet1.Cells(i, 5).Value = 0
End Sub
Sub dojpg()
  Dim sPath As String, WSH As Variant
    Set WSH = CreateObject("WScript.Shell")
    sPath = WSH.SpecialFolders("Desktop") & "\"
    Dim directory As String
    Dim filename As String
    directory = sPath
    If (Right(Len(directory), 1) <> "\") Then directory = directory & "\"

  Dim sheet1 As String
  Dim i As Long

  sheet1 = "jpg用"

  Dim sheetn As Worksheet
  Set sheetn = Sheets(sheet1)
  Dim r As Long
  r = sheetn.Cells(Rows.Count, 4).End(xlUp).Row
  For i = 2 To r
    If (sheetn.Cells(i, 4).Value = "") Then
      GoTo a1:
    End If
    filename = sheetn.Cells(i, 3).Text
    If (filename = "") Then filename = Format(Now(), "yyyy-mm-dd-hh-mm-ss")
    Dim result
    result = Dir(sheetn.Cells(i, 2).Text, vbDirectory)
    If (directory <> "" Or result <> True) Then sPath = sheetn.Cells(i, 2).Text
    Call toJPG(sPath, filename, sheetn.Cells(i, 4).Text, i, sheet1)
a1:
  Next i
End Sub

PNG用

Option Explicit

Sub toPNG(ByVal directory, ByVal filename, ByVal url, ByVal i, ByVal sheetn As String)
  On Error GoTo myerror:
    Dim sheet1 As Worksheet
    Set sheet1 = Sheets(sheetn)
    Dim sPath As String, WSH As Variant
    Set WSH = CreateObject("WScript.Shell")
    sPath = WSH.SpecialFolders("Desktop") & "\"
    If (directory = "") Then directory = sPath
    If (Right(directory, 1) <> "\") Then directory = directory & "\"
    If (filename = "") Then filename = Format(Now(), "yyyy-mm-dd-hh-mm-ss")
    Dim savepath As String
    savepath = directory & filename & ".png"
    Dim driver As New Selenium.ChromeDriver
    driver.SetPreference "download.default_directory", directory
    driver.SetPreference "download.directory_upgrade", True
    driver.SetPreference "download.prompt_for_download", False
    driver.SetPreference "safebrowsing.enabled", True
    driver.SetPreference "plugins.plugins_disabled", Array("Chrome PDF Viewer")
    driver.AddArgument "headless"
    driver.AddArgument "disable-gpu"
    driver.AddArgument "hide-scrollbars"
    Dim w As Long
    Dim h As Long

    driver.Start
    driver.Get url
    driver.FindElementByClass("tab02").Click
    driver.ExecuteScript ("this.document.getElementById('tab01').setAttribute('class','tabContent01');")
    driver.ExecuteScript ("this.document.getElementById('tab03').setAttribute('class','tabContent03');")
    w = driver.ExecuteScript("return document.body.scrollWidth")
    h = driver.ExecuteScript("return document.body.scrollHeight")
    driver.Window.SetSize w, h
    driver.TakeScreenshot.SaveAs savepath
    sheet1.Cells(i, 5).Value = 1
    sheet1.Cells(i, 6).Value = savepath
    driver.Quit
    Exit Sub
myerror:

    sheet1.Cells(i, 5).Value = 0
End Sub
Sub dopng()
  Dim sPath As String, WSH As Variant
    Set WSH = CreateObject("WScript.Shell")
    sPath = WSH.SpecialFolders("Desktop") & "\"
    Dim directory As String
    Dim filename As String
    directory = sPath
    If (Right(Len(directory), 1) <> "\") Then directory = directory & "\"

  Dim sheet1 As String
  Dim i As Long

  sheet1 = "png用"

  Dim sheetn As Worksheet
  Set sheetn = Sheets(sheet1)
  Dim r As Long
  r = sheetn.Cells(Rows.Count, 4).End(xlUp).Row
  For i = 2 To r
    If (sheetn.Cells(i, 4).Value = "") Then
      GoTo a1:
    End If
    filename = sheetn.Cells(i, 3).Text
    If (filename = "") Then filename = Format(Now(), "yyyy-mm-dd-hh-mm-ss")
    Dim result
    result = Dir(sheetn.Cells(i, 2).Text, vbDirectory)
    If (directory <> "" Or result <> True) Then sPath = sheetn.Cells(i, 2).Text
    Call toPNG(sPath, filename, sheetn.Cells(i, 4).Text, i, sheet1)
a1:
  Next i
End Sub

ブック構成は、"pdf用","png用","jpg用"シート3つです。

B,C,D列2行目以降に
保存先、保存名,URLを入れて
マクロを動かすと保存されます。

ちなみに、driver.executescirpts等で、DOM操作もできるので、WebページをいじってからそのWebページをキャプチャすることができます。

眠い。。。
また、写真とか後からつけます。

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

VuetifyのV2以降のグリッドシステムで縦並びさせる方法

こんにちは。

Vue/NuxtのUIフレームワークとしてよく用いられるVuetifyについて書き記したいと思います。

タイトルにもある通りなんですが、縦並びさせる方法についてです。
VuetifyはV2以降はグリッドシステムが大きく変わって、自分は縦並びさせる方法わかるまでに時間かかりました。。。

V2以降は、v-row,v-colあたりを作ってレイアウトを作っていくスタイルになりました。
まず横並びは簡単です。

hgoe.vue
<template>
  <v-row>
    <v-col cols="12" style="outline: solid 1px black;">12</v-col>
    <v-col cols="6" style="outline: solid 1px black;">6</v-col>
    <v-col cols="6" style="outline: solid 1px black;">6</v-col>
    <v-col cols="4" style="outline: solid 1px black;">4</v-col>
    <v-col cols="4" style="outline: solid 1px black;">4</v-col>
    <v-col cols="4" style="outline: solid 1px black;">4</v-col>
  </v-row>
</template>

こんな感じにすると、こんな感じになります。
スクリーンショット 2020-04-03 0.41.20.png
cols="12" が横幅いっぱいです。
で、僕がやりたかったのは、縦並びにすること

hgoe.vue
<template>
  <v-row class="flex-column">
    <v-col cols="12" style="outline: solid 1px black;">12</v-col>
    <v-col cols="6" style="outline: solid 1px black;">6</v-col>
    <v-col cols="6" style="outline: solid 1px black;">6</v-col>
    <v-col cols="4" style="outline: solid 1px black;">4</v-col>
    <v-col cols="4" style="outline: solid 1px black;">4</v-col>
    <v-col cols="4" style="outline: solid 1px black;">4</v-col>
  </v-row>
</template>

v-rowにclass="flex-column"をつければ。。。
スクリーンショット 2020-04-03 0.49.11.png
縦になりました。

ネストさせて組み合わせれば複雑なレイアウトも組めます。

V2以降のVuetifyを使ってみて、縦並びさせる方法わかるまで時間かかったので、同じようなことに悩んでいる人いたらと思って書きました。

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

Vue.jsでローディング画面作ってみた

初投稿です。
御見苦しいところなどあればすみません…

仕事でVue.jsを使ってSPAを作っていた際に、どうしても少し時間がかかってしまう処理を行っている間に別の操作をさせたくなかったので、ローディング画面を表示させて他の操作を実行できないようにしました。

そのとき使った「vue-loading」が便利だったのでご紹介。

インストール

以下のコマンドでインストール

yarnの場合

yarn add vue-loading-template

npmの場合

npm install vue-lodaing-template

使い方

他のモジュールと同様に使いたいViewでインストールして使います。

今回は、親のコンポーネントから受け取ったisLoadingがtrueの場合、全体にマスクしてクルクルを表示させました。

Lading.vue
<template>
  <div class="modal-mask" v-if="isLoading === true">
    <div class="loading">
      <vue-loading type="spin" color="#333" :size="{ width: '50px', height: '50px'}"></vue-loading>
    </div>
  </div>
</template>

<script>
import { VueLoading } from 'vue-loading-template';

export default {
  props: {
    isLoading: Boolean,
  },
  components: {
    VueLoading
  },
}
</script>

<style scoped>
.modal-mask {
  z-index:1;
  position:fixed;
  top:0;
  left:0;
  width:100%;
  height:100%;
  background-color:rgba(0,0,0,0.5);
  display: flex;
  align-items: center;
  justify-content: center;
}
</style>


これだけでロード中とわかりやすいクルクルを表示できます。便利…
他の表示パターンもあるので、デモサイトを見てみてください。

参考文献

Vue.jsでシンプルなローディングを表示する「vue-loading」の使い方
GitHub jkchao/vue-loading

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