10円未満寄付専用レジ
消費税導入からもうすぐ20年。おつりの1円玉にもだいぶ慣れてきたころ、コンビニのレジ横に寄付用のハコが置かれるようになった。
最近は10円未満のおつりはあのハコに寄付するようにしているのだけど、どの店のも口が小さくてけっこう入れにくい。それに、おつりの中から1円玉と5円玉をより分ける作業もちょっとササっとできないと、後ろのせっかちな人に舌打ちされたり。
そこで、「現金専用レジ」っていうのを見かけるように、「10円未満寄付専用レジ」を作ってもらえないものか。「10円未満寄付専用レジ」に並ぶことでちょっと早く会計が済んだり、空いているといいよなあ。みんなが「10円未満寄付専用レジ」を使うようになると、寄付は増えるし、扱う1円玉を減らせるし(お店にとってはコストダウン)、消費税上げても気づかれにくいし(アレッ)。
でもレジが2つぐらいのコンビニでは難しいか。どこか大手コンビニ、スーパーで検討してもらえないものだろうかなあ。話がややこしくなるのでクレジットカードやプリペイドカードってのはとりあえず無しでね。
今日、舌打ちされたので、考えてみた。
「祖先の物語」は買い
進化論は、直感的にはシンプルだけれど、現実を理解するために使おうとするととても難しい。
ドーキンスの近刊「祖先の物語」は、厳密で率直で謙虚で面白い。
「利己的な遺伝子 (科学選書)」以来、安易な間違った解釈や誤解と戦い続けているせいで、誤解されやすいポイントをしつこいくらいに念押しするのもちょっと面白い。
本当を言うと、最初と最後の100ページずつぐらいはものすごくスリリングで面白いのだけれど、挟まれた真ん中あたり(まあ大半ですわ)は、分類学の専門用語となじみのない生物の学名がオンパレードで、僕にはかなり難解。だいぶ読み飛ばした。
でも、そういったところは、後で事典的に読めばいい。序章と終章を楽しみながら、ときどき戻ってじっくり読んで理解すれば、もっと、ずっと、楽しめるんだと思う。
うまく言えないのだけれど、システム設計をしていると感じるいろいろなことや、問題解決のための良く知られた手法と、よく似たパターンがそこかしこにでてくるところも興味深い。
それほど深く触れられてはいないけれど、「進化しやすさの進化」というのも気になる概念。
再読するだろうなあと思える数少ない本で、今のところ今年一番。
ちょっと高いと思うかもしれないけれど、買いです。
Oracle PL/SQLとUNIXタイムスタンプ
Oracleのdate型とUNIXタイムスタンプを相互に変換するPL/SQL関数が必要になった。
作ってみた。
create or replace function to_unix_timestamp(dt date) return number is begin return (dt - to_date('1970-01-01','YYYY-MM-DD')) * 86400; end; / create or replace function from_unix_timestamp(ts number) return date is begin return to_date(trunc(ts / 86400, 0) + 2440588, 'J') + (mod(ts, 86400) / 86400); end; / select to_unix_timestamp(sysdate) from dual; select to_char(from_unix_timestamp(to_unix_timestamp(sysdate)),'YYYY-MM-DD HH24:MI:SS') from dual;
消費税を計算するOracle PL/SQLパッケージ
消費税込み金額を計算したり、税抜き金額を計算したりするPL/SQLパッケージ。
消費税率が変わったらTAX_CALENDARという名前のテーブルに税率を追加すればいい。
これで消費税が上がっても大丈夫。10%でも20%でも平気。どんとこい。
TAX_RATE2は、テーブルを使わずに、FUNCTION内で日付とレートをハードコードして判断している。税率が変わったら書き直さないといけない。税率変わるたびにメンテナンス=ソース変更とリコンパイルが必要だけれど、まあ、運用環境によってはこっちがいい場合も多いか。シンプルだし速いはず。
/* || 消費税率のカレンダーテーブル */ DROP TABLE TAX_CALENDAR; / -- TAX CALENDAR TABLE CREATE TABLE TAX_CALENDAR ( DtS date NOT NULL, DtE date NOT NULL, TAX number(3,2) ); -- TAX TABLE ALTER TABLE TAX_CALENDAR ADD CONSTRAINT DISP_TAX_CALENDAR_PK PRIMARY KEY (DtS,DtE); / /* || TAX_CALENDAR INSERTION */ DELETE FROM TAX_CALENDAR; INSERT INTO TAX_CALENDAR(DtS,DtE,Tax) VALUES(to_date('1900-01-01'),to_date('1989-04-01'),0.000); INSERT INTO TAX_CALENDAR(DtS,DtE,Tax) VALUES(to_date('1989-04-01'),to_date('1997-04-01'),0.03); INSERT INTO TAX_CALENDAR(DtS,DtE,Tax) VALUES(to_date('1997-04-01'),to_date('2999-04-01'),0.05); CREATE OR REPLACE PACKAGE TAX_PKG AS -- 日付で消費税率を返す FUNCTION TAX_RATE(i_date IN date DEFAULT SYSDATE) RETURN NUMBER; -- 日付と金額で消費税額を返す FUNCTION TAX(i_price IN NUMBER,i_date IN date DEFAULT SYSDATE) RETURN NUMBER; -- 日付と金額で税込金額を返す FUNCTION INCLUDE_TAX(i_price IN NUMBER,i_date IN date DEFAULT SYSDATE) RETURN NUMBER; PRAGMA RESTRICT_REFERENCES(TAX_RATE,WNDS); PRAGMA RESTRICT_REFERENCES(TAX,WNDS); PRAGMA RESTRICT_REFERENCES(INCLUDE_TAX,WNDS); END TAX_PKG; / CREATE OR REPLACE PACKAGE BODY TAX_PKG AS /* || 日付で消費税率を返す || TAX_CALENDARテーブルを参照する */ FUNCTION TAX_RATE(i_date IN date DEFAULT SYSDATE) RETURN NUMBER IS ATax number; BEGIN SELECT TAX INTO ATAX FROM TAX_CALENDAR WHERE i_date >= DtS AND i_date < DtE; RETURN ATAX; END; /* || 日付で消費税率を返す */ FUNCTION TAX_RATE2(i_date IN date DEFAULT SYSDATE) RETURN NUMBER IS BEGIN IF i_date < to_date('89-04-01') THEN RETURN 0.00; ELSIF i_date < to_date('97-04-01') THEN RETURN 0.03; ELSE RETURN 0.05; --現状 END IF; END; ------------------------------------------------------- -- 日付と金額で消費税額を返す ------------------------------------------------------- FUNCTION TAX(i_price IN NUMBER, i_date IN date DEFAULT SYSDATE) RETURN NUMBER IS BEGIN RETURN ROUND(i_price * TAX_RATE(i_date)); END; ------------------------------------------------------- -- 日付と金額で税込金額を返す ------------------------------------------------------- FUNCTION INCLUDE_TAX(i_price IN NUMBER, i_date IN date DEFAULT SYSDATE) RETURN NUMBER IS BEGIN RETURN ROUND(i_price + (i_price * TAX_RATE(i_date))); END; END TAX_PKG; / ------------------------------------------------------- -- 消費税計算のテスト用 ------------------------------------------------------- select tax_pkg.tax_rate('89-03-31'), tax_pkg.tax_rate('89-04-01'), tax_pkg.tax_rate('97-03-31'), tax_pkg.tax_rate('97-04-01'), tax_pkg.tax_rate(SYSDATE) from dual; select tax_pkg.tax(101,'89-03-31'), tax_pkg.tax(101,'89-04-01'), tax_pkg.tax(101,'97-03-31'), tax_pkg.tax(101,'97-04-01'), tax_pkg.tax(101,SYSDATE) from dual; select tax_pkg.include_tax(101,'89-03-31'), tax_pkg.include_tax(101,'89-04-01'), tax_pkg.include_tax(101,'97-03-31'), tax_pkg.include_tax(101,'97-04-01'), tax_pkg.include_tax(101,SYSDATE) from dual;
ユーザ管理をするOracle PL/SQLパッケージ
ユーザIDやパスワードは、OracleのDBMS_OBFUSCATION_TOOLKITパッケージを使って暗号化して保管するPL/SQLパッケージ。DBMS_OBFUSCATION_TOOLKITを使っているので、Oracle9i以降が必要(たぶん)。
ユーザ情報管理機能の必要なアプリケーションで使うと便利かもしれない。
もう使ってはいけないとどこかで読んだような気もするmd5でハッシュをとっている部分は、簡単な変更で入れ替えることもできるはず。
このパッケージを使うことで、アプリケーションからは暗号化/復号化の処理、テーブルの列構成の詳細を隠蔽することができるはず。
小道具というにはちょっと大きいかも。
/** * ユーザ認証管理パッケージ * テーブルUSER_AUTHとパッケージAUTH_PKGで構成される。 * ユーザIDとパスワードはMD5ダイジェストとして格納される。 * Oracle9i以降専用!! * * ユーザIDの管理(あるユーザIDは誰か?など)は当パッケージを利用するシステムの責任 * ユーザIDを登録(REGIST_USER)、削除(DROP)、パスワード認証(AUTHENTICATE)などを管理する * * クラス:ユーザIDのクラス。ユーザIDはクラス内でユニークである必要がある。 * ユーザID:何文字でもいい。内部ではMD5のハッシュで管理している。クラス内でユニークとなること。 * パスワード:何文字でもいい。内部ではMD5のハッシュで管理している * 複数のクラスを管理する場合は、SET_USERCLASSを呼び出してから他の操作をすること。 * USERCLASS()で現在どのクラスを使っているかを知ることができる。 * 最初にAUTH_PKG.SET_USERCLASS(ユーザクラス)を呼び出して使うクラスの設定を初期化する。 * この初期化をしないと、クラスとして「DEFAULT」が設定される。 * 初期化しないで使う場合は、全関数でユーザクラスを最初の引数に指定する。 * * RETURN_CODE_TRUEとRETURN_CODE_FALSEはFUNCTIONが返す論理値に割り当てる文字列を指定する。 * RETURN_CODE_TRUEにはTRUEの時に返す文字列。デフォルトは'Y'。 * RETURN_CODE_FALSEにはFASEの時に返す文字列。デフォルトは'N'。 * * テーブル名 * USER_AUTH * * 順序名 * SEQ_USER_AUTH_ID * */ /****************************************************************** ユーザ認証管理パッケージ用の認証テーブル USER_AUTHの作成 *******************************************************************/ DROP SEQUENCE SEQ_USER_AUTH_ID; CREATE SEQUENCE SEQ_USER_AUTH_ID; DROP TABLE USER_AUTH; CREATE TABLE USER_AUTH( AuthId number(9,0) NOT NULL, UserClass varchar2(10) NOT NULL, UserId char(32) NOT NULL, Password char(32) NOT NULL, PasswordChanged date DEFAULT SYSDATE, Updated date DEFAULT SYSDATE, Created date NOT NULL ); COMMENT ON TABLE USER_AUTH IS 'ユーザ認証システム:認証テーブル'; COMMENT ON COLUMN USER_AUTH.AuthID IS '認証ID AUTH_ID_SEQで発番された連番'; COMMENT ON COLUMN USER_AUTH.UserClass IS 'ユーザIDの属しているクラス'; COMMENT ON COLUMN USER_AUTH.UserID IS 'ユーザID:ユーザ入力値のMD5メッセージダイジェスト。'; COMMENT ON COLUMN USER_AUTH.Password IS 'パスワード:ユーザ入力値のMD5メッセージダイジェスト'; COMMENT ON COLUMN USER_AUTH.PasswordChanged IS '最後にパスワードを変更した日時'; COMMENT ON COLUMN USER_AUTH.Updated IS '更新日時'; COMMENT ON COLUMN USER_AUTH.Created IS '作成日時'; CREATE UNIQUE INDEX USER_AUTH_USERID ON USER_AUTH(UserClass,UserID); ALTER TABLE USER_AUTH ADD CONSTRAINT USER_AUTH_PK PRIMARY KEY (AuthID); /****************************************************************** AUTH_PKG本体の作成 *******************************************************************/ CREATE OR REPLACE PACKAGE AUTH_PKG AS --グローバル変数定義 --TRUE/FALSEやOK/NGやYES/NOに変更してもいい RETURN_CODE_TRUE VARCHAR2(10) := 'Y'; RETURN_CODE_FALSE VARCHAR2(10) := 'N'; --USERCLASSを設定しない場合のデフォルトユーザクラス DEFAULT_USERCLASS CONSTANT VARCHAR2(10) := 'DEFAULT'; --初期登録でパスワードを指定しなかった場合にデフォルトで指定するパスワード DEFAULT_PASSWORD CONSTANT VARCHAR2(32) := 'changeOnInstall'; -- パッケージのユーザクラスをセットする PROCEDURE SET_USERCLASS(i_userclass IN VARCHAR2); -- 設定されているユーザクラスを返す FUNCTION USERCLASS RETURN VARCHAR2; --パスワード変更 PROCEDURE CHANGE_PASSWORD(i_userclass IN VARCHAR2, i_userid IN VARCHAR2, i_password IN VARCHAR2); PROCEDURE CHANGE_PASSWORD(i_userid IN VARCHAR2, i_password IN VARCHAR2); --ユーザ完全削除 PROCEDURE DROP_USER(i_userclass IN VARCHAR2, i_userid IN VARCHAR2); PROCEDURE DROP_USER(i_userid IN VARCHAR2); --ユーザ登録 --パスワードを指定しないとデフォルト値が設定される PROCEDURE REGIST_USER(i_userclass IN VARCHAR2, i_userid IN VARCHAR2, i_password IN VARCHAR2); PROCEDURE REGIST_USER(i_userid IN VARCHAR2, i_password IN VARCHAR2); --ユーザの存在チェック FUNCTION USER_EXISTS(i_userclass IN VARCHAR2, i_userid IN VARCHAR2) RETURN CHAR; FUNCTION USER_EXISTS(i_userid IN VARCHAR2) RETURN CHAR; --認証(パスワードチェック)してOKならRETURN_CODE_TRUEを返す。失敗したらRETURN_CODE_FALSEを返す FUNCTION AUTHENTICATE(i_userid IN VARCHAR2, i_password IN VARCHAR2) RETURN VARCHAR2; FUNCTION AUTHENTICATE(i_userclass IN VARCHAR2, i_userid IN VARCHAR2, i_password IN VARCHAR2) RETURN VARCHAR2; --最後にパスワードを変更した日時を返す FUNCTION PASSWORD_CHANGED(i_userclass IN VARCHAR2, i_userid IN VARCHAR2) RETURN DATE; FUNCTION PASSWORD_CHANGED(i_userid IN VARCHAR2) RETURN DATE; --最後にパスワードを変更した日時が有効な日数(i_days)を過ぎていたらRETURN_CODE_TRUEを返す FUNCTION PASSWORD_EXPIRED(i_userclass IN VARCHAR2, i_userid IN VARCHAR2, i_days IN NUMBER) RETURN CHAR; FUNCTION PASSWORD_EXPIRED(i_userid IN VARCHAR2, i_days IN NUMBER) RETURN CHAR; --指定した文字数のランダムな文字列を返す。初期パスワードの生成に。 FUNCTION RANDOM_STRING(i_string_length IN NUMBER DEFAULT 6) RETURN VARCHAR2; --MD5 基本的には内部利用を想定しているけれど、別に外で使っても良い FUNCTION MD5(input_string IN VARCHAR2) RETURN VARCHAR2; --暗号化した文字列を返す 現バージョンはMD5を使った1方向だけに対応。 FUNCTION ENCRYPT(input_string IN VARCHAR2, ENCRYPT_METHOD IN VARCHAR2 DEFAULT 'MD5') RETURN VARCHAR2; END AUTH_PKG; / CREATE OR REPLACE PACKAGE BODY AUTH_PKG AS --パッケージローカルのセッション内変数 CURRENT_USERCLASS VARCHAR2(10) := DEFAULT_USERCLASS; SEED_COUNTER number := 1; ---------------------------------------------------------- -- md5ダイジェストを得るためのストアードファンクション -- USER_AUTHテーブルのpasswordフィールドにはmd5ダイジェストを記録する ---------------------------------------------------------- FUNCTION MD5(input_string IN VARCHAR2) RETURN VARCHAR2 IS hex_digest varchar2(32); digest varchar2(16); BEGIN digest := DBMS_OBFUSCATION_TOOLKIT.MD5(INPUT_STRING => input_string); SELECT Lower(RAWTOHEX(digest)) INTO hex_digest FROM dual; RETURN hex_digest; END; ---------------------------------------------------------- -- 受け取った文字列を暗号化して返す ---------------------------------------------------------- FUNCTION ENCRYPT(input_string IN VARCHAR2, ENCRYPT_METHOD IN VARCHAR2 DEFAULT 'MD5') RETURN VARCHAR2 IS BEGIN IF ENCRYPT_METHOD = 'MD5' THEN RETURN MD5(input_string); ELSE RAISE_APPLICATION_ERROR(-20003,'暗号化方法' || ENCRYPT_METHOD || 'が私には理解できません。'); END IF; END; ---------------------------------------------------------- -- P 初期化する ---------------------------------------------------------- PROCEDURE SET_USERCLASS( i_userclass IN VARCHAR2) IS BEGIN CURRENT_USERCLASS := i_userclass; END; ---------------------------------------------------------- -- F CURRENT_USERIDを返す ---------------------------------------------------------- FUNCTION USERCLASS RETURN VARCHAR2 IS BEGIN RETURN CURRENT_USERCLASS; END; ---------------------------------------------------------- -- P CHANGE_PASSWORD ---------------------------------------------------------- PROCEDURE CHANGE_PASSWORD ( i_userclass IN VARCHAR2, i_userid IN VARCHAR2, i_password IN VARCHAR2) IS ret_val CHAR(1) := RETURN_CODE_TRUE; BEGIN ret_val := USER_EXISTS(i_userclass, i_userid); IF ret_val = RETURN_CODE_TRUE THEN UPDATE USER_AUTH SET password = ENCRYPT(i_password), PasswordChanged=SYSDATE, Updated=SYSDATE WHERE userclass = i_userclass AND userid = ENCRYPT(i_userid); END IF; END; PROCEDURE CHANGE_PASSWORD ( i_userid IN VARCHAR2, i_password IN VARCHAR2) IS BEGIN CHANGE_PASSWORD(CURRENT_USERCLASS, i_userid, i_password); END; ---------------------------------------------------------- -- P DROP_USER 完全にレコードを削除する ---------------------------------------------------------- PROCEDURE DROP_USER (i_userclass IN VARCHAR2, i_userid IN VARCHAR2) IS BEGIN DELETE USER_AUTH WHERE userclass = i_userclass AND userid = ENCRYPT(i_userid); END; PROCEDURE DROP_USER (i_userid IN VARCHAR2) IS BEGIN DROP_USER(CURRENT_USERCLASS, i_userid); END; ---------------------------------------------------------- -- P REGIST_USER ---------------------------------------------------------- PROCEDURE REGIST_USER(i_userclass IN VARCHAR2, i_userid IN VARCHAR2, i_password IN VARCHAR2) IS BEGIN IF USER_EXISTS(i_userclass, i_userid)=RETURN_CODE_TRUE THEN RAISE_APPLICATION_ERROR(-20002,'ご指定のユーザ' || i_userid || 'は既に存在しています。'); ELSE INSERT INTO USER_AUTH(authid,userclass,userid,password,updated,created) VALUES(SEQ_USER_AUTH_ID.NEXTVAL,i_userclass,ENCRYPT(i_userid),ENCRYPT(i_password),SYSDATE,SYSDATE); END IF; END; PROCEDURE REGIST_USER( i_userid IN VARCHAR2, i_password IN VARCHAR2) IS BEGIN REGIST_USER(CURRENT_USERCLASS, i_userid, i_password); END; ---------------------------------------------------------- -- F USER_EXISTS ユーザが存在したらRETURN_CODE_TRUE、存在しなければRETURN_CODE_FALSEを返す ---------------------------------------------------------- FUNCTION USER_EXISTS(i_userclass IN VARCHAR2, i_userid IN VARCHAR2) RETURN CHAR IS r number; BEGIN SELECT COUNT(*) INTO r FROM USER_AUTH WHERE userclass=i_userclass AND userid = ENCRYPT(i_userid); IF r > 0 THEN RETURN RETURN_CODE_TRUE; ELSE RETURN RETURN_CODE_FALSE; END IF; END; FUNCTION USER_EXISTS( i_userid IN VARCHAR2) RETURN CHAR IS BEGIN RETURN USER_EXISTS(CURRENT_USERCLASS, i_userid); END; ---------------------------------------------------------- -- F AUTH ---------------------------------------------------------- --認証(パスワードチェック)してOKならRETURN_CODE_TRUEを返す。失敗したらRETURN_CODE_FALSEを返す FUNCTION AUTHENTICATE( i_userclass IN VARCHAR2, i_userid IN VARCHAR2, i_password IN VARCHAR2 ) RETURN VARCHAR2 IS r number; BEGIN SELECT COUNT(*) INTO r FROM USER_AUTH WHERE userclass=i_userclass AND userid = ENCRYPT(i_userid) AND password = ENCRYPT(i_password); IF r > 0 THEN RETURN RETURN_CODE_TRUE; ELSE RETURN RETURN_CODE_FALSE; END IF; END; --認証(パスワードチェック)してOKならRETURN_CODE_TRUEを返す。失敗したらRETURN_CODE_FALSEを返す FUNCTION AUTHENTICATE( i_userid IN VARCHAR2, i_password IN VARCHAR2 ) RETURN VARCHAR2 IS BEGIN RETURN AUTHENTICATE(CURRENT_USERCLASS, i_userid, i_password); END; ---------------------------------------------------------- -- F PASSWORD ---------------------------------------------------------- --パスワードの変更日時を返す。 FUNCTION PASSWORD_CHANGED( i_userclass IN VARCHAR2, i_userid IN VARCHAR2 ) RETURN DATE IS r date; BEGIN SELECT PasswordChanged INTO r FROM USER_AUTH WHERE userclass=i_userclass AND userid = ENCRYPT(i_userid); RETURN r; END; --パスワードの変更日時を返す。 FUNCTION PASSWORD_CHANGED( i_userid IN VARCHAR2 ) RETURN DATE IS BEGIN RETURN PASSWORD_CHANGED(CURRENT_USERCLASS, i_userid); END; ---------------------------------------------------------- -- F PASSWORD_EXPIRED ---------------------------------------------------------- --パスワードの有効期限が過ぎているかどうかをチェックしてRETURN_CODEを返す。 FUNCTION PASSWORD_EXPIRED( i_userclass IN VARCHAR2, i_userid IN VARCHAR2, i_days IN NUMBER ) RETURN CHAR IS r date; BEGIN SELECT PASSWORD_CHANGED(i_userclass,i_userid) + i_days INTO r FROM DUAL; IF r < SYSDATE THEN RETURN RETURN_CODE_TRUE; ELSE RETURN RETURN_CODE_FALSE; END IF; END; FUNCTION PASSWORD_EXPIRED( i_userid IN VARCHAR2, i_days IN NUMBER ) RETURN CHAR IS r date; BEGIN RETURN PASSWORD_EXPIRED(CURRENT_USERCLASS, i_userid); END; ---------------------------------------------------------- -- F ランダムな文字列を返す ---------------------------------------------------------- FUNCTION RANDOM_STRING(i_string_length IN NUMBER DEFAULT 6) RETURN VARCHAR2 IS seed number; r char(32); BEGIN seed := to_number(to_char(sysdate,'SSSSS')) + SEED_COUNTER; SEED_COUNTER := SEED_COUNTER + 1; dbms_random.initialize(seed); dbms_random.seed(seed); r := MD5(to_char(abs(dbms_random.random()))); RETURN substr(r,-i_string_length,i_string_length); END; END AUTH_PKG;