TES Blog

株式会社テクニカルエンジニアリングサポートに所属する社員が、自身が携わるテクノロジーやイベントに関する情報を発信しています。

RubyとGoで、DBFファイルを扱う

既存システムのリプレイス対応にて、元々機能として提供されていたDBFファイルの入出力処理をRailsで実装することになったので、ノウハウの書き起こしです。
(ファイルフォーマットとしてはレガシーな部類なのかと思いますので、誰得な内容です… 😅)

はじめに説明しておきますが、Ruby単体でDBFファイルの取り扱いは完全にサポートされていないので、処理を部分的にGoへ委譲しました。ご参考になれば幸いです。

使用するGem

  • dbf
    • ファイルの読み込み用
  • shp
    • ファイルへの出力用

DBFファイルの読み込み

DBF::Table.new を使用して対象のファイルを読み込みます。
結果は2次元配列に格納されるので、取得したい値のインデックス位置を特定して加工するのがベターかと思います。

DBF_COLUMN_INDEX = {
  id: 0,
  name: 1,
  email: 2
}.freeze
  
records = DBF::Table.new('/path/to/import.dbf', nil, 'Shift_JIS').map do |row|
  {
    id: row[DBF_COLUMN_INDEX[:id]],
    name: row[DBF_COLUMN_INDEX[:name]],
    email: row[DBF_COLUMN_INDEX[:email]]
  }
end

DBFファイルへの書き込み

SHP::DBF.create を使用してファイルを生成します。
生成したファイルを対象に、カラム情報と紐づくデータを追加していきます。 fields 変数の name で指定している値については、読み取りを行う環境に合わせてエンコードが必要です。(今回はWindowが対象なので CP932

dbf_file = SHP::DBF.create(file_path)

fields = [
  { name: 'ID'.encode('CP932'), type: DbfFileMaker::COLUMN_STRING, width: 254, decimals: 0 },
  { name: '名前'.encode('CP932'), type: DbfFileMaker::COLUMN_STRING, width: 254, decimals: 0 },
  { name: '年齢'.encode('CP932'), type: DbfFileMaker::COLUMN_INTEGER, width: 3, decimals: 0 }
]
fields.each do |field|
  dbf_file.add_field(field[:name], field[:type], field[:width], field[:decimals])
end

data = [
  [1, 'test1', 12],
  [2, 'test2', 22],
  [3, 'test3', 32]
]

data.each_with_index do |details, row_index|
  details.each_with_index do |value, column_index|
    if value.nil?
      dbf_file.write_string_attribute(record_no, index, '')
      next
    end

    case fields[index][:type]
    when COLUMN_STRING
      dbf_file.write_string_attribute(row_index, column_index, value)
    when COLUMN_INTEGER
      dbf_file.write_integer_attribute(row_index, column_index, value)
    end
  end
end

dbf_file.close

が、ここで落とし穴なのですが、 shp は日付型のサポートはしていないため、RailsがDBから取得した値をそっくりそのまま出力することができませんでした。 他のDBFファイルを扱うGemを探してみましたが見つからず…。 ここで冒頭に書いたとおり、他言語であるGoを頼りにしました。

処理の流れを整理

すべてをGoに任せると構成がややこしくなるため、以下の通りで役割を整備しました。

  • Ruby on Rails
    • DBからのデータ取得
    • 抽出したレコードをCSVへ出力
    • Goの実行ファイル呼び出し(引数として保存先のパス文字列を渡す)
  • Go
    • CSVファイルのパスを受け取り、DBFファイルを出力

使用ライブラリ

DBFファイルへの書き込み(Go版)

(色々エラーハンドリングが足りていないですが…) godbf.New を使用してファイルの実体を作成し、カラム情報の追加と、CSVから取得したレコードを読み込んだ行を追加しています。 最後に SaveFile を呼び出して指定のパスへ出力します。

package main
import (
    "fmt"
    "flag"
    "github.com/LindsayBradford/go-dbf/godbf"
    "encoding/csv"
    "os"
)

func main(){
        // パスの受け取り
        var (
                csv_path = flag.String("csv_path", "default", "string flag")
                out_path = flag.String("out_path", "default", "string flag")
        )
        flag.Parse()
        fmt.Println(*csv_path)
        fmt.Println(*out_path)

        dbfTable := godbf.New("Shift_JIS")

        dbfTable.AddTextField("ID", 254)
        // 日付が使える!       
        dbfTable.AddDateField("日付")
        dbfTable.AddNumberField("年齢", 3, 0)
        dbfTable.AddTextField("名前", 254)

        file, err := os.Open(*csv_path)
        if err != nil {
                panic(err)
        }
        defer file.Close()

        reader := csv.NewReader(file)
        var line []string
        var currentRow int = 0
        for {
                line, err = reader.Read()
                if err != nil {
                        break
                }

                dbfTable.AddNewRecord()
                dbfTable.SetFieldValue(currentRow, 0, line[0]) // ID
                dbfTable.SetFieldValue(currentRow, 1, line[1]) // 日付
                dbfTable.SetFieldValue(currentRow, 2, line[2]) // 年齢
                dbfTable.SetFieldValue(currentRow, 3, line[3]) // 名前
                currentRow = currentRow + 1
        }

        dbfTable.SaveFile(*out_path)
}

最終的にビルドしたGoファイルをRuby側で呼び出してあげれば処理は完了です。

Eloquent の chunk でも skip がしたい!

はじめに

TES に入社して6年目となる、橋本に勤務する橋本です。

最近は Laravel で開発するのが楽しくて、会社でも広まってきて嬉しいです。

今回は、Laravel に同梱されている Eloquent ORM の chunk メソッドで skip(offset)を指定する方法を紹介します。

続きを読む

C# と Bluetooth Low Energy(BLE)の連携

業務にてWindowsフォームアプリケーションとBluetooth Low Energy(BLE)の連携する機会があったので、備忘の意味合いを込めて本記事を作成しました。
サンプル自体はMicrosoftが提供しているコードがあるので、そちらも参考にすると良いと思います。
https://github.com/microsoft/Windows-universal-samples/tree/master/Samples/DeviceEnumerationAndPairing/cs

環境

  • Windows 10
  • Visual Studio 2017

デバイスの検索

アプリケーションが動作するPCの周囲に存在するデバイスを検索する処理です。 DeviceInformation.CreateWatcher() メソッドに渡すセレクタの要素に、抽出したいデバイス種別のIDを渡すことで検索対象の絞り込みが可能です。

// 検索条件を設定
// Bluetooth: e0cbf06c-cd8b-4647-bb8a-263b43f0f974
// BLE:       bb7bb05e-5972-42b5-94fc-76eaa7084d49
string selector = "(System.Devices.Aep.ProtocolId:=\"{bb7bb05e-5972-42b5-94fc-76eaa7084d49}\")";

// private DeviceWatcher Watcher { get; set; }
this.Watcher = DeviceInformation.CreateWatcher(selector, null, DeviceInformationKind.AssociationEndpoint);

検知したデバイス情報を画面に表示する等の要件がある場合、 デバイスを検知した時・デバイスの検索が完了した時のイベントをそれぞれ定義しておくと良いですね。

// デバイス検知時のイベント
this.Watcher.Added += this.WatcherDeviceAdded;

// 検索完了時のイベント
this.Watcher.EnumerationCompleted += this.DeviceWatcherEnumerationCompleted;

// デバイス検索の開始
this.Watcher.Start();

デバイス検知時のイベント

private void WatcherDeviceAdded(DeviceWatcher sender, DeviceInformation deviceInfo)
{
    if (deviceInfo.Name != string.Empty)
    {
        // デバイス情報をListに保存
        this.devices.Add(deviceInfo);
    }
}

検索完了時のイベント

private void DeviceWatcherEnumerationCompleted(DeviceWatcher sender, object args)
{
    // メインスレッド外なので、画面に出力する場合はDelegateを使用する
    this.Watcher.Stop();
    // private delegate void BindDataSource();
    var bindData = new BindDataSource(this.DataGridViewBindDataSource);

    try
    {
        if (!this.dgvDevices.IsDisposed)
        {
            this.dgvDevices.Invoke(bindData);
        }
    }
    catch (InvalidOperationException ex)
    {
        Console.WriteLine(ex.Message);
    }
}

ペアリング

検索して取得したデバイスとペアリングする場合、 PairAsync() を呼び出します。
処理の前後に PairingRequested に対してイベントを登録していますが、私が対応したデバイスではカスタムペアリングを使用した場合にペアリングが可能な状態でしたので、今回は Custom.PairAsync を使用しました。

var deviceInfo = this.devices[selectedIndex];
deviceInfo.Pairing.Custom.PairingRequested += this.PairingRequestedHandler;
DevicePairingResult result = await deviceInfo.Pairing.Custom.PairAsync(
    DevicePairingKinds.ConfirmOnly, DevicePairingProtectionLevel.None);
deviceInfo.Pairing.Custom.PairingRequested -= this.PairingRequestedHandler;

if (result.Status == DevicePairingResultStatus.Paired || result.Status == DevicePairingResultStatus.AlreadyPaired)
{
    Console.WriteLine("ペアリングに成功しました。");
}
else
{
    Console.WriteLine("ペアリングに失敗しました。");
}

ペアリング時のイベント

private void PairingRequestedHandler(DeviceInformationCustomPairing sender, DevicePairingRequestedEventArgs args)
{
    switch (args.PairingKind)
    {
        case DevicePairingKinds.ConfirmOnly:
            // PIN入力が不要なケース
            args.Accept();
            break;

        case DevicePairingKinds.ProvidePin:
            // PIN入力が必要なケース
            var collectPinDeferral = args.GetDeferral();
            args.Accept("000000");
            collectPinDeferral.Complete();
            break;
    }
}

ペアリング成功後、対象のデバイスとの通信を行うため、Characteristicとの疎通を行います。

var deviceInfo = this.devices[selectedIndex];
var device = await BluetoothLEDevice.FromIdAsync(deviceInfo.Id);

// デバイスのUUID
Guid vendorSpecificServiceUuid = new Guid("xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxx1");
Guid characteristic1Uuid = new Guid("xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxx2");
Guid characteristic2Uuid = new Guid("xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxx3");

// サービスの検索
var result = await device.GetGattServicesForUuidAsync(vendorSpecificServiceUuid);
if (result.Status != GattCommunicationStatus.Success)
{
    return;
}

var services = result.Services;

// 書き込み用のCharacteristic
var characteristicsWriteWithoutResponse = await services[0].GetCharacteristicsForUuidAsync(characteristic1Uuid);
this.WriteWithoutResponseCharacteristic = characteristicsWriteWithoutResponse.Characteristics[0];

// レスポンス用のCharacteristic
var characteristicsWriteAndNotify = await services[0].GetCharacteristicsForUuidAsync(characteristic2Uuid);
this.WriteAndNotifyCharacteristic = characteristicsWriteAndNotify.Characteristics[0];

// Notifyの通知
var status = await this.WriteAndNotifyCharacteristic.WriteClientCharacteristicConfigurationDescriptorAsync(GattClientCharacteristicConfigurationDescriptorValue.Notify);
if (status != GattCommunicationStatus.Success)
{
    return;
}

// BLE側で値の変更が発生した場合に実行するイベント
this.WriteAndNotifyCharacteristic.ValueChanged += this.Characteristic_ValueChanged;

// 電文を作成してBLEへ書き込む
var messageData = new byte[] { 0x01, 0x02 };
var writer = new DataWriter();
writer.ByteOrder = ByteOrder.BigEndian;
writer.WriteBytes(messageData);
var writeResult = await this.WriteWithoutResponseCharacteristic.WriteValueAsync(writer.DetachBuffer());

if (writeResult != GattCommunicationStatus.Success)
{
    throw new Exception();
}

デバイス側の値に変更が発生したことを検知するイベント

private void Characteristic_ValueChanged(GattCharacteristic sender, GattValueChangedEventArgs args)
{
    // 値を読み込み
    var reader = DataReader.FromBuffer(args.CharacteristicValue);
    byte[] input = new byte[reader.UnconsumedBufferLength];
    reader.ReadBytes(input);
    
    // 画面への出力
}

実装前の前提知識が不足していましたが、なんとか疎通までこぎつけることができました 🎉

RubyWorld Conference 2019 に行ってきました!

はじめに 📌

TES に入社して5年目となる、橋本に勤務する橋本です。

橋本はこの季節とても寒いです(二重の意味で笑)。

普段は Web アプリケーションのエンジニアとして Ruby 、 PHP 、Vue.js などを使って開発しています。

前置きはさておき、2019年11月07日(木)と11月08日(金)に開催された「RubyWorld Conference 2019」に参加してきました!

開催してからだいぶ時間が空いてしまいましたが、講演を聞いての感想などをお伝えできればと思います。

いざ島根へ 📌

朝03時起きで羽田空港に向かいました…前日もお仕事だったのでだいぶしんどかったです…。

社長、常務、私というなんとも息苦しいメンバーで、いざ島根へ向かいます。

f:id:h-marei:20191225222717j:plain
羽田空港

出雲縁結び空港に到着しました!!

(良いご縁がありますように…🙏🏻)

f:id:h-marei:20191225223133j:plain
出雲縁結び空港

そして、「RubyWorld Conference 2019」の会場である島根県立産業交流会館 「くにびきメッセ」に到着しました!!

f:id:h-marei:20191225223519j:plain
くにびきメッセ内 会場受付前

RubyWorld Conference 2019 📌

ここからは、実際に講演を聞いての感想などをお話します。

大変恐縮ではあるのですが、全ての講演については書くことができないので、まつもとゆきひろ氏の講演をメインとして、その他の方々が行った講演に関してはまとめさせていただきます🙇🏻‍♂️

まつもとゆきひろ氏による基調講演

Ruby が世界に認知されるまで、10年かかったと言っていました。

その中で、成功するモノを作るのに大切なことを4つ語っていました。

それぞれについて、私の思ったことや考えを添えつつお話しようと思います。

1. Motivation

何かを作るのに、モチベーションってすごく大事ですよね。

それこそ、 Ruby のように世界に認知されるまでに10年もかかるものだと、何らかのモチベーションが無いと続きません。

個人開発で何か作る時には、作りたいものを作るのでモチベーションを保ちやすいですが、業務になるとそうもいきません。

その中でも、何かしらのモチベーションに繋がるようなことを少しでも見つけて、業務に関わっていきたいと思いました。

(↑ちょっと「小並感」感がありますが…🥴笑)

2. Target Audience

何かモノを作るのにあたって、誰がそのモノを利用するのか想定のユーザーを考える必要があります。

でなければ、作られたモノは何のために生まれてきたモノなのか分かりません。

基調講演の中で印象的だったのが、想定のユーザーを考える必要はあるが、空想のユーザーを作ってはならないことです。

たしかに、空想上で生まれたユーザーは空想でしかないので、現実にはいません。

モノを作っても現実のユーザーには使われないのです。

3. Community

基調講演を聞いていて、 Ruby の特に強い部分はこのコミュニティだと思いました。

オープンソースにしていたことによって、様々な人(世界各国)が集まり、結果としてコミュニティが広がったと話しています。

また、完璧なモノではない方が人が集まるとも言っていました。

たしかに、完璧なモノには手の出しようが無いですが、完璧ではないモノなら「あーじゃない、こーじゃない」とコミュニティの中で議論しながら活性化していきそうですよね。

GitHub で公開されてるオープンソースの Issue などで、よく議論している様子を見ますが、自分もその中に混じって議論したい気持ちになります。

こうした気持ちを持った人が集まり、コミュニティがどんどん広がっていくのかもしません。

ちなみに、コミュニティが広がったおかげで、まつもとゆきひろ氏は Ruby のソースに手を入れてなくて、今では mruby のコミットをしているそうです。

GitHub のコミット履歴を見てみるとその様子が伺えます。

github.com

4. Goal Seeking

何かをするにあたって、目標を決めることはとても大切なことだと思います。

目標がないと方向性が定まらず、設計が崩壊し、将来性が無くなってしまうかもしれません。

Ruby では、バージョンごとに以下のような目標を掲げていると話していました。

Ruby3

  • Performance
  • Concurrency
  • Analysis

Ruby4

  • Faster(より早く)
  • Smarter(より賢く)

※この時に世界初出し情報!

また、このようなことを言っていました。

  • モノを作るのに、予想の超えるモノを考える
  • 人の言うことを聞いてはならない
  • キーフーズ決めは、無謀なことをゴールに掲げる

つまり何が言いたかったのかというと、良いモノを作ろうとする時に、簡単にできるようなことを考えても良いモノはできないということです。

まさにそうだなと思っていて、難しいことにチャレンジする時はたくさんの勉強と様々な挑戦をすると思います。

たとえ目標を達成できなくても、そこまでの過程は必ずモノを良くしますし、自分の成長にも繋がります。

高い目標に向けて進み続けるためには、最初にお話したモチベーションに繋がってくるのかなと思いました。

全体を通して

特に印象に残った2つのテーマについてお話します。

Ruby を活用した IoT

全体を通して講演の数も多く印象に残ったのが、 Ruby を活用した IoT 絡みの講演です。

近年、 IoT を活用したシステムが増えてきていますが、その開発に Ruby を活用できるのは夢が広がります!

前述しましたが、まつもとゆきひろ氏も今は mruby に力を注いでいるようなので、今後のさらなる発展に期待がもてます。

mruby に関しては私自身一度も触ったことがないので、 Raspberry Pi で mruby を動かすくらいのことはしてみたい気持ちです。

我々は Ruby を知らない

どういうことかというと、Ruby を知らなくても Ruby で開発できてしまうということです。

というのも、 Web のアプリケーションを開発するときは基本的に Ruby On Rails で開発をすると思いますが、 フレームワークが優秀であるがために、 ある程度の書き方だけわかっていれば Ruby の知識がなくともアプリケーションを作ることできてしまいます。

私も Ruby On Rails で開発をする時は、 Ruby On Rails で開発をするという感覚の方が大きいです。

ある講演では、「Ruby プログラマ」ではなく「Rails プログラマ」だと表現していました。

質疑応答の際にも、

Q:Ruby だから改善できた内容はなかったのか?

という質問に対し、

A:Ruby というより、 Rails だからうまくいった点が多かった

という回答をしている方がいて、少し複雑な気持ちになったのを覚えています。

それが悪いことなのかと言われるとそうではないのかもしれませんが、言語の本質を知ることはプログラマとして大事なことだと思いますし、 Ruby でできることを把握できていればビジネスチャンスを逃さずに済むかもしれません。

Ruby には「Ruby技術者認定試験」があるので、うまく活用して Ruby の知識を深めたいですね!

(私は Silver しか持ってません…Gold 取らなきゃ…)

さいごに 📌

こういった大きなカンファレンスは初めて参加させていただきましたが、エンジニアとして多くの刺激を受けました。

またこういう機会があれば積極的に参加したいです。

今回の講演や発表資料は「RubyWorld Conference 2019」の公式サイトから見ることができます。

興味のある方は是非見てみてください!

2019.rubyworld-conf.org

余談 📌

レセプションの時に、まつもとゆきひろ氏と名刺交換をさせていただき、ツーショットまでさせていただきました!

残念ながらブログ掲載の許可を頂き損ねてしまったので、代わりに島根観光した時の写真でも貼っておきます(代わりとは)。

f:id:h-marei:20191225230827j:plain
出雲大社 しめ縄

f:id:h-marei:20191225231225j:plain
出雲大社 いなばのしろうさぎ

f:id:h-marei:20191225231420j:plain
松江城

f:id:h-marei:20191225231600j:plain
松江城 天守閣からの景色

【インターン】インフラ向け学生インターンシップを行いました!(2019年8月)

こんにちは! 更新がだいぶご無沙汰しております。

夏真っ盛りで毎日へとへとのもち太郎です。
毎日とっても暑いですね。
節約と称してエアコンなどをつけずにいると室内でも熱中症になる可能性もあるので皆さん「いのちだいじに」で行きましょう!

そんな猛暑の中、今年もインターンの時期がやってきました!

弊社のインターンについて

TES(弊社)では、ときどきインターン実習を行っております。
昨年のインターンの様子は以下の記事をご覧ください。

blog.tes.co.jp

私が実施させて頂いたのはインフラエンジニア志望向けの学生インターンシップ!
今回はそのインフラ向けインターンの様子を紹介させて頂きます👏(パチパチ)

f:id:manabkr:20190822124344p:plain

  • 弊社のインターンについて
  • インフラ向けの学生インターンシップの内容
    • インターン概要
    • 課題
    • シナリオ
    • つまり…
  • インターン生の様子
    • スケジュール・タスク分担
    • いざ実践!
    • 成果発表
      • 驚きのプレゼント!
  • 5日を通してみて
    • 後日談
続きを読む