LoginSignup
0

More than 1 year has passed since last update.

ループを使わずに配列上の連続する2つの値を要素とするリストを作る

Posted at

はじめに

SENSYN Robotics(センシンロボティクス)の中山です。
Webアプリやそのインフラ周りと、Web側とドローンの接続を行うデバイスドライバ的な部分を担当しています。

ここ1〜2年探していた、ループを使わずにリスト上の連続する2つの値を要素とするリストを作る方法をやっと思いついたので、それを記録するためのエントリです。

なにをしたいか

直近でやりたかったのは、ドローンの飛行記録から実際に飛行した距離を算出することでした。ドローンは飛行している場所(緯度・経度)を定期的に通知しています。CSVにすると、以下のようなイメージのデータです。

時刻,緯度,経度
2020-12-08T02:26:33Z.231,35.166264279608,138.67945381096
2020-12-08T02:26:33.834Z,35.166264279610,138.67945381097
2020-12-08T02:26:34.102Z,35.166264279611,138.67945381098
...

二点の緯度・経度が分かれば、その二点間の距離は計算可能です。そして、2点間の距離が計算できれば、あとは各2点間の距離を足し合わせて飛行した距離が計算できます。

ループを使って実装してみる

2点間の距離の計算にはnpm install geolibしてgeolibを使います。

ループを使うとこんな感じでしょうか。

const geolib = require('geolib');

const route = [
  {latitude: 35.166264279608, longitude: 138.67945381096},
  {latitude: 35.167274290660, longitude: 138.67945381157},
  {latitude: 35.168284321721, longitude: 138.67945381218},
];

let from = null;
let dist = 0.0;
for (const to of route) {
  if (from != null) {
    dist += geolib.getDistance(from, to);
  }
  from = to;
}
console.log(`distance: ${dist}m`);

連続する2つの値を扱うために、前回のループの値をfromに入れて、今回の値をtoとして扱っています。

これはこれで悪くないのですが、fromやdistを書き換えているのが気持ち悪いです。if文があるのも美しさに欠けます。

reduce()を使って実装してみる

変数の書き換えを排除するためにreduce()を使ってみると、こんな風になりました。

const geolib = require('geolib');

const route = [
  {latitude: 35.166264279608, longitude: 138.67945381096},
  {latitude: 35.167274290660, longitude: 138.67945381157},
  {latitude: 35.168284321721, longitude: 138.67945381218},
];

const dist = route
  .reduce((acc, v) => [...acc, {from: acc[acc.length - 1].to, to: v}], [{from: route[0], to: route[0]}])
  .filter(v => v.from != v.to)
  .map(v => geolib.getDistance(v.from, v.to))
  .reduce((sum, d) => sum + d, 0);
console.log(`distance: ${dist}m`);

最初のreduce()

  .reduce((acc, v) => [...acc, {from: acc[acc.length - 1].to, to: v}], [{from: route[0], to: route[0]}])

ここで連続する2つの値を要素とするリストを作っています。routeの各要素に対して、適用結果の配列の最後の要素とのペアを作っていきます。

  1. accの初期値 [{from: 要素0, to: 要素0}]
  2. 1回目の適用 [{from: 要素0, to: 要素0}, {from: 要素0, to: 要素0}]
  3. 2回目の適用 [{from: 要素0, to: 要素0}, {from: 要素0, to: 要素0}], {from: 要素0, to: 要素1}]
  4. 3回目の適用 [{from: 要素0, to: 要素0}, {from: 要素0, to: 要素0}], {from: 要素0, to: 要素1}, {from: 要素1, to: 要素2}]

スプレッド演算子...を使わずに、(acc, v) => {acc.push({from: acc[acc.length - 1].to, to: v}); return acc}とかでもいいです。

filter()

ここで目的の「連続する2つの値を要素とするリスト」を含むリストができたのですが、最初の2つの要素はfromtoがどちらも要素0を指しています。距離を計算するのであればこのままでもいいのですが、汎用性を考えるとちょっと問題があります。そこでfromtoが同じものを排除して、「連続する2つの値を要素とするリスト」だけを取り出すのがここのfilter()です

  .filter(v => v.from != v.to)

map()

2点から2点間の距離を計算しています。

  .map(v => geolib.getDistance(v.from, v.to))

2個目のreduce()

距離を合計して、ルート全体の飛行距離を計算しています。

  .reduce((sum, d) => sum + d, 0);

まとめ

ループを使わずに配列上の連続する2つの値を要素とするリストを作るには、以下のようなコードを書けば良い。

list
  .reduce((acc, v) => [...acc, {from: acc[acc.length - 1].to, to: v}], [{from: list[0], to: list[0]}])
  .filter(v => v.from != v.to)

JavaScript以外でも、reduce()filter()が使えれば同じ手法が使えると思います。

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
0