覚えたら書く

IT関係のデベロッパとして日々覚えたことを書き残したいです。twitter: @yyoshikaw

jqで基礎的な操作をしてみる

前回、jqコマンドをインストールしました。


基礎的な操作だけ試しておきます。

仮に person.json というファイルの内容が以下のようになっているとします

{ "name": { "first" : "taro", "last": "yamada" }, "age": 20 }


これを整形して表示する場合は以下になります

cat person.json | jq .

出力結果は以下の通りで整形されています。

{
  "name": {
    "first": "taro",
    "last": "yamada"
  },
  "age": 20
}


このJSONから「name」項目を取得したい場合は以下です

cat person.json | jq .name

出力結果は以下の通りで整形されています。

{
  "first": "taro",
  "last": "yamada"
}


「name」の中の 「first」の値だけ取りたいときは以下になります

cat person.json | jq .name

出力結果は以下の通り

"taro"


このやり方が望ましいかわかりませんが以下でもいけますね

cat person.json | jq .name | jq .first

出力結果は以下の通り

"taro"


出力結果を囲んでいるダブルクォートが邪魔な場合は -r オプションをつけます

cat person.json | jq -r .name

出力結果は以下の通り

taro


「name.first」 と 「name.last」 を結合した結果を出力してみます

cat person.json | jq .name | jq -r '.first + " " + .last'

出力結果は以下の通り

taro yamada


少し複雑めなJSONを操作

$ curl -s "http://geoapi.heartrails.com/api/json?method=searchByPostal&postal=1060032" | jq . 
{
  "response": {
    "location": [
      {
        "city": "港区",
        "city_kana": "みなとく",
        "town": "六本木一丁目",
        "town_kana": "ろっぽんぎ1ちょうめ",
        "x": "139.740991",
        "y": "35.665082",
        "prefecture": "東京都",
        "postal": "1060032"
      },
      {
        "city": "港区",
        "city_kana": "みなとく",
        "town": "六本木二丁目",
        "town_kana": "ろっぽんぎ2ちょうめ",
        "x": "139.737087",
        "y": "35.666974",
        "prefecture": "東京都",
        "postal": "1060032"
      },
      {
        "city": "港区",
        "city_kana": "みなとく",
        "town": "六本木三丁目",
        "town_kana": "ろっぽんぎ3ちょうめ",
        "x": "139.735452",
        "y": "35.663977",
        "prefecture": "東京都",
        "postal": "1060032"
      },
      {
        "city": "港区",
        "city_kana": "みなとく",
        "ç": "六本木四丁目",
        "town_kana": "ろっぽんぎ4ちょうめ",
        "x": "139.733837",
        "y": "35.665489",
        "prefecture": "東京都",
        "postal": "1060032"
      },
      {
        "city": "港区",
        "city_kana": "みなとく",
        "town": "六本木五丁目",
        "town_kana": "ろっぽんぎ5ちょうめ",
        "x": "139.735248",
        "y": "35.658358",
        "prefecture": "東京都",
        "postal": "1060032"
      },
      {
        "city": "港区",
        "city_kana": "みなとく",
        "town": "六本木六丁目",
        "town_kana": "ろっぽんぎ6ちょうめ",
        "x": "139.729932",
        "y": "35.659856",
        "prefecture": "東京都",
        "postal": "1060032"
      },
      {
        "city": "港区",
        "city_kana": "みなとく",
        "town": "六本木七丁目",
        "town_kana": "ろっぽんぎ7ちょうめ",
        "x": "139.726863",
        "y": "35.664751",
        "prefecture": "東京都",
        "postal": "1060032"
      }
    ]
  }
}


上記の中から「town」だけ列挙してみる

curl -s "http://geoapi.heartrails.com/api/json?method=searchByPostal&postal=1060032" | jq .response.location[].town
"六本木一丁目"
"六本木二丁目"
"六本木三丁目"
"六本木四丁目"
"六本木五丁目"
"六本木六丁目"
"六本木七丁目"


当初取得した値の中から 「town」,「x」, 「y」だけのJSONにしたい場合は map 使うと可能です

$ curl -s "http://geoapi.heartrails.com/api/json?method=searchByPostal&postal=1060032" | jq '.response.location | map({town: .town, x: .x, y: .y})'
[
  {
    "town": "六本木一丁目",
    "x": "139.740991",
    "y": "35.665082"
  },
  {
    "town": "六本木二丁目",
    "x": "139.737087",
    "y": "35.666974"
  },
  {
    "town": "六本木三丁目",
    "x": "139.735452",
    "y": "35.663977"
  },
  {
    "town": "六本木四丁目",
    "x": "139.733837",
    "y": "35.665489"
  },
  {
    "town": "六本木五丁目",
    "x": "139.735248",
    "y": "35.658358"
  },
  {
    "town": "六本木六丁目",
    "x": "139.729932",
    "y": "35.659856"
  },
  {
    "town": "六本木七丁目",
    "x": "139.726863",
    "y": "35.664751"
  }
]


まとめ

まとめるほど触ってませんが、まだまだ相当な機能やオプションをjqコマンドは備えています。
もっと使いこなせるようになりたいです。



関連エントリ

HomeBrew で jqをインストールしてみる

jsonデータを整形・絞り込みできるjqコマンドをmacOSにインストールしたかったので、HomeBrewでインストールしてみました。


インストール

以下を実行します。

$ brew install jq


実行時のログは以下の様な感じでした

$ brew install jq
==> Installing dependencies for git: pcre2
==> Installing git dependency: pcre2
==> Downloading https://homebrew.bintray.com/bottles/pcre2-10.32.mojave.bottle.tar.gz
Updating Homebrew...
######################################################################## 100.0%
==> Pouring pcre2-10.32.mojave.bottle.tar.gz
🍺  /usr/local/Cellar/pcre2/10.32: 224 files, 5.5MB
==> Installing git
==> Downloading https://homebrew.bintray.com/bottles/git-2.19.2.mojave.bottle.tar.gz
######################################################################## 100.0%
==> Pouring git-2.19.2.mojave.bottle.tar.gz
==> Caveats
Bash completion has been installed to:
  /usr/local/etc/bash_completion.d

zsh completions and functions have been installed to:
  /usr/local/share/zsh/site-functions

Emacs Lisp files have been installed to:
  /usr/local/share/emacs/site-lisp/git
==> Summary
🍺  /usr/local/Cellar/git/2.19.2: 1,520 files, 40.1MB
==> Caveats
==> git
Bash completion has been installed to:
  /usr/local/etc/bash_completion.d

zsh completions and functions have been installed to:
  /usr/local/share/zsh/site-functions

Emacs Lisp files have been installed to:
  /usr/local/share/emacs/site-lisp/git
xcrun: error: invalid active developer path (/Library/Developer/CommandLineTools), missing xcrun at: /Library/Developer/CommandLineTools/usr/bin/xcrun
Warning: Aleth (formerly cpp-ethereum) has been removed from Homebrew. Please install binary releases from https://github.com/ethereum/aleth/releases.
==> Auto-updated Homebrew!
Updated 3 taps (homebrew/core, homebrew/cask and ethereum/ethereum).
==> New Formulae

(中略)

==> Renamed Formulae
ark -> velero                              gnatsd -> nats-server                      todolist -> ultralist
confluent-oss -> confluent-platform        php72 -> php@7.2                           transmission -> transmission-cli
gloo-ctl -> glooctl                        resin-cli -> balena-cli
==> Deleted Formulae
apple-gcc42          gdnsd                js-test-driver       pdftoedn             rock                 tomcat@6
at-spi2-atk          gnome-doc-utils      ld64                 percona-server@5.6   ruby@1.8             typesafe-activator
at-spi2-core         go@1.4               libggz               php@5.6              ruby@2.3             varnish@4
cctools              go@1.8               libguess             php@7.0              safe                 whirr
cctools-headers      gradle@2.14          liblastfm            plan9port            scala@2.10           xmoto
compose2kube         gtk-engines          libutf               pldebugger           smlnj                zxing-cpp
cputhrottle          gtk-murrine-engine   lysp                 protobuf@2.5         solr@5.5
dsd                  guile@2.0            minisat              protobuf@2.6         solr@6.6
erlang@18            gv                   monax                pyexiv2              swig@3.04
ffmbc                hyper                node@6               rlvm                 tmux-cssh

==> Installing dependencies for jq: oniguruma
==> Installing jq dependency: oniguruma
==> Downloading https://homebrew.bintray.com/bottles/oniguruma-6.9.3.mojave.bottle.tar.gz
==> Downloading from https://akamai.bintray.com/f0/f00f8c6f8afd8875fed685a9190cb0c5e9b5ceef58ef1e489fb17a42bddc9672?__gda__=exp=1
######################################################################## 100.0%
==> Pouring oniguruma-6.9.3.mojave.bottle.tar.gz
🍺  /usr/local/Cellar/oniguruma/6.9.3: 17 files, 1.3MB
==> Installing jq
==> Downloading https://homebrew.bintray.com/bottles/jq-1.6.mojave.bottle.1.tar.gz
==> Downloading from https://akamai.bintray.com/71/71f0e76c5b22e5088426c971d5e795fe67abee7af6c2c4ae0cf4c0eb98ed21ff?__gda__=exp=1
######################################################################## 100.0%
==> Pouring jq-1.6.mojave.bottle.1.tar.gz
🍺  /usr/local/Cellar/jq/1.6: 18 files, 1MB
==> `brew cleanup` has not been run in 30 days, running now...
Removing: /Users/yuki/Library/Caches/Homebrew/git--2.19.2.mojave.bottle.tar.gz... (15.2MB)
Removing: /Users/yuki/Library/Caches/Homebrew/go--1.11.1.sierra.bottle.tar.gz... (140.5MB)
Removing: /Users/yuki/Library/Caches/Homebrew/icu4c--62.1.sierra.bottle.tar.gz... (25.4MB)
Removing: /Users/yuki/Library/Caches/Homebrew/jenv--0.4.4.tar.gz... (18KB)
Removing: /Users/yuki/Library/Caches/Homebrew/nkf--2.1.4.sierra.bottle.tar.gz... (157.8KB)
Removing: /Users/yuki/Library/Caches/Homebrew/node--11.2.0.sierra.bottle.tar.gz... (13.3MB)
Removing: /Users/yuki/Library/Caches/Homebrew/pcre2--10.32.mojave.bottle.tar.gz... (1.8MB)
Removing: /Users/yuki/Library/Caches/Homebrew/tree--1.8.0.sierra.bottle.tar.gz... (50.5KB)
Removing: /Users/yuki/Library/Caches/Homebrew/yarn--1.12.3.tar.gz... (1.1MB)
Removing: /Users/yuki/Library/Caches/Homebrew/z3-4.7.1.sierra.bottle.tar.gz... (27.3MB)
Removing: /Users/yuki/Library/Caches/Homebrew/openssl-1.0.2o_2.sierra.bottle.tar.gz... (3.7MB)
Removing: /Users/yuki/Library/Caches/Homebrew/gdbm-1.14.1_1.sierra.bottle.tar.gz... (182.5KB)
Removing: /Users/yuki/Library/Caches/Homebrew/gettext-0.19.8.1.sierra.bottle.tar.gz... (7.8MB)
Removing: /Users/yuki/Library/Caches/Homebrew/readline-7.0.3_1.sierra.bottle.tar.gz... (497.3KB)
Removing: /Users/yuki/Library/Caches/Homebrew/python@2-2.7.15.sierra.bottle.tar.gz... (18.3MB)
Removing: /Users/yuki/Library/Caches/Homebrew/wget-1.19.5.sierra.bottle.tar.gz... (1.3MB)
Removing: /Users/yuki/Library/Caches/Homebrew/cmake-3.11.3.sierra.bottle.tar.gz... (11.8MB)
Removing: /Users/yuki/Library/Caches/Homebrew/boost-1.67.0_1.sierra.bottle.tar.gz... (86.6MB)
Removing: /Users/yuki/Library/Caches/Homebrew/linkage.db... (48KB)
Removing: /Users/yuki/Library/Caches/Homebrew/sqlite-3.24.0.sierra.bottle.tar.gz... (1.7MB)
Removing: /Users/yuki/Library/Caches/Homebrew/nodebrew-1.0.0.tar.gz... (26.3KB)
Removing: /Users/yuki/Library/Caches/Homebrew/solidity-0.4.24.tar.gz... (1.1MB)
Removing: /Users/yuki/Library/Caches/Homebrew/portable-ruby-2.3.3_2.leopard_64.bottle.tar.gz... (12.4MB)
Removing: /Users/yuki/Library/Caches/Homebrew/ethereum-1.8.10.sierra.bottle.tar.gz... (64.2MB)
Removing: /Users/yuki/Library/Caches/Homebrew/portable-ruby-2.3.7.leopard_64.bottle.tar.gz... (12.4MB)
Removing: /Users/yuki/Library/Caches/Homebrew/go-1.10.3.sierra.bottle.tar.gz... (102.6MB)
Removing: /Users/yuki/Library/Caches/Homebrew/openssl-1.0.2o_1.sierra.bottle.tar.gz... (3.7MB)
Removing: /Users/yuki/Library/Caches/Homebrew/watch-3.3.15.sierra.bottle.tar.gz... (30.9KB)
Removing: /Users/yuki/Library/Caches/Homebrew/nmap-7.70.sierra.bottle.tar.gz... (7.1MB)
Removing: /Users/yuki/Library/Caches/Homebrew/libunistring-0.9.10.sierra.bottle.tar.gz... (1.4MB)
Removing: /Users/yuki/Library/Caches/Homebrew/ccache-3.4.2.sierra.bottle.tar.gz... (89.8KB)
Removing: /Users/yuki/Library/Caches/Homebrew/libidn2-2.0.5.sierra.bottle.tar.gz... (217.4KB)
Removing: /Users/yuki/Library/Caches/Homebrew/Cask/adoptopenjdk--11.0.1,13.tar.gz... (181.1MB)
Removing: /Users/yuki/Library/Logs/Homebrew/z3... (64B)
Removing: /Users/yuki/Library/Logs/Homebrew/tree... (64B)
Removing: /Users/yuki/Library/Logs/Homebrew/wget... (64B)
Removing: /Users/yuki/Library/Logs/Homebrew/libidn2... (64B)
Removing: /Users/yuki/Library/Logs/Homebrew/ccache... (64B)
Removing: /Users/yuki/Library/Logs/Homebrew/python@2... (3 files, 123.9KB)
Removing: /Users/yuki/Library/Logs/Homebrew/go... (64B)
Removing: /Users/yuki/Library/Logs/Homebrew/gdbm... (64B)
Removing: /Users/yuki/Library/Logs/Homebrew/cmake... (64B)
Removing: /Users/yuki/Library/Logs/Homebrew/boost... (64B)
Removing: /Users/yuki/Library/Logs/Homebrew/libunistring... (64B)
Removing: /Users/yuki/Library/Logs/Homebrew/icu4c... (64B)
Removing: /Users/yuki/Library/Logs/Homebrew/nkf... (64B)
Removing: /Users/yuki/Library/Logs/Homebrew/readline... (64B)
Removing: /Users/yuki/Library/Logs/Homebrew/sqlite... (64B)
Removing: /Users/yuki/Library/Logs/Homebrew/yarn... (100B)
Removing: /Users/yuki/Library/Logs/Homebrew/solidity... (6 files, 563.5KB)
Removing: /Users/yuki/Library/Logs/Homebrew/nodebrew... (104B)
Removing: /Users/yuki/Library/Logs/Homebrew/gettext... (64B)
Removing: /Users/yuki/Library/Logs/Homebrew/watch... (64B)
Removing: /Users/yuki/Library/Logs/Homebrew/node... (64B)
Removing: /Users/yuki/Library/Logs/Homebrew/jenv... (100B)
Removing: /Users/yuki/Library/Logs/Homebrew/nmap... (64B)
Removing: /Users/yuki/Library/Logs/Homebrew/openssl... (64B)
Removing: /Users/yuki/Library/Logs/Homebrew/ethereum... (64B)
Pruned 0 symbolic links and 2 directories from /usr/local


サンプル的に試してみる

例えば、以下の郵便番号による住所検索 API

curl で実行して、それを jq コマンドにパイプで渡してみます(queryパラメータの postal に指定している値が郵便番号)

curl "http://geoapi.heartrails.com/api/json?method=searchByPostal&postal=1060032" | jq


実行してみると以下のようになります。

$ curl "http://geoapi.heartrails.com/api/json?method=searchByPostal&postal=1060032" | jq
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100  1882    0  1882    0     0  59882      0 --:--:-- --:--:-- --:--:-- 60709
{
  "response": {
    "location": [
      {
        "city": "港区",
        "city_kana": "みなとく",
        "town": "六本木一丁目",
        "town_kana": "ろっぽんぎ1ちょうめ",
        "x": "139.740991",
        "y": "35.665082",
        "prefecture": "東京都",
        "postal": "1060032"
      },
      {
        "city": "港区",
        "city_kana": "みなとく",
        "town": "六本木二丁目",
        "town_kana": "ろっぽんぎ2ちょうめ",
        "x": "139.737087",
        "y": "35.666974",
        "prefecture": "東京都",
        "postal": "1060032"
      },
      {
        "city": "港区",
        "city_kana": "みなとく",
        "town": "六本木三丁目",
        "town_kana": "ろっぽんぎ3ちょうめ",
        "x": "139.735452",
        "y": "35.663977",
        "prefecture": "東京都",
        "postal": "1060032"
      },
      {
        "city": "港区",
        "city_kana": "みなとく",
        "town": "六本木四丁目",
        "town_kana": "ろっぽんぎ4ちょうめ",
        "x": "139.733837",
        "y": "35.665489",
        "prefecture": "東京都",
        "postal": "1060032"
      },
      {
        "city": "港区",
        "city_kana": "みなとく",
        "town": "六本木五丁目",
        "town_kana": "ろっぽんぎ5ちょうめ",
        "x": "139.735248",
        "y": "35.658358",
        "prefecture": "東京都",
        "postal": "1060032"
      },
      {
        "city": "港区",
        "city_kana": "みなとく",
        "town": "六本木六丁目",
        "town_kana": "ろっぽんぎ6ちょうめ",
        "x": "139.729932",
        "y": "35.659856",
        "prefecture": "東京都",
        "postal": "1060032"
      },
      {
        "city": "港区",
        "city_kana": "みなとく",
        "town": "六本木七丁目",
        "town_kana": "ろっぽんぎ7ちょうめ",
        "x": "139.726863",
        "y": "35.664751",
        "prefecture": "東京都",
        "postal": "1060032"
      }
    ]
  }
}

これだけで、JSONを見やすく整形した状態で出力してくれます


jq コマンドは、かなり色々とできるはずなので、使い方調べながらもっと使いこなせるようになりたいです。

codility - Arrays OddOccurrencesInArray

codility の OddOccurrencesInArrayを解いてみます。

問題の概要

  • インプット(引数)
    • 整数の配列
      • 配列の要素の整数は奇数
      • 配列の要素数は奇数個
      • 要素の値同士のペアができるが、1要素だけペアができない値が含まれている
      • 例:[9, 3, 9, 3, 9, 7, 9]
  • アウトプット(戻り値)
    • ペアができなかった要素の値
    • 例にあげた引数の場合、7 がアウトプットになります


解答

以下の方針でやってます。

  • まず配列をソートする。
  • ソートされているので、基本的に偶数番目と奇数番目の要素の値は一致する(ペアになる)はず。
  • ペアにならなかったら、その偶数番目の値が1個だけしか存在しないものということになるので、それがそのまま答え
  • 最後の最後まで進んだら最終要素が答えとなる
import java.util.Arrays;

class Solution {

    public int solution(int[] array) {
        Arrays.sort(array);

        for (int i = 0; i < array.length - 1;){
            if (array[i] != array[i + 1]) {
                return array[i];
            }
            i = i + 2;
        }
        return array[array.length - 1];
    }
}


これでやってみると結果は以下の通りで、問題ないようです。

f:id:nini_y:20190830204710p:plain

ISBN-13を求める

ISBN や ISBN-13 の説明はWikipediaにお任せするとして

「接頭記号」 + 「グループ記号」 + 「出版者記号」 + 「書名記号」 (ハイフン除くと12桁)の値から
末尾に付与するチェックディジットまで含めたISBN-13の値を求めたい場合、
以下のようなメソッドを用意すればできます。

public final class ISBN13 {

    public static String generate(String src) {
        final String str12 = src.replace("-", "");

        if (str12.length() != 12) {
            throw new IllegalArgumentException(str12);
        }

        return str12 + checkDigit(str12);
    }

    private static int checkDigit(String str12) {
        // ウェイトの1および3
        final int[] weights = { 1, 3 };

        int sum = 0;
        for (int i = 0; i < str12.length(); i++) {
            // sum += Integer.valueOf(str12.charAt(i)) * weights[i & 1]   <- NG!!!
            sum += Character.getNumericValue(str12.charAt(i)) * weights[i & 1];
        }
        final int r = 10 - (sum % 10);
        if (r == 10) {
            return 0;
        }
        return r;
    }


試してみる

public static void main(String[] args) {
    System.out.println(ISBN13.generate("978479733720"));
    System.out.println(ISBN13.generate("978-4-7973-3720"));
    System.out.println(ISBN13.generate("978410109205"));
    System.out.println(ISBN13.generate("978030640615"));
    System.out.println(ISBN13.generate("978-0-30-640615"));
    System.out.println(ISBN13.generate("978316148410"));
}


出力結果

9784797337204
9784797337204
9784101092058
9780306406157
9780306406157
9783161484100


分かりにくいですが、ISBN-13を意味する13桁表現の値が取得できます。

2の累乗の加算で表現できる値の分解

  • N個の要素の整数を持つ配列Aを与えられる(N >= 1)
  • binarian(A) = pow2(A[0]) + pow2(A[1]) + ... + pow2(A[M-1])
  • 上記の式で求めた結果と同じ値となるための 2の累乗n の加算 の最小の組み合わせ数を求める

例:

A[0]=1
A[1]=5
A[2]=4
A[3]=4

binarian(A) = pow2(A[0]) + pow2(A[1]) + pow2(A[2]) + pow2(A[3])
            = pow2(1) + pow2(5) + pow2(4) + pow2(4)
            = 2 + 32 + 16 + 16
            = 66


この 66 を表現する2の累乗の和の表現は以下になります(2の1乗 + 2の6乗 で表現できます)

66 = pow2(1) + pow2(6)

結果として答えは 2です。(2の1乗 と 2の6乗 の2つの組み合わせで表現できるので)


これを求めるプログラムは以下になります

import java.util.Map;
import java.util.TreeMap;

public class Solution {

    public static int solution(int[] A) {
        TreeMap<Integer, Integer> reiterations = new TreeMap<>();
        int lengthSolution = 0;
        for (int i = 0; i < A.length; i++) {
            int item = A[i];
            if (reiterations.containsKey(item)) {
                reiterations.put(item, reiterations.get(item) + 1);
            } else {
                reiterations.put(item, 1);
            }
        }
        reiterations = factorize(reiterations, reiterations.firstEntry());

        for (Map.Entry<Integer,Integer> entry : reiterations.entrySet()) {
            int value = entry.getValue();
            if (value == 1) {
                lengthSolution++;
            }
        }

        return lengthSolution;
    }

    public static TreeMap<Integer, Integer> factorize(TreeMap<Integer, Integer> remaining,
                                                      Map.Entry<Integer, Integer> entry) {
        int key = entry.getKey();
        int value = entry.getValue();

        if (value > 1 && value % 2 == 0) {
            if (remaining.containsKey(key + 1)) {
                remaining.put(key + 1, remaining.get(key + 1) + value / 2);
                remaining.put(key, 0);
            } else {
                remaining.put(key + 1, value / 2);
                remaining.put(key, 0);
            }
        } else if (value > 1 && value % 2 == 1) {
            if (remaining.containsKey(key + 1)) {
                remaining.put(key + 1, remaining.get(key + 1) + (value - 1) / 2);
                remaining.put(key, 1);
            } else {
                remaining.put(key + 1, (value - 1) / 2);
                remaining.put(key, 1);
            }
        }

        while (remaining.higherEntry(key) != null) {
            if (remaining.higherEntry(key).getValue() > 1) {
                factorize(remaining, remaining.higherEntry(key));
            }
            key++;
        }
        return remaining;
    }
}


念のために [1, 5, 4, 4]の配列を渡して試してみると

public class Main {

    public static void main(String[] args) {
        int[] test = {1, 5, 4, 4};
        System.out.println(Solution.solution(test));
    }
}


出力結果

2

予定通りの結果が得られています