LoginSignup
1
0

More than 3 years have 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()が使えれば同じ手法が使えると思います。

1
0
3

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
  3. You can use dark theme
What you can do with signing up
1
0