システム・PC

pythonのdatetimeのtzinfoについての話

pythonのdatetimeはnativeとawareという二つのものが存在します。端的にいうと
nativeとは純粋に日付と時間を表したもので年月日時分秒を表したものということでよいかと思います。
2023/4/1から2023/8/24まで何秒か?なんて計算をするときに有効です。

一方awareはその地域における時間で例えば日本で9時、イギリスで1時、アメリカで何時のようなもので、
その地域の壁時計時間が何時かなどという処理をするときに有効です。

何も考えずにdatetimeオブジェクトを作成するとnativeになります。tzinfoにtimezoneを表すオブジェクトを引き渡して作成をすると
awareになります

from datetime import datetime, timezone
datetime(2023,10,1) #native
datetime(2023,10,1, tzinfo=timezone.utc) # aware 

nativeとawareはその用途や目的が違うので相互に演算をすることが出来ません。演算しようとするとエラーとなります。

さておよそPCの時間というのは所謂epochで管理されています。epochというのは
1970/1/1 00:00:00を起源としそこからの経過秒数です。これは閏秒を考慮していないUTCでtime()関数の戻り値でもあります。

この閏秒を考慮していないというのが非常に快適で今現在のepochに3600*24を足せば必ず明日の同時刻になります。
UTCとのずれに「分」がない地域では3600で割り切れる時間は必ず正時になります。
日本はUTCから9時間進んでいるので3600*24で割ったあまりが3600*9ならばそれは夜中の0時ということになります。

以下awareなdatetimeについて主に話を進めます。pythonのtimezoneはtzinfoというクラスで管理されているのですが
このクラスはインスタンス化できない仮想クラスで長らく実装されたクラスは標準で datetime.timezone.utcしかありませんでした。
そのためpytzというモジュールを利用したりしていたのですが、最近ではzoneinfoというモジュールが追加されこの辺り使いやすくなっています。

試しにAsia/Tokyoでawareな時刻を作成してそのepochを取得してみたりtimedeltaを足したりして時間を進めてみたりしてみます。
datetimeからepochを得るにはtimestamp()メソッドを使います。
datetimeにtimedelta(seconds=3600)を足すと1時間先の時刻になることがtimestamp()の結果からわかると思います。

from zoneinfo import ZoneInfo
from datetime import datetime, timedelta

TZ = ZoneInfo('Asia/Tokyo')
delta = timedelta(hours=1)
d = datetime(2023, 3, 26, 0, 0, 0, tzinfo=TZ)
print(d, d.timestamp())
d += delta
print(d, d.timestamp())
d += delta
print(d, d.timestamp())
d += delta
print(d, d.timestamp())

実行結果

2023-03-26 00:00:00+09:00 1679756400.0
2023-03-26 01:00:00+09:00 1679760000.0
2023-03-26 02:00:00+09:00 1679763600.0
2023-03-26 03:00:00+09:00 1679767200.0

同じプログラムをAsia/TokyoではなくEurope/Zurichで動かしてみます。すると以下のような結果が表示されます。

2023-03-26 00:00:00+01:00 1679785200.0
2023-03-26 01:00:00+01:00 1679788800.0
2023-03-26 02:00:00+01:00 1679792400.0
2023-03-26 03:00:00+02:00 1679792400.0


3/26の03:00から+02:00となっているのがわかるかと思います。これはチューリッヒでは2023/3/26の深夜に夏時間に移行するからです。
そしてさらに詳しくみてみるとepochが1679792400.0の1時間後がまた1679792400.0になっています。つまりepoch的に全く進んでないことがわかります。

サマールールへの移行のルールを詳しく書くと

2023年は3/26の01:59の次は02:00ではなく03:00(そして時差+02:00)です。つまり上記の出力結果である

2023-03-26 02:00:00+01:00 1679792400.0

は存在しない時間です。ですがpythonでは出力されてしまいます。どうもpythonはawareであっても純粋にhourに1を足してしまうようで、その結果epochが進まないという事態になっているようです。
これは結構トラップな気がします。

個人的にはtimedelta(hours=1)をawareな時刻であっても足したらawareの時刻において1時間先になってほしいところですがpythonはそうはなっていないようです。

なおこのプログラムを以下のように書き換えepochで管理をするように務めると

ts =  1679785200
d = datetime.fromtimestamp(ts, tz=TZ)
print(d, d.timestamp())

ts += 3600
d = datetime.fromtimestamp(ts, tz=TZ)
print(d, d.timestamp())

ts += 3600
d = datetime.fromtimestamp(ts, tz=TZ)
print(d, d.timestamp())

ts += 3600
d = datetime.fromtimestamp(ts, tz=TZ)
print(d, d.timestamp())

以下のように期待した出力が得られます

2023-03-26 00:00:00+01:00 1679785200.0
2023-03-26 01:00:00+01:00 1679788800.0
2023-03-26 03:00:00+02:00 1679792400.0
2023-03-26 04:00:00+02:00 1679796000.0

これらの実験は全てpython3.9での実行結果です。

rubyのhttpclientでletsencryptの証明書を保有するサイトに接続できなくなった

例えばこんなプログラムでlet's encryptで証明されたサイト(例えばこのブログ)にアクセスするとエラーになるようになってしまいました

require 'httpclient'
HTTPClient.new.get('https://ror.hj.to/ja/issei')
Traceback (most recent call last):
	17: from httpclienterror.rb:2:in `<main>'
	16: from /usr/local/lib/ruby/gems/2.7.0/gems/httpclient-2.8.3/lib/httpclient.rb:743:in `get'
	15: from /usr/local/lib/ruby/gems/2.7.0/gems/httpclient-2.8.3/lib/httpclient.rb:856:in `request'
	14: from /usr/local/lib/ruby/gems/2.7.0/gems/httpclient-2.8.3/lib/httpclient.rb:1014:in `do_request'
	13: from /usr/local/lib/ruby/gems/2.7.0/gems/httpclient-2.8.3/lib/httpclient.rb:1133:in `protect_keep_alive_disconnected'
	12: from /usr/local/lib/ruby/gems/2.7.0/gems/httpclient-2.8.3/lib/httpclient.rb:1019:in `block in do_request'
	11: from /usr/local/lib/ruby/gems/2.7.0/gems/httpclient-2.8.3/lib/httpclient.rb:1242:in `do_get_block'
	10: from /usr/local/lib/ruby/gems/2.7.0/gems/httpclient-2.8.3/lib/httpclient/session.rb:177:in `query'
	 9: from /usr/local/lib/ruby/gems/2.7.0/gems/httpclient-2.8.3/lib/httpclient/session.rb:511:in `query'
	 8: from /usr/local/lib/ruby/gems/2.7.0/gems/httpclient-2.8.3/lib/httpclient/session.rb:748:in `connect'
	 7: from /usr/local/Cellar/ruby@2.7/2.7.4/lib/ruby/2.7.0/timeout.rb:105:in `timeout'
	 6: from /usr/local/Cellar/ruby@2.7/2.7.4/lib/ruby/2.7.0/timeout.rb:95:in `block in timeout'
	 5: from /usr/local/lib/ruby/gems/2.7.0/gems/httpclient-2.8.3/lib/httpclient/session.rb:752:in `block in connect'
	 4: from /usr/local/lib/ruby/gems/2.7.0/gems/httpclient-2.8.3/lib/httpclient/ssl_socket.rb:26:in `create_socket'
	 3: from /usr/local/lib/ruby/gems/2.7.0/gems/httpclient-2.8.3/lib/httpclient/ssl_socket.rb:26:in `new'
	 2: from /usr/local/lib/ruby/gems/2.7.0/gems/httpclient-2.8.3/lib/httpclient/ssl_socket.rb:41:in `initialize'
	 1: from /usr/local/lib/ruby/gems/2.7.0/gems/httpclient-2.8.3/lib/httpclient/ssl_socket.rb:103:in `ssl_connect'
/usr/local/lib/ruby/gems/2.7.0/gems/httpclient-2.8.3/lib/httpclient/ssl_socket.rb:103:in `connect': SSL_connect returned=1 errno=0 state=error: certificate verify failed (certificate has expired) (OpenSSL::SSL::SSLError)


httpclientを使わずにnet/https等を利用すると問題なく接続できるようで、いろいろ調べていくとhttpclientが独自の信頼できる証明書を保持しているためということがわかりました。以下のようにOpenSSLのデフォルトの証明書を利用するようにすると動作します。

require 'httpclient'
HTTPClient.new{self.ssl_config.add_trust_ca(OpenSSL::X509::DEFAULT_CERT_FILE)}.get('https://ror.hj.to/ja/issei')

HTTPClientは2015年のバージョンを最後にアプデートされていないようなので別なgemを使うべきなのかもしれません。個人的にはhttp.rbがいい感じな気がします。

rails6.1.3 対応だん

それなりに大変だった。

  1. jQueryの完全削除
  2. WebPackerへの移行

webpackerはやめてwebpackを直接使う感じに完全移行せよみたいなブログも多々あるけど、経験的にrailsの道から外れるコードは将来地雷になるんだよなぁ・・というわけでwebpackerでとりあえず素直に実装。噂通りwebpackerの仕組みを理解するのに相当時間かかった。他にもテーマを設定できるようにしてたりしてたんだけど、そもそも自分(と身内)しか使ってないのでその辺りをごっそり削って固定にした。

動的にテーマを実装するためにrailsのcoreの中をハック溶かしてたのだけど、これが地雷になりつつあるので削除して正解だったと思う。railsの道から外れるコードは削除しておく。これが鉄則。

そして、地味に取り掛かってるのがhamlの削除。hamlは事実上railsでしか使われていないので別なサイトから見本をコピペってくるときに面倒くさい。全部erbにする予定。

さらにform_builderとかもbootstrapのコードをコピペってくるときに面倒なので削除する方向。text_field_tagとかHTML生成するためにどういうヘルパーがあってどういう仕様だったかとか思い出すのも大変。直接買いておいた方が結局メンテナンス性が高いと結論。そういう流れで。

新型のMac Book Air購入したら初期不良をつかみました

1png

2020年3月に発売になったMac Book Air。 今使っているMac Book AirがCatalinaに非対応故そろそろ新しいのを買いたいところだったのでポチってしましました。 それが3/21の話。その時点でのお届け予定日は4/9でした。

ブツは実際予定通り4/9に到着しました。 しかしながらこのMac Book Air。キーボードの「S」が超反応することに気がつきました。ちょっと触っただけで文字がタイプされてしまう。 Sはホームポジションに近いこともあり指のタッチなど頻繁にあるので、流石にこれは困る。これは初期不良では無いか?ということでオンラインストアのサポートに電話しました。

しかし、コロナウィルスが蔓延しているような状況で中々サポートに繋がりません。あまりに繋がらないのでApple Storeに相談仕様にもコロナ禍で全店閉まってるし、ヨドバシカメラの上にあるMac修理の正規代理店に持ち込もうとするも予約が一杯。もうこれは意地でもオンラインストアに繋ぐしか無い。しかし、この返品のためのサポートに中々繋がらない(ちなみにサポートはチャットという手もありますが返品は電話のみです)。結局9日のコンタクトは諦め次の日に再度電話をしました。

次の日は40分くらい電話口で待たされた後サポートに繋がり、返品・交換の手続きを終えることができました。 返品の回収は早かったですね。指定の運送会社が次の日に取りにきました。そして、4/13に代替品の手配をしたとの連絡。

ところがこの代替品の手配からが長い長い。当初4/21〜4/28に配送予定ということでしたが、待てど暮らせど配送ステータスが配送にならず、結局届いたのは4/30でした。実に購入をしてから40日。 

しかも今回古いMacを下取りに出しており、送ってしまってからキーボードの不良に気がついたので、しばらくMacのノートが無い状態。別のMac miniがあるから事無を得ましたが危うく詰むところでした。

コロナ禍で国際便で発送されるもの買ったらダメですね。流通が正常になってからじゃないと何かあった時に非常に困る。

rails5.2.1 対応だん

このサイトのバージョンを 5.2.1にあげました。

最初に作ったときのバージョンは1.3.6。

そこから上げつづけて最新版に追いつきました。ただ上げただけではなく、

  • tubolinksに対応
  • jquery-ujsの成仏 (rails-ujsに移行)
  • もうサポートされていないgemの大量成仏

など対応したところは数しれず。感無量だな。

いい加減 SSL化をしてみた

アドレスバーに鍵のマーク

ま、Let's Encryptですけどね。

VAIOを Windows10にしてみたら動かなかった話

そもそもVAIOは公式にWindows10のアプグレードはちょっとやめとけと言ってるのでアプデートは完全な自己責任なわけですが、右下に「入手できます」というアイコンが表示されたり「Windows10にしたよ。ひゃっほー」などというつぶやきなどを見たりするとアプグレードしたくなるというのが人情というもの。ということでアプグレードしてみました。ちなみにオイラのVAIOはWindows8プレインストールモデルのType E14(SVE14A3AJ)です。

多分簡単にいくだろうと思ってたのですが60%ほど進行したところでブルースクリーン(INTERNAL POWER ERROR)。ここでセットアップが中断し、またWindows8.1に戻るという状態になりました。

いろいろググッてみると、どうもAMDのグラフィックボードが悪さをしているようです。このVAIOには AMD Radeon HD 7600Mと Intelの HD Graphics 4000というグラボが登載されているのですが、このAMDのほうのドライバがどうもよろしくないようです。

最新版とかにしても上手くいかないのでBIOSから(電源を切ったのち、ASSISTボタンでPC起動する)で、discrete graphics adapterを disabledにしたら Windows10にアップグレードできました。

firefoxのメニューを日本語にするメモ

ここに書いてある。

https://support.mozilla.org/ja/questions/1018686

かいつまんで言うと、

  1. 言語パックをインストールして
  2. about:configで設定を開き
  3. "I'll be careful, I promise!".というボタンを押し
  4. intl.locale.matchOStrueにし
  5. general.useragent.localeを jpにして
  6. firefoxをリスタートする

4の手順が書いてないサイトが多いです。これをしないとダメな模様

rubyの class_eval とスコープのメモ

rubyのclass_evalは「ブロックをクラス定義やモジュール定義の中のコードであるように実行します。ブロックの戻り値がメソッドの戻り値になります。」と書いてますが、次の例は以下のような警告がでるようです。

warning: class variable access from toplevel

class A
  def self.a
    p @@a
  end
end

A.class_eval{
  @@a = 1
}

A.a

どうも class_evalのブロック中で @@aはクラスAの@@aを見ているのではなくトップレベルに @@aという変数を定義してしまっているようです。

class A
  def self.a
    p @@a
  end
end

A.class_eval{
  @@a = 1
}

p @@a #=>1

これを期待したように実行するには

class_variable_set

を使うとよいようです。

class A
  def self.a
    p @@a
  end
end

A.class_eval{
  class_variable_set :@@a, 1
}

A.a

あるいは class_evalの中身を文字列で渡してしまえばOK

class A
  def self.a
    p @@a
  end
end

A.class_eval <<-EOS
  @@a = 1
EOS

A.a

 

 

RailsでTimeWithZoneを使う

真面目にプログラムをしはじめるとタイムゾーンは非常に面倒です。まず、rubyのTimeオブジェクトはUNIXのシステムをそのまま使っているようなので時刻として

UTC

または

現在のOSのlocaltime

の2つしかとりえません。なのでユーザ毎に異なるタイムゾーンをもつシステムとかを作るときは非常に面倒です。localtimeは/etc/localtimeや 環境変数TZで決まるので例えば現在台北で今何時よ?というときは

ENV['TZ'] = "Asia/Taipei
Time.now

とかやればいいわけですが、ENVはプロセス全体に影響を及ぼしうるので、あまり推奨されません。こういったことを何とかしてくれるライブラリとしてTZinfoというのがあるのですが、 Railsの4あたりからTimeWithZoneとして標準で入っているようです。 これを使うのが簡単でしょう。

Timeを TimeWithZoneに変換するには、次のようにします。

Time.utc(2000).in_time_zone('Alaska') # => Fri, 31 Dec 1999 15:00:00 AKST -09:00
Time.local(2000).in_time_zone('Alaska') # => Fri, 31 Dec 1999 06:00:00 AKST -09:00

逆ににアラスカの2000年1月1日の0時を求めたいときは、次のようにします。

Time.zone = 'Alaska'
Time.zone.local(2000,1, 1) #=> Sat, 01 Jan 2000 00:00:00 AKST -09:00

Time.zoneに代入していると、クラス変数に代入しているような気がしますが、これはスレッドセーフに作られているようなのでとりあえず代入してしまって問題ないようです。

Time.zoneを設定すると、ActiveRecord等の時間のフィールドはそのtimezoneになるようなのでControllerのフィルタあたりでユーザ毎にTime.zoneを設定してやれば一発で済みそうです。個人的には内部的に時間を格納するときはすべてUTCにしておいて、表示や検索するときに適宜変換するのが混乱がないと思います。Railsの標準設計もそうなっているでしょう。

現在時刻を持ったTimeWithZoneオブジェクトを作るには

Time.zone.now

とすればOKです。TimeWithZoneオブジェクトはTimeオブジェクトとメソッドで互換があるのでActiveSupportのの便利なメソッドが使えます。

夏時間等あるエリアでは、24時間前などを取得しても1日前になる保証はないので、日付の差分を取得したいときは、yesterdayやbeginning_of_yearなどを活用しましょう。

例えばハワイの今年の正月は以下のようにすればUTCで求まります。

Time.zone.now.beginning_of_year.utc

2015年3月29日の2時の一日前の同時刻との差はどうなるでしょうか?

Time.zone="London"
a=Time.zone.local(2015,3,29,2,0,0) # => Sun, 29 Mar 2015 02:00:00 BST +01:00
b=a.yesterday #=> Sat, 28 Mar 2015 02:00:00 GMT +00:00
(a-b)/3600 => 23.0

ロンドンでは2015年3月29日の2時に1時間時間が進みますので前日の同じ時間との差は23時間ということがわかります。

その他、詳しい使い方は

に例があるのでこちらを参考にしてください。

注記)

ruby-2.2からはlocaltimeでもないUTCでもないオフセットを持った時刻を保持できるようです

Time.parse("2015-02-24 16:32:20 +1200") #=> 2015-02-24 16:32:20 +1200