end0tknr's kipple - web写経開発

太宰府天満宮の狛犬って、妙にカワイイ

RHEL8 へ Apache2.4 & mod_php(ver.7.4 from source) を install

https://end0tknr.hateblo.jp/entry/20201129/1606610007

上記entryのphp 7.4版です。

以前のphp 7.2と configure時のoptionが仕様変更になっていましたので、 メモとして残しておきます。 (--with-mysqlを削除、--with-curlを追加)

$ wget https://www.php.net/distributions/php-7.4.30.tar.gz
$ tar -xvf php-7.4.30.tar.gz
$ cd php-7.4.30
./configure   --with-apxs2=/usr/bin/apxs \
              --enable-mbstring \
          --with-mysqli=/usr/bin/mysql_config \
          --with-pdo-mysql=/usr/bin/mysql_config \
          --with-pdo-pgsql=/usr/bin/pg_config \
              --with-curl
$ make
$ make test
$ sudo cp libs/libphp7.so /etc/httpd/modules/

rhel8でdefaultのphp 7.2を 7.4へ update

https://access.redhat.com/solutions/6809611

↑↓の通りです

$ cat /etc/redhat-release 
Red Hat Enterprise Linux release 8.7 (Ootpa)

$ /usr/bin/php -v
PHP 7.2.24 (cli) (built: Oct 22 2019 08:28:36) ( NTS )
Copyright (c) 1997-2018 The PHP Group
Zend Engine v3.2.0, Copyright (c) 1998-2018 Zend Technologies

$ yum list installed | grep php
php.x86_64         7.2.24-1.module+el8.2.0+4601+7c76a223   @rhel-8-for-x86_64-appstream-rpms        
php-cli.x86_64     7.2.24-1.module+el8.2.0+4601+7c76a223   @rhel-8-for-x86_64-appstream-rpms        
php-common.x86_64  7.2.24-1.module+el8.2.0+4601+7c76a223   @rhel-8-for-x86_64-appstream-rpms        
php-devel.x86_64   7.2.24-1.module+el8.2.0+4601+7c76a223   @rhel-8-for-x86_64-appstream-rpms        
php-fpm.x86_64     7.2.24-1.module+el8.2.0+4601+7c76a223   @rhel-8-for-x86_64-appstream-rpms        
php-gd.x86_64      7.2.24-1.module+el8.2.0+4601+7c76a223   @rhel-8-for-x86_64-appstream-rpms        
php-pdo.x86_64     7.2.24-1.module+el8.2.0+4601+7c76a223   @rhel-8-for-x86_64-appstream-rpms        
php-pear.noarch    1:1.10.5-9.module+el8.1.0+3202+af5476b9 @rhel-8-for-x86_64-appstream-rpms        
php-pgsql.x86_64   7.2.24-1.module+el8.2.0+4601+7c76a223   @rhel-8-for-x86_64-appstream-rpms        
php-process.x86_64 7.2.24-1.module+el8.2.0+4601+7c76a223   @rhel-8-for-x86_64-appstream-rpms        
php-xml.x86_64     7.2.24-1.module+el8.2.0+4601+7c76a223   @rhel-8-for-x86_64-appstream-rpms 

$ sudo yum module reset php
$ sudo yum module list php
Name   Stream   Profiles                  
php    7.2 [d]  common [d], devel, minimal
php    7.3      common [d], devel, minimal
php    7.4      common [d], devel, minimal
php    8.0      common [d], devel, minimal

$ sudo yum module enable php:7.4
$ sudo yum update php

$ /usr/bin/php -v
PHP 7.4.30 (cli) (built: Jun  7 2022 08:38:19) ( NTS )
Copyright (c) The PHP Group
Zend Engine v3.4.0, Copyright (c) Zend Technologies
    with Zend OPcache v7.4.30, Copyright (c), by Zend Technologies

英文法 ふりかえり

メモ。今後も追記すると思います

目次

確認テスト

https://www.stepnet.co.jp/englishquiz/

5文型 (1:SV、2:SVC、3:SVO、4:SVOO、5:SVOC)

文型 説明
1 SV V:自動詞
2 SVC C:名詞,形容詞. S=C. He looks happy.
3 SVO V:他動詞. O:名詞.S≠C I follow you.(あなたについて行く)
4 SVOO V:他動詞. He taught me science.(私に科学を教えた)
5 SVOC V:他動詞. He keeps the kitchen clean.

参考url https://www.shane.co.jp/column/detail/id=22177

不定詞 (TO不定詞、原形不定詞)

不定 説明
TO原形 名詞的、形容詞、副詞的の用法があり、未来のことを指す
原形 使役動詞(let,make,have)、知覚動詞(hear,feel,see)に使用

参考url https://www.k-wam.jp/wamken/39446

TO不定詞の用法 (名詞的, 形容詞的, 副詞的)

用法 役割
名詞 主語(S) To play the guiter is fun. (ギターを弾くのは楽しい)
目的語(O) I promise to go home. (帰宅することを約束します)
補語(C) His hope is to be a player. (望みは選手になること)
形容詞 名詞を修飾 He needs something to drink (彼は飲み物が必要)
副詞 ※1 動詞を修飾 They practice to compete (出場する為、練習する)
He grew up to be an actor.(成長の結果、俳優になった)
独立不定詞※2 Strange to say, ~(奇妙なことに、幽霊を見たと言った)
To tell the truth, ~(実を言うと)
  • ※1 - 副詞的用法では 動詞、形容詞、副詞、文全体を修飾
  • ※2 - 慣用表現として、本文の前で、文全体を修飾

原形不定詞(知覚動詞) - hear, feel, see

動詞 動詞型
see 原形(※1) I saw him fall.(私は、彼がこけるのを見た)
notice I noticed her steal the goods.(彼女が盗むのに気付いた)
hear I heard her get angry.(彼女が怒るのを聞いた)
feel <省略>
smell <省略>

※1 原形 or ~ing or 過去分詞がOK

原形不定詞(使役動詞) - make, let, get, have

ニュアンス 動詞 動詞型
強制的 get TO原形 Get him to clean. (彼に掃除させなさい)
make 原形※1 I'll make him go. (彼に行かせるつもり)
許可する let Let children enter.(子供を入れて下さい)
一般的 have 自由※2 I have sister press. (姉にアイロンを頼む)

参考url https://www.rarejob.com/englishlab/column/20200813/

※1

ただし、wad受動態の場合、to不定詞を使用.

He was make to look like a feel in the movie. (映画では馬鹿に見えるように強いられた)

※2

動詞型 説明
原形 OとCが能動的.
I had friend repair bicycle.(友達に直してもらった)
過去分詞 OとCが受動的.
I had bicycle repaired by friend.(友達に直してもらった)
現在分詞 口語表現.
I won't have you using bicycle.(あなたに自転車は使わせない)

参考url https://www.eigo21.com/01/situmon/25.htm

tell は≠使役動詞の為、「tell 人 to不定詞」

参考url https://detail.chiebukuro.yahoo.co.jp/qa/question_detail/q1393162308

例: The doctor told me to stop drinking.

tell以外に、次も同様. want, ask, would like, order, require, request, recommend

時制

参考url https://www.amazon.co.jp/dp/4046056193

時制 説明
過去形 今現在は含まない. 過去1度きりのこと.過去の習慣,状態
現在形 過去,現在,未来に起こること.習慣,不変の真理,確定した未来
進行形 ~している最中
現在完了形 過去から~している. ちょうど~したところ. ~した経験がある

進行形にできない動詞(状態動詞と、その例外)

状態動詞
所属・構成 live, belong to, resemble, have, own, remain, 
知覚・心理 hear, see, smell, taste, like, love, prefer, hate, want,
know, believe, think, doubt, remember, suppose, understand

進行形にできない動詞の例外

  • have は所有以外の「食べる,飲む」以外の場合、OK ( She is having lunch. )
  • 知覚・心理の動作が「意思のある動作」の場合、OK ( He is tasting several wines. )
  • 「5秒以内に中断・再開できるもの」は、OK

時制の一致

「時制の一致」とは 主節と従属節が存在する場合、主節に従属節の時制を合わせること

時制
現在形 I think that she is sad.
過去形 I thought that she wad sad.
時制
過去形 I think that Ren wad pleased with the result.
過去完了形 I thought that Ren had been pleased with the result.
時制
will I think that she will be sad.
would I thought that she would be sad.

分詞構文 - 2つのこと(状態,動作)をまとめた文語表現

2つの節の主語が同じ場合、主語や接続詞を省略し、分詞構文化OK

参考url https://english-club.jp/blog/english-participial-construction/

現在分詞 - 分詞の主語が「~している」場合. ~ing.

He is sitting on a bench and he is reading a book.
-(分詞構文化)-> He is sitting on a bench reading a book.
 He broke his leg while he was playing soccer.
-(分詞構文化)-> He broke his leg playing soccer.

過去分詞 - 分詞の主語が「~されている」場合

The teacher is surrounded by his students, and the teacher is singing a song.
-(分詞構文化)->  Surrounded by his students, the teacher is singing a song.

※ 分詞を含む句を最初に置く場合「,」要

慣用句的 分詞構文

説明
Speaking of ~ ~と言えば
Taking ~ into consideration ~を考慮すると

動名詞

need to と need~ing

参考url https://philippines-gogakuryugaku.jp/archives/599

用法 主語
need to する人や物 I need to wash this shirt.
need ~ing ※1 される人や物 This shirt needs washing.

※1 needやwantの特別な用法の為、~ing が必ず受け身になるわけではない

stop to と stop ~ing

用法 意味
stop to ~する為に立ち止まる I stopped to smoke.
stop ~ing ~することを止める I stopped smoking.

仮定法 - 過去,過去完了,現在,未来

参考url https://eikaiwa-highway.com/past-conditional/

仮定法~ 意味や用法 / 例
過去 現在に反する仮定.If S V過去, S + 助動詞過去 + V原形
例:If I had a million dollars, I would buy a new house.
過去完了 過去に反する仮定 If S had V過去分詞, S + 助動詞過去 + have V過去分詞
例:If I had asked her out, she would have said yes.
現在 提案や要求.主節の時制に関係なく,that節は動詞の原形.
例:I suggested that my son study math at univ.
未来 未来の実現可能性が低い場合.
例:If He should call me, tell him that I will call back.

比較

参考url https://www.eibunpou.net/05/front.html

同等比較(原級),優劣比較(比較級,最上級)

比較 形容詞,副詞
同等比較 原級 He is as old as she.
優劣比較 比較級 He is older than she.
最上級 He is the oldest of the three.

比較を使用した慣用句

慣用句 説明 / 例
more(=rather)+原級 ~より、むしろ~.
例:He is more lucky than clever.
no more~than… …でないと同様に~でない.
He is no more capable of reading Chinese than I am.
no less than~ = as many as.
There were no less than 100 guests at the party.
no less ~ than… …と同様に~
He is no less guilty than you are.
no sooner ~ than… I had no sooner left the office than it began to rain.
(事務所を出たとたん雨が降りだした)

no/not more thanとno/not less thanの違い

参考url https://www.eskill-inc.com/nonotlessmorethan

「no」は完全否定で、「むしろその逆」の勢い

意味
no less than~ もある The wallet has no less than 100 yen.
not less than~ 少なくとも The wallet has not less than 100 yen.
not more than~ 多くても The wallet has not more than 100 yen.
no more than~ しかない The wallet has no more than 100 yen.

関係詞 - 関係代名詞の種類と格変化

参考url https://www.eibunpou.net/12/front.html

主格 所有格 目的格 先行詞
who whose whom 人間
which of which(口語),whose(文語) which 動物・事物
that -   that 人間・動物・事物
what what 先行詞を兼ねる

その他

似た単語

lie(嘘をつく), lie(横になる), lay(横にする)

意味 動詞種類 現在系 過去形 過去分詞 現在分詞
嘘をつく 自動詞 lie lied lied lying
横になる 自動詞 lie lay lain lying
横にする 他動詞 lay laid laid laying

rise(上がる)、raise(~を上げる)

意味 動詞種類 現在系 過去形 過去分詞
上がる 自動詞 rise rose risen
~を上げる 他動詞 raise raised raised

熟語

  • Are you going to keep me waiting all the morning? (私を朝の間中、待たせておくつもりですか?)

  • The death of her son has made her another person. (息子の死が彼女を別の人間にした)

  • She felt something move on her back. (後ろで何かが動くのを感じた)

  • He raised our expectations only to disapoint us. (私たちの期待を膨らませたが、結局、失望させた)

  • I woke up to find myself lying on the bench. (目を覚まし、自分がベンチに寝ていることに気付いた)

  • work closely with~ (~と密接に協力する)

  • work somoething out (なんとかする)

  • put together (組み立てる)

  • Feel free to ~ (気兼ねなく~して下さい)

  • good to go (準備ができている)

  • familliarize oneself with ~(~に慣れる)

  • Let's start off by ~ing (~始める)

  • reach out for help (助けを求める)

  • get more comfortable with ~ (~に慣れる)

  • take time off (休みをとる)

  • had better not ~ (~しない方がいい)

  • may well ~ (~するのも当然だ)

  • may as well ~ (~した方がいい)

  • S seem to have 過去分詞 (~だったように思える)

  • wake up to不定詞 (目を覚まして~する)

  • in a nutshell (要するに)

  • wrap up ~ (~を切り上げる)

google広告やgunosy広告の javascript コンバージョン計測

urlの変化なしに画面遷移する場合において google analytics の GA4 で Conversion通知 - end0tknr's kipple - web写経開発

上記entryの広告コンバージョン版。

動作確認は必要ですが、googlegunosyの document によれば、 以下で、動作する気がします。

google adsの場合

https://support.google.com/google-ads/answer/6331304?hl=ja&ref_topic=3165803

if(typeof goog_report_conversion != 'undefined' ){
    goog_report_conversion(location.href + "?status=input");
}

gunosy adsの場合

https://gunosyads.zendesk.com/hc/ja/articles/4407278177817-1-6-%E3%82%B3%E3%83%B3%E3%83%90%E3%83%BC%E3%82%B8%E3%83%A7%E3%83%B3%E3%82%BF%E3%82%B0%E8%A8%AD%E7%BD%AE

if(typeof GunosyTransit != 'undefined' ){
    GunosyTransit.link( location.href + "?status=input");
}

urlの変化なしに画面遷移する場合において google analytics の GA4 で Conversion通知

urlの変化なしに画面遷移する場合において google analytics の UA(Universal Analytics) で Conversion通知 - end0tknr's kipple - web写経開発

上記entryのGA4版です。

Step1 - カスタムイベント作成

「管理」→「イベント」で画面遷移後、「イベントを作成」をクリック

設定画面で以下のように入力後、「作成」をクリック

Step2 - イベントをコンバージョンに登録

「管理」→「コンバージョン」で画面遷移後、 「新しいコンバージョンイベント」をクリック

Step1で登録したイベント名を入力後、「保存」をクリック

Step3 - テスト用htmlを作成

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="utf-8">
  <title>CONVERSION TEST</title>
</head>
<body>

  <!-- Global site tag (gtag.js) - Google Analytics GA4 -->
  <script async src="https://www.googletagmanager.com/gtag/js?id=G-????????????">
  </script>
  <script>
    window.dataLayer = window.dataLayer || [];
    function gtag(){dataLayer.push(arguments);}
    gtag('js', new Date());
    gtag('config', 'G-????????????');
  </script>
      
  <button type="button" onClick="do_ga4_event()">
    GA4 EVENT発火
  </button>
  <script>
    function do_ga4_event(){
        gtag('event','page_view',
             {'page_location':location.href + "?status=complete"});
    }
  </script>
</body>
</html>

Step4 - GA4の画面でコンバージョン計測をテスト

「レポート」→「リアルタイム」で画面遷移し、確認

2018年の 経産省 DXレポート「2025年の崖」を2023年に読む

DXレポート ~ITシステム「2025年の崖」克服とDXの本格的な展開~(METI/経済産業省)

当時、読んだ際、「理解できるが、2025年迄にはまだ時間がある」と、 目の前の問題へのパッチ当てを優先してしまった結果、益々、時間がなくなった。

という印象。

他参考

輸出管理におけるリスト規制とキャッチオール規制、パラメータシート

輸出管理(規制?)は、法務担当ときちんと確認する必要がありますが、 すっかり忘れてしまっているので、振り返り

【目次】

導入としては「貿易アカデミー 」のyoutube

正確には? 経産省のurlを参照すべきですが、以下が分かりやすい。

www.youtube.com

www.youtube.com

3つの視点(商品、需要者、用途)で考える

視点 内容
商品 輸出許可が必要な商品か?
需要者 どのような人が使うか?
用途 どのように使われるか?

「商品」は「貨物(物)」「役務(技術)」に分け考える

例えば、PCの場合は、以下の通りで、 プログラム(ソフトウエア)は役務(えきむ)に該当。

分類 内容
貨物(物) ハードの部分
役務(技術) ソフトや説明書、仕様書

貨物は「リスト規制」「キャッチオール規制」「自由品」に分類

┌ キャッチオール規制品─────────┐
│┌ リスト既製品┐        ┌自由品┐│
││          │        │      ││
│└─────┘        └───┘│
└────────────────┘

リスト規制品

輸出許可が必要な物や技術を経産省がリスト化したもの

別表 内容
別表1 貨物・技術のマトリクス表
安全保障貿易管理**Export Control*貨物・技術のマトリクス表
別表2 輸出承認対象貨物一覧
輸出承認対象貨物一覧(METI/経済産業省)

キャッチオール規制品

「リスト規制品」以外でも「需要者」が 大量破壊や兵器開発の「用途」にするおそれがあるもの。 (殆どの工業製品が該当)

ただし、米国等の「ホワイト国( https://www.meti.go.jp/policy/anpo/anpo03.html )」は対象外

自由品

「キャッチオール規制品」にも該当しないもの。

具体的には、経産省公開するpdfの「規制の有無=✕」が該当

https://www.meti.go.jp/policy/anpo/law_document/tutatu/t07sonota/t07sonota_kanzeiteiritu.pdf

ソフトウエアの「該非判定」は「パラメータシート」で

「貨物」「技術」がリスト規制貨物等に該当するかを 「該非判定」と呼び、上記までを読んでも、 ソフトウエアのリスト規制やキャッチオール規制は理解が難しいですが、 (結局?) ソフトウエアでは主に「パラメータシート」を作成し、 該非判定するようです。

パラメータシートの参考url

「リスト規制」と「キャッチオール規制」のフロー

既製品 url
リスト規制 https://www.meti.go.jp/policy/anpo/apply01.html
キャッチオール規制 https://www.meti.go.jp/policy/anpo/kanri/catch-all/frouzu.pdf

インボイス制度 メモ

2023/10より、インボイス制度が開始されますが、 理解できていない点がありますので、自分なりにメモ

正式名称と、開始時期

  • 適格請求書等保存方式
  • 2023年10月1日 開始

影響

免税事業者(下請け業者等)と取引する場合、 売上から仕入れに要した消費税額を差し引けず(仕入税額控除)、金銭負担:増

影響を受ける人

  • 課税事業者と、課税事業者と取引のある免税事業者
  • 適格請求書発行事業者になるためには、登録申請が必要

仕入税額控除 とは

課税事業者が支払う消費税は「売上の消費税」から 「仕入や経費の消費税」を差し引き計算。

インボイス制度導入後、仕入税額控除には、必要事項記載の適格請求書 (インボイス)を保存する必要あり。

仕入税額控除の例

  • 発注者より、一次請会社(課税事業者)が税込6,600千円で請負契約。
  • 更に、一次請会社は、下請け業者へ、税込4,400千円で請負契約。

この場合、一次請会社は

  • 下請けが課税事業者の場合、600 - 400 = 200千円
  • 下請けが免税事業者の場合、600千円

の納税額となる。

課税事業者と、免税事業者

「課税事業者」とは、 消費税を除く年間の売上が10,000千円以上ある事業者で、 消費税の納税義務のある事業者。

「免税事業者」は、課税事業者以外

checkstyle によるjavaの 循環複雑度( Cyclomatic Complexity Metrics)計測

久しぶりにjavaの cyclomatic complexity metrics の計測。

当初は、以前使用した sonarqube or pmd での計測を考えましたが、 sonarqube ver.9は elasticsearch を必要になっていましたし、 pmd も「循環複雑度のみの計測」が不明でしたので、今回は、checkstyle

参考url

install

といっても、jarをdownloadするのみです。

$ cd ~/local
$ wget https://github.com/checkstyle/checkstyle/releases/download/checkstyle-10.5.0/checkstyle-10.5.0-all.jar

config

https://gist.github.com/ryan0x44/c95718cc59e987dc2d44f629433d73b6

上記urlを参考にルールファイルである checkstyle_rule.xml を作成

$ vi ~/local/checkstyle_rule.xml
<?xml version="1.0"?>
<!DOCTYPE module PUBLIC "-//Checkstyle//DTD Checkstyle Configuration 1.3//EN" "https://checkstyle.org/dtds/configuration_1_3.dtd">
<module name = "Checker">
  <property name="charset" value="UTF-8"/>
  <module name="TreeWalker">
    <module name="CyclomaticComplexity">
      <property name="max" value="1"/>
    </module>
  </module>
</module>

実行

先程のxml閾値=1にしていますので、かなりの量のエラーが表示されますが、 以下の通りです。

$ java -jar /home/end0tknr/local/checkstyle-10.5.0-all.jar \
  -c /home/end0tknr/local/checkstyle_rule.xml \
  /home/end0tknr/tmp/X-CORE

[ERROR] /SBAG029ServiceImpl.java:76:5: Cyclomatic Complexity is 3 (max allowed is 1). [CyclomaticComplexity]
[ERROR] /SBAG032ServiceImpl.java:71:5: Cyclomatic Complexity is 4 (max allowed is 1). [CyclomaticComplexity]
[ERROR] /SBAG033ServiceImpl.java:86:5: Cyclomatic Complexity is 2 (max allowed is 1). [CyclomaticComplexity]
[ERROR] /SBAG033ServiceImpl.java:128:5: Cyclomatic Complexity is 2 (max allowed is 1). [CyclomaticComplexity]

cloc によるソース行数(sloc)カウント

https://github.com/AlDanial/cloc

clocの存在は知りませんでした。

ソース行数(sloc)カウントの機能のみで、複雑度等のmetricsを計測できませんが、 yumでインストールできて、 コマンドラインでお手軽に、様々な言語を集計できます。

install

以下を見ると、分かりますが、perlで実装されているようです。

$ sudo yum install cloc
   :
===========================================================
 Package                Arch      Version        Repository
===========================================================
Installing:
 cloc                   noarch    1.70-1.el7         epel  
Installing for dependencies:
 perl-Algorithm-Diff    noarch    1.1902-17.el7      base  
 perl-Regexp-Common     noarch    2013031301-1.el7   epel  
===========================================================

$ /usr/bin/cloc --help

実行例

言語別や、ファイル別で集計結果を表示できます。

$ /usr/bin/cloc /tmp/myproj/src
----------------------------------------------------------------------------------------
Language                              files          blank        comment           code
----------------------------------------------------------------------------------------
Java                                  20553         642989        1308923        2827434
JavaScript                              685          18661          51139         189923
JSP                                    1023           4447          12051         127191
SQL                                    1612            690          23669         124569
CSS                                     616            653           3746          25792
XML                                      93            749           1519           8374
Velocity Template Language              179            803            371           8076
Ant                                      21            268            122           1865
Ruby                                     17            545           1473           1815
HTML                                     11             99             39            694
PowerShell                                1             30             55            370
DOS Batch                                 6             29             24            368
PHP                                       1             15              0            274
Visualforce Component                    13              0              0            238
Perl                                      9             27             27            162
JSON                                      2              1              0            107
Markdown                                  3             44              0             65
Python                                    1             14              2             32
Bourne Shell                              7              6              0             27
YAML                                      3              1              1             15
----------------------------------------------------------------------------------------
SUM:                                  24856         670071        1403161        3317391
----------------------------------------------------------------------------------------
$ /usr/bin/cloc --by-file-by-lang .

pmd による java 循環的複雑度(code metrics CyclomaticComplexity ) 2022年版

久しぶりにpmdを操作したところ、以前のバージョンと比較して、 引数が仕様変更されていましたので、メモ

$ cd ~/local
$ wget https://github.com/pmd/pmd/releases/download/pmd_releases%2F6.52.0/pmd-bin-6.52.0.zip
$ unzip pmd-bin-6.52.0.zip
$ ln -s pmd-bin-6.52.0 pmd
$ /home/end0tknr/local/pmd/bin/run.sh pmd \
   --dir ./app/ag/src/main/java \
   --format text \
   --rulesets rulesets/java/codesize.xml

参考url

gantt chart by vue.js 3

上記urlの写経です。

先日の工程計画作成pythonと併せて、 AI、かつ、インタラクティブな、工程計画ガントチャートwebアプリを 完成できると思います。

その他、Frappe Gantt( ※1 )のように先行&後続taskを、svgによる線で連結させたい。

※1 https://zenn.dev/phi/articles/how-to-use-frappe-gantt-js

https://end0tknr.github.io/sandbox/vue_js_3/vue_gantt.html

↓こうかくと、↑こう表示されます

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <script src="https://unpkg.com/vue@next"></script>
  <link href="https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css" rel="stylesheet">
  <script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.1/moment.min.js"></script>
  <title>スクラッチから作るガントチャート</title>
  <style>
    .base {
        position: fixed;
        top: 0;
        left: 0;
        right: 0;
        display: flex;
        justify-content: center;
        margin-top: 50px;
    }
    
    .overlay {
        position: fixed;
        top: 0;
        left: 0;
        right: 0;
        bottom: 0;
        background-color: gray;
        opacity: 0.5;
    }
    
    .content {
        background-color: white;
        position: relative;
        border-radius: 10px;
        padding: 40px;
    }
  </style>
</head>
<body>

<div id="app">
  <div id="gantt-header" class="h-12 p-2 flex items-center">
    <h1 class="text-sm font-bold">ガントチャート</h1>
    <button
      @click="addTask"
      class="bg-indigo-700 hover:bg-indigo-900 text-white py-2 px-4 rounded-lg flex items-center">
      <svg class="w-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
      </svg>
      <span class="font-bold text-xs">
        タスクを追加する
      </span>
    </button>
    cf. https://reffect.co.jp/vue/vue-js-calendar-from-scratch
    
    <teleport to="#form">
      <div class="base" v-show="show">
        <div class="overlay" v-show="show" @click="show=false">
        </div>
        <div class="content" v-show="show">

          <h2 class="font-bold" v-if="update_mode">タスクの更新</h2>
          <h2 class="font-bold" v-else>タスクの追加</h2>

          <div class="my-4">
            <label class="text-xs">カテゴリーID:</label>
            <select v-model="form.category_id" class="text-xs border px-4 py-2 rounded-lg">
              <option v-for="category in categories" :key="category.id" :value="category.id">{{ category.name }}
              </option>
            </select>
          </div>
          <div class="my-4">
            <label class="text-xs">ID:</label>
            <input class="text-xs border rounded-lg px-4 py-2" v-model.number="form.id">
          </div>
          <div class="my-4">
            <label class="text-xs">タスク名:</label>
            <input class="text-xs border rounded-lg px-4 py-2" v-model="form.name">
          </div>
          <div class="my-4">
            <label class="text-xs">担当者:</label>
            <input class="text-xs border rounded-lg px-4 py-2" v-model="form.incharge_user">
          </div>
          <div class="my-4">
            <label class="text-xs">開始日:</label>
            <input class="text-xs border rounded-lg px-4 py-2" v-model="form.start_date" type="date">
          </div>
          <div class="my-4">
            <label class="text-xs">完了期限日:</label>
            <input class="text-xs border rounded-lg px-4 py-2" v-model="form.end_date" type="date">
          </div>
          <div class="my-4">
            <label class="text-xs">進捗度:</label>
            <input class="text-xs border rounded-lg px-4 py-2" v-model="form.percentage" type="number">
          </div>

          <div v-if="update_mode" class="flex items-center justify-between">
            <button
              @click="updateTask(form.id)"
              class="bg-green-500 hover:bg-green-700 text-white font-bold py-2 px-4 rounded-lg text-xs flex items-center">
              <svg class="w-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
                      d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
              </svg>
              <span class="text-xs font-bold text-white">タスクを更新</span>
            </button>
            <button @click="deleteTask(form.id)"
                    class="bg-red-500 hover:bg-red-700 text-white py-2 px-4 rounded-lg flex items-center ml-2">
              <svg class="w-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
                      d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
              </svg>
              <span class="text-xs font-bold text-white">タスクを削除</span>
            </button>
          </div>
          <div v-else>
            <button
              @click="saveTask"
              class="bg-indigo-500 hover:bg-indigo-700 text-white font-bold py-2 px-4 rounded-lg flex items-center">
              <svg class="w-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
              </svg>
              <span class="font-bold text-xs">
                タスクを追加する
              </span>
            </button>
          </div>
        </div>
      </div>
    </teleport>
    
  </div>
  
  <div id="gantt-content" class="flex">
    <div id="gantt-task">
      <div id="gantt-task-title" class="flex items-center bg-green-600 text-white h-20" ref="task">
        <div class="border-t border-r border-b flex items-center justify-center font-bold text-xs w-48 h-full">タスク
        </div>
        <div class="border-t border-r border-b flex items-center justify-center font-bold text-xs w-24 h-full">開始日
        </div>
        <div class="border-t border-r border-b flex items-center justify-center font-bold text-xs w-24 h-full">完了期限日
        </div>
        <div class="border-t border-r border-b flex items-center justify-center font-bold text-xs w-16 h-full">担当
        </div>
        <div class="border-t border-r border-b flex items-center justify-center font-bold text-xs w-12 h-full">進捗
        </div>
      </div>

      <div id="gantt-task-list" class="overflow-y-hidden" :style="`height:${calendarViewHeight}px`">
        
        <div v-for="(task, index) in displayTasks" 
             :key="index" 
             class="flex h-10 border-b"
             @dragstart="dragTask(task)"
             @dragover.prevent="dragTaskOver(task)"
             draggable=true>
  
          <template v-if="task.cat === 'category'">
            <div class="flex items-center font-bold w-full text-sm pl-2 flex justify-between items-center bg-teal-100">
              <span>{{task.name}}</span>
              <div class="pr-4" @click="toggleCategory(task.id)">
                <span v-if="task.collapsed">
                  <svg class="w-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
                       stroke="currentColor">
                    <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
                  </svg>
                </span>
                <span v-else>
                  <svg class="w-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
                       stroke="currentColor">
                    <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
                  </svg>
                </span>
              </div>
            </div>
          </template>
          <template v-else>
            <div
              @click="editTask(task)"
              class="border-r flex items-center font-bold w-48 text-sm pl-4">
              {{task.name}}
            </div>
            <div class="border-r flex items-center justify-center w-24 text-sm">
              {{task.start_date}}
            </div>
            <div class="border-r flex items-center justify-center w-24 text-sm">
              {{task.end_date}}
            </div>
            <div class="border-r flex items-center justify-center w-16 text-sm">
              {{task.incharge_user}}
            </div>
            <div class="flex items-center justify-center w-12 text-sm">
              {{task.percentage}}%
            </div>
          </template>
        </div>
      </div>
    </div>

    <div id="gantt-calendar" class="overflow-x-scroll overflow-y-hidden border-l" :style="`width:${calendarViewWidth}px`" ref="calendar">      
      <div id="gantt-date" class="h-20">
        <div id="gantt-year-month" class="relative h-8">
          <div v-for="(calendar,index) in calendars" :key="index">
            <div
              class="bg-indigo-700 text-white border-b border-r border-t h-8 absolute font-bold text-sm flex items-center justify-center"
              :style="`width:${calendar.calendar*block_size}px;left:${calendar.start_block_number*block_size}px`">
              {{calendar.date}}
            </div>
          </div>
        </div>
        
        <div id="gantt-day" class="relative h-12">
          <div v-for="(calendar,index) in calendars" :key="index">
            <div v-for="(day,index) in calendar.days" :key="index">
              <div class="border-r border-b h-12 absolute flex items-center justify-center flex-col font-bold text-xs"
                   :class="{'bg-blue-100': day.dayOfWeek === '土', 'bg-red-100': day.dayOfWeek ==='日',
                           'bg-red-600 text-white': calendar.year=== today.year() && calendar.month === today.month() && day.day === today.date()}"
                   :style="`width:${block_size}px;left:${day.block_number*block_size}px`">
                <span>{{ day.day }}</span>
                <span>{{ day.dayOfWeek }}</span>
              </div>
            </div>
          </div>
        </div>
        
        <div id="gantt-height" class="relative">
          <div v-for="(calendar,index) in calendars" :key="index">
            <div v-for="day in calendar.days" :key="index">
              <div class="border-r border-b absolute"
                   :class="{'bg-blue-100': day.dayOfWeek === '土', 'bg-red-100': day.dayOfWeek ==='日'}"
                   :style="`width:${block_size}px;left:${day.block_number*block_size}px;height:${calendarViewHeight}px`">
              </div>
            </div>
          </div>
        </div>
        
      </div>

      <div id="gantt-bar-area" class="relative" :style="`width:${calendarViewWidth}px;height:${calendarViewHeight}px`">
        <div v-for="(bar,index) in taskBars" :key="index">
          <div :style="bar.style"
               class="rounded-lg absolute h-5 bg-yellow-100"
               v-if="bar.task.cat === 'task'"
               @mousedown="mouseDownMove(bar.task)">
            
            <div class="w-full h-full" style="pointer-events: none;">
              <div class="h-full bg-yellow-500 rounded-l-lg" 
                   style="pointer-events: none;" 
                   :style="`width:${bar.task.percentage}%`"
                   :class="{'rounded-r-lg': bar.task.percentage === 100}"></div>
            </div>

            <div class="absolute w-2 h-2 bg-gray-300 border border-black" 
                 style="top:6px;left:-6px;cursor:col-resize" 
                 @mousedown.stop="mouseDownResize(bar.task,'left')">
            </div>
            <div class="absolute w-2 h-2 bg-gray-300 border border-black" 
                 style="top:6px;right:-6px;cursor:col-resize" 
                 @mousedown.stop="mouseDownResize(bar.task,'right')">
            </div>
            
          </div>
        </div>
      </div>
      
    </div>
    
  </div>

  
</div>

<div id="form">
</div>

</body>
</html>
<script>
    const app = Vue.createApp({
        data(){
            return {
                start_month: '2020-10',
                end_month: '2021-02',
                block_size: 30,
                block_number: 0,
                calendars:[],
                inner_width: '',
                inner_height: '',
                task_width: '',
                task_height: '',
                today:moment(),
                position_id:0,
                dragging:false,
                pageX:'',
                elememt:'',
                left:'',
                task_id:'',
                width:'',
                leftResizing:false,
                rightResizing:false,
                task:'',
                show:false,
                update_mode:false,
                form: {
                    category_id: '',
                    id: '',
                    name: '',
                    start_date: '',
                    end_date: '',
                    incharge_user: '',
                    percentage: 0
                },
                categories: [
                    {id: 1, name: 'テストA', collapsed: false, },
                    {id: 2, name: 'テストB', collapsed: false, }
                ],
                tasks: [
                    {id: 1, category_id: 1, name: 'テスト1',
                     start_date: '2020-11-18', end_date: '2020-11-20',
                     incharge_user: '鈴木', percentage: 100,},
                    {id: 2, category_id: 1, name: 'テスト2',
                     start_date: '2020-11-19', end_date: '2020-11-23',
                     incharge_user: '佐藤', percentage: 90, },
                    {id: 3, category_id: 1, name: 'テスト3',
                     start_date: '2020-11-19', end_date: '2020-12-04',
                     incharge_user: '鈴木', percentage: 40, },
                    {id: 4, category_id: 1, name: 'テスト4',
                     start_date: '2020-11-21',end_date: '2020-11-30',
                     incharge_user: '山下', percentage: 60, },
                    {id: 5, category_id: 1, name: 'テスト5',
                     start_date: '2020-11-25', end_date: '2020-12-04',
                     incharge_user: '佐藤', percentage: 5, },
                    {id: 6, category_id: 2, name: 'テスト6',
                     start_date: '2020-11-28', end_date: '2020-12-08',
                     incharge_user: '佐藤', percentage: 0, },
                ],
            }
        },

        methods:{
            deleteTask(id) {
                let delete_index;
                this.tasks.map((task, index) => {
                    if (task.id === id) delete_index = index;
                })
                this.tasks.splice(delete_index, 1)
                this.form = {}
                this.show = false;
            },
            
            updateTask(id) {
                let task = this.tasks.find(task => task.id === id);
                Object.assign(task, this.form);
                this.form = {}
                this.show = false;
            },
            
            editTask(task){
                this.update_mode=true;
                this.show = true;
                Object.assign(this.form, task);
            },
            
            saveTask() {
                this.tasks.push(
                    this.form
                )
                this.form = {}
                this.show = false
            },
            
            addTask(){
                this.update_mode = false;
                this.form = {}
                this.show = true;
            },
            
            toggleCategory(task_id) {
                let category = this.categories.find(category => category.id === task_id)
                category['collapsed'] = !category['collapsed'];
            },
            
            dragTaskOver(overTask) {
                let deleteIndex;
                let addIndex;
                if (this.task.cat !== 'category') {
                    if (overTask.cat === 'category') {
                        let updateTask = this.tasks.find(task => task.id === this.task.id)
                        updateTask['category_id'] = overTask['id']
                    } else {
                        if (overTask.id !== this.task.id) {
                            this.tasks.map((task, index) => { if (task.id === this.task.id) deleteIndex = index })
                            this.tasks.map((task, index) => { if (task.id === overTask.id) addIndex = index })
                            this.tasks.splice(deleteIndex, 1)
                            this.task['category_id'] = overTask['category_id']
                            this.tasks.splice(addIndex, 0, this.task)
                        }
                    }
                }
            },      
            dragTask(dragTask) {
                this.task = dragTask;
            },
            
            mouseResize() {
                if (this.leftResizing) {
                    let diff = this.pageX - event.pageX
                    if (parseInt(this.width.replace('px', '')) + diff > this.block_size) {
                        this.element.style.width = `${parseInt(this.width.replace('px', '')) + diff}px`
                        this.element.style.left = `${this.left.replace('px', '') - diff}px`;
                    }
                }
                if (this.rightResizing) {
                    let diff = this.pageX - event.pageX;
                    if (parseInt(this.width.replace('px', '')) - diff > this.block_size) {
                        this.element.style.width = `${parseInt(this.width.replace('px', '')) - diff}px`
                    }
                }
            },
            
            mouseDownResize(task, direction) {
                direction === 'left' ? this.leftResizing = true : this.rightResizing = true;
                this.pageX = event.pageX;
                this.width = event.target.parentElement.style.width;
                this.left = event.target.parentElement.style.left;
                this.element = event.target.parentElement;
                this.task_id = task.id
            },
            
            stopDrag(){
                if (this.dragging) {
                    let diff = this.pageX - event.pageX
                    let days = Math.ceil(diff / this.block_size)
                    if (days !== 0) {
                        console.log(days)
                        let task = this.tasks.find(task => task.id === this.task_id);
                        let start_date = moment(task.start_date).add(-days, 'days')
                        let end_date = moment(task.end_date).add(-days, 'days')
                        task['start_date'] = start_date.format('YYYY-MM-DD')
                        task['end_date'] = end_date.format('YYYY-MM-DD')
                    } else {
                        this.element.style.left = `${this.left.replace('px', '')}px`;
                    }
                }
                if (this.leftResizing) {
                    let diff = this.pageX - event.pageX;
                    let days = Math.ceil(diff / this.block_size)
                    if (days !== 0) {
                        let task = this.tasks.find(task => task.id === this.task_id);
                        let start_date = moment(task.start_date).add(-days, 'days')
                        let end_date = moment(task.end_date)
                        if (end_date.diff(start_date, 'days') <= 0) {
                            task['start_date'] = end_date.format('YYYY-MM-DD')
                        } else {
                            task['start_date'] = start_date.format('YYYY-MM-DD')
                        }
                    } else {
                        this.element.style.width = this.width;
                        this.element.style.left = `${this.left.replace('px', '')}px`;
                    }
                }
                if (this.rightResizing) {
                    let diff = this.pageX - event.pageX;
                    let days = Math.ceil(diff / this.block_size)
                    if (days === 1) {
                        this.element.style.width = `${parseInt(this.width.replace('px', ''))}px`;
                    } else if (days <= 2) {
                        days--;
                        let task = this.tasks.find(task => task.id === this.task_id);
                        let end_date = moment(task.end_date).add(-days, 'days')
                        task['end_date'] = end_date.format('YYYY-MM-DD')
                    } else {
                        let task = this.tasks.find(task => task.id === this.task_id);
                        let start_date = moment(task.start_date);
                        let end_date = moment(task.end_date).add(-days, 'days')
                        if (end_date.diff(start_date, 'days') < 0) {
                            task['end_date'] = start_date.format('YYYY-MM-DD')
                        } else {
                            task['end_date'] = end_date.format('YYYY-MM-DD')
                        }
                    }
                }
                this.dragging = false;
                this.leftResizing = false;
                this.rightResizing = false;
            },
            
            mouseMove() {
                if (this.dragging) {
                    let diff = this.pageX - event.pageX;
                    this.element.style.left = `${parseInt(this.left.replace('px', '')) - diff}px`;
                }
            },
            
            mouseDownMove(task){
                this.dragging = true;
                this.pageX = event.pageX;
                this.element = event.target;
                this.left = event.target.style.left;
                this.task_id = task.id
                console.log('mouseDownMove')
            },
            
            windowSizeCheck() {
                let height = this.lists.length - this.position_id
                if (event.deltaY > 0 && height * 40 > this.calendarViewHeight) {
                    this.position_id++
                } else if (event.deltaY < 0 && this.position_id !== 0) {
                    this.position_id--
                }
            },
            
            getDays(year, month, block_number) {
                const dayOfWeek = ['日', '月', '火', '水', '木', '金', '土'];
                let days = [];
                let date = moment(`${year}-${month}-01`);
                let num = date.daysInMonth();
                for (let i = 0; i < num; i++) {
                    days.push({
                        day: date.date(),
                        dayOfWeek: dayOfWeek[date.day()],
                        block_number
                    })
                    date.add(1, 'day');
                    block_number++;
                }
                return days;
            },

            getCalendar() {
                let block_number = 0;
                let days;
                let start_month = moment(this.start_month)
                let end_month = moment(this.end_month)
                let between_month = end_month.diff(start_month, 'months')
                for (let i = 0; i <= between_month; i++) {
                    days = this.getDays(start_month.year(), start_month.format('MM'), block_number);
                    this.calendars.push({
                        date: start_month.format('YYYY年MM月'),
                        year: start_month.year(),
                        month: start_month.month(), //month(), 0,1..11と表示
                        start_block_number: block_number,
                        calendar: days.length,
                        days: days
                    })
                    start_month.add(1, 'months')
                    block_number = days[days.length - 1].block_number
                    block_number++;
                }
                return block_number;
            },

            getWindowSize() {
                this.inner_width = window.innerWidth;
                this.inner_height = window.innerHeight;
                this.task_width = this.$refs.task.offsetWidth;
                this.task_height = this.$refs.task.offsetHeight;
            },

            todayPosition() {
                this.$refs.calendar.scrollLeft = this.scrollDistance
            },
        },
        computed: {
            displayTasks() {
                let display_task_number = Math.floor(this.calendarViewHeight / 40);
                return this.lists.slice(this.position_id, this.position_id + display_task_number);
            },
            
            taskBars() {
                let start_date = moment(this.start_month);
                let top = 10;
                let left;
                let between;
                let start;
                let style;
                return this.displayTasks.map(task => {
                    style = {}
                    if(task.cat==='task'){
                        let date_from = moment(task.start_date);
                        let date_to = moment(task.end_date);
                        between = date_to.diff(date_from, 'days');
                        between++;
                        start = date_from.diff(start_date, 'days');
                        left = start * this.block_size;
                        style = {
                            top: `${top}px`,
                            left: `${left}px`,
                            width: `${this.block_size * between}px`,
                        }
                    }
                    top = top + 40;
                    return {
                        style,
                        task
                    }
                })
            },
            
            calendarViewWidth() {
                return this.inner_width - this.task_width;
            },

            calendarViewHeight() {
                return this.inner_height - this.task_height - 48 - 20;
            },

            scrollDistance() {
                let start_date = moment(this.start_month);
                let between_days = this.today.diff(start_date, 'days')
                return between_days * this.block_size;
            },

            scrollDistance() {
                let start_date = moment(this.start_month);
                let between_days = this.today.diff(start_date, 'days')
                return (between_days + 1) * this.block_size - this.calendarViewWidth / 2;
            },
            lists() {
                let lists = [];
                this.categories.map(category => {
                    lists.push({ cat: 'category', ...category });
                    this.tasks.map(task => {
                        if (task.category_id === category.id && !category.collapsed) {
                            lists.push({ cat: 'task', ...task })
                        }
                    })
                })
                return lists;
            },
        },
        
        mounted() {
            this.getCalendar();
            this.getWindowSize();
            this.$nextTick(() => {
                this.todayPosition();
            });
            window.addEventListener('resize', this.getWindowSize);
            window.addEventListener('wheel', this.windowSizeCheck);
            window.addEventListener('mousemove', this.mouseMove);
            window.addEventListener('mousemove', this.mouseResize);
            
            window.addEventListener('mouseup', this.stopDrag);
        }
    }).mount('#app')
</script>

pythonによる工程計画の自動作成(AI?) - その2

pythonによる工程計画の自動作成 - end0tknr's kipple - web写経開発

上記 entry の続きです。

先程までは、プロジェクト自体?の休日を考慮し、 各タスクの着手日、完工日を算出していましたが、 更に、作業者のアサイン可否を考慮してみた。

平行する複数プロジェクトの工程計画など、 実務に使用するには、機能強化すべき点はありますが、今回はここまで。

$ python3 build_project.py
2022-12-26 00:00:00 2023-04-05 00:00:00
1       内装    None    2022-12-26      2023-02-02
1.1     内装    8       2022-12-26      2023-01-10
1.2     外構    0       2023-01-11      2023-01-11
1.3     電気    5       2023-01-11      2023-01-19
1.4     外構    1       2023-01-20      2023-01-20
1.5     外構    8       2023-01-23      2023-02-02
2       外構    None    2023-02-03      2023-03-03
2.1     内装    6       2023-02-03      2023-02-10
2.2     電気    3       2023-02-13      2023-02-15
2.3     電気    0       2023-02-13      2023-02-13
2.4     外構    5       2023-02-13      2023-02-17
2.5     外構    8       2023-02-17      2023-03-03
3       電気    None    2023-03-06      2023-04-05
3.1     内装    9       2023-03-06      2023-03-16
3.2     電気    9       2023-03-17      2023-03-30
3.3     内装    None    2023-03-20      2023-04-05
3.3.1   外構    6       2023-03-20      2023-03-28
3.3.2   外構    5       2023-03-29      2023-04-05
3.3.3   外構    3       2023-03-29      2023-03-31

外構 2023-01-30 ['1.5', '-', '-']
外構 2023-02-21 ['2.4', '-']
<略>
内装 2022-12-26 []
内装 2022-12-27 ['1.1', '-']
<略>
電気 2023-01-12 ['1.2', '-', '-']
電気 2023-03-29 ['3.3.3']
<略>

↓こう書くと、↑こう表示されます

build_project.py

# -*- coding: utf-8 -*-
import datetime
import os
import sys
sys.path.append( os.path.join(os.path.dirname(__file__), './lib') )
from project import Project
from team    import Team

def main():
    proj = Project()

    teams = {}
    man_days = proj.man_days_group_by_type()
    for type, man_days in man_days.items():
        proj.teams[type] = Team(type)
    
    start_date,goal_date = proj.calc_project_period("2022-12-25")
    print( start_date,goal_date )
    for task_id, task in proj.tasks.items():

        disp_str = "%s\t%s\t%s\t%s\t%s" % \
            (task.id,
             task.type,
             task.man_days,
             task.start_date.strftime('%Y-%m-%d'),
             task.goal_date.strftime('%Y-%m-%d'))
        print( disp_str )

    for type, team in proj.teams.items():
        for date, assign in team.assigns.items():
            print(type, date.strftime('%Y-%m-%d'), assign )

if __name__ == '__main__':
    main()

lib/project.py

# -*- coding: utf-8 -*-
import datetime
import dateutil.parser
import uuid
import sys
from bizcalendar import Calendar
from task        import Task

#┌1 ────────┐┌2 ───────┐┌3 ─────────┐
#│1.1┳1.2┳1.4━1.5┝┥2.1┳2.2┳2.5 ┳┝┥3.1┳3.2 ━━━━┳ │
#│   ┗1.3┛        ││   ┣2.3┛    ┃││   ┗3.3.1┳3.3.2┫ │
#│                  ││   ┗2.4━━━┛││          ┗3.3.3┛ │
#└─────────┘└────────┘└──────────┘
default_tasks = [
    # id     pre task      parent
    ["1",    {},           ""    ],
    ["1.1",  {},           "1"   ],
    ["1.2",  {"1.1"},      "1"   ],
    ["1.3",  {"1.1"},      "1"   ],
    ["1.4",  {"1.2","1.3"},"1"   ],
    ["1.5",  {"1.4"},      "1"   ],
    ["2",    {"1"},        ""    ],
    ["2.1",  {},           "2"   ],
    ["2.2",  {"2.1"},      "2"   ],
    ["2.3",  {"2.1"},      "2"   ],
    ["2.4",  {"2.1"},      "2"   ],
    ["2.5",  {"2.2","2.3"},"2"   ],
    ["3",    {"2"},        ""    ],
    ["3.1",  {},           "3"   ],
    ["3.2",  {"3.1"},      "3"   ],
    ["3.3",  {"3.1"},      "3"   ],
    ["3.3.1",{},           "3.3" ],
    ["3.3.2",{"3.3.1"},    "3.3" ],
    ["3.3.3",{"3.3.1"},    "3.3" ],
]
max_man_days = 100       # 無限loopを避ける為

class Project():
    def __init__(self):
        self.id           = uuid.uuid1()
        self.calendar     = Calendar()
        self.start_date   = None
        self.goal_date    = None
        self.tasks        = {}
        self.teams        = {}
        self.max_man_days = max_man_days

        for task_info in default_tasks:
            task = Task( task_info[0],task_info[1],task_info[2] )
            self.tasks[task.id] = task

        # 先行&後続、親&子 の関係を双方向list化
        for task_id, task in self.tasks.items():
            # 後続taskとして登録
            for pre_id in task.pre_ids:
                self.tasks[pre_id].next_ids.add( task_id )

            # root taskに親taskはありません
            if not task.parent_id:
                continue
            self.tasks[task.parent_id].child_ids.add( task_id )
            
        # 子taskがある場合、工数は子taskに依存
        for task_id, task in self.tasks.items():
            if len( task.child_ids ):
                task.man_days = None

    def calc_project_period(self,start_date_str):
        self.start_date = dateutil.parser.parse( start_date_str )
        root_task_ids = self.find_root_task_ids()
        start_date,goal_date = self.calc_tasks_period( root_task_ids )
        return start_date,goal_date

    def calc_tasks_period(self,org_task_ids):
        func_name = sys._getframe().f_code.co_name
        
        ret_start_date = None
        ret_goal_date  = None
        task_ids = self.find_first_task_ids(org_task_ids)
        
        while len(task_ids):
            next_ids = set()
            
            for task_id in task_ids:
                task = self.tasks[task_id]
                start_goal_date = self.calc_task_period(task)
                task.start_date = start_goal_date[0]
                task.goal_date  = start_goal_date[1]

                if not ret_start_date or task.start_date < ret_start_date:
                    ret_start_date = task.start_date
                if not ret_goal_date or ret_goal_date < task.goal_date:
                    ret_goal_date = task.goal_date
                    
                next_ids.update( task.next_ids )
            task_ids = self.select_next_task(next_ids)
        return ret_start_date, ret_goal_date

    def select_next_task(self,next_ids):
        task_ids = set()
        for next_id in next_ids:
            next_task = self.tasks[next_id]
            goal_dates = []
            for pre_id in next_task.pre_ids:
                pre_task = self.tasks[pre_id]
                
                if self.tasks[pre_id].goal_date:
                    goal_dates.append(pre_task.goal_date)

            if len(goal_dates) == len(next_task.pre_ids):
                task_ids.add(next_id)

            next_task.start_date = max(goal_dates) + datetime.timedelta(days=1)
        return task_ids
                
    def get_default_start_date(self, task):
        func_name = sys._getframe().f_code.co_name
        
        goal_dates = []
        # 先行taskがある場合
        for pre_id in task.pre_ids:
            pre_task = self.tasks[pre_id]
            
            if not pre_task.goal_date:
                return None
            goal_dates.append( pre_task.goal_date )
            
        if len( goal_dates ):
            start_date = max(goal_dates) + datetime.timedelta(days=1)
            return start_date

        # 親taskがある場合
        if task.parent_id:
            parent_task = self.tasks[task.parent_id]
            return self.get_default_start_date( parent_task )

        # root taskを参照
        return self.start_date
            
    def calc_task_period(self, task):
        func_name = sys._getframe().f_code.co_name
        
        default_start_date = self.get_default_start_date( task )

        # 子taskがある場合
        if len(task.child_ids):
            return self.calc_tasks_period(task.child_ids)
        
        start_date = self.calc_task_start_date(task, default_start_date)
        if not start_date:
            return [None,None]

        team = self.teams[ task.type ]

        assign_dates = team.assign_dates(self, task, start_date)
        return assign_dates
        # goal_date = self.calc_task_goal_date(task, start_date)
        # return [start_date,goal_date]
        
    def is_biz_day(self, date):
        return self.calendar.is_biz_day( date )
    
    def calc_task_start_date(self, task, start_date):
        ret_date = start_date
        while (ret_date - start_date).days < self.max_man_days:
            if self.is_biz_day( ret_date ):
                return ret_date
            
            ret_date = ret_date + datetime.timedelta(days=1)
        return None

    # def calc_task_goal_date(self, task, start_date):
    #     business_days = 0
    #     ret_date = start_date
    #     while (ret_date - start_date).days < self.max_man_days:
    #         if self.is_biz_day( ret_date ):
    #             business_days += 1
    #         if task.man_days <= business_days:
    #             return ret_date
    #         ret_date = ret_date + datetime.timedelta(days=1)
    #     return None
    
    def find_first_task_ids(self, task_ids):
        ret_ids = set()
        for task_id in task_ids:
            
            task = self.tasks[task_id]
            can_add = True
            
            for pre_id in task.pre_ids:
                pre_task = self.tasks[pre_id]
                
                if pre_task.parent_id == task.parent_id:
                    can_add = False
                    break
            if can_add == True:
                ret_ids.add(task_id)
        return ret_ids
                
    def find_root_task_ids(self):
        root_task_ids = []
        for id, task in self.tasks.items():
            if not task.parent_id and len(task.pre_ids) == 0:
                root_task_ids.append( id )
        return root_task_ids

    def man_days_group_by_type(self):
        ret_datas = {}
        for i, task in self.tasks.items():
            if not task.type in ret_datas.keys():
                ret_datas[task.type] = 0

            if task.man_days:
                ret_datas[task.type] += task.man_days
        return ret_datas

lib/task.py

# -*- coding: utf-8 -*-
import random
import uuid

class Task():
    def __init__(self,id, pre_ids,parent_id):
        if id:
            self.id = id
        else:
            self.id = uuid.uuid1()
            
        self.pre_ids    = pre_ids    # 前工程
        self.next_ids   = set()      # 後工程
        self.parent_id  = parent_id  # 親工程(1コ)
        self.child_ids  = set()      # 子工程
        self.start_date = None
        self.goal_date  = None
        self.progress   = 0          # 進捗 %
        # 標準工数 人日
        self.man_days   = random.randint(0, 10)
        # 職種
        self.type       = ["電気","内装","外構"][random.randint(0, 2)]

lib/bizcalendar.py

# -*- coding: utf-8 -*-
import datetime
import jpholiday

class Calendar():
    def __init__(self):
        pass

    def is_biz_day(self,date):
        if date.weekday() >= 5 or jpholiday.is_holiday(date):
            return False
        return True

lib/team.py

# -*- coding: utf-8 -*-
import datetime
import random
import uuid
from bizcalendar import Calendar

workers_size = 3

# 職人を抱える工務店? と考えてください
class Team():
    
    def __init__(self, type):
        self.id       = uuid.uuid1()
        self.calendar = Calendar()
        self.type     = type # 電気, 内装, 外構
        self.assigns  = {}   # ココへ、日別x職人xtaskを登録

    def assign_dates(self, proj, task, start_date):
        assign_dates = {} # アサインできる日付群
        tmp_date = start_date

        while (tmp_date - start_date).days < proj.max_man_days:
            # プロジェクト自体の定休日
            if not proj.is_biz_day( tmp_date ):
                tmp_date = tmp_date + datetime.timedelta(days=1)
                continue

            woker_no = self.chk_assign_date(tmp_date)
            # アサインできない場合
            if woker_no == None:
                tmp_date = tmp_date + datetime.timedelta(days=1)
                continue
            assign_dates[ tmp_date ] = woker_no

            if len( assign_dates ) >= task.man_days:
                break
            
            tmp_date = tmp_date + datetime.timedelta(days=1)

        if len( assign_dates ) < task.man_days:
            return []

        self.commit_assign_dates(task, assign_dates )
        
        ret_start_date = min(assign_dates)
        ret_goal_date  = max(assign_dates)
        return [ret_start_date,ret_goal_date]

    def commit_assign_dates(self, task, assign_dates ):
        for date, woker_no in assign_dates.items():
            self.assigns[date][woker_no] = task.id
        
    def chk_assign_date(self, date):
        # 予定が未定の場合、予定を初期化
        if not date in self.assigns:
            self.assigns[date] = self.init_assigns( date )
            
        for i, assign in enumerate( self.assigns[date] ):
            if assign == "-":
                return i
        return None
    
    def init_assigns(self,date):
        if not self.is_biz_day(date):
            return [] # 定休日
        
        # "-"を空きのある作業者と考えます
        workers = []
        for i in range( random.randint(0,workers_size) ):
            workers.append("-")
        return workers
    
    def is_biz_day(self, date):
        return self.calendar.is_biz_day( date )

pythonによる工程計画の自動作成

まずは、

  • 各taskは、親子関係がある
  • 各taskの工数(人日)は、random.randint()で
  • 各taskは、土日や祝日を休み

で考えてみた ( 次は、リソース(人)の割り当てを考える )

$ python3 build_project.py
2022-12-26 00:00:00 2023-02-24 00:00:00
1       None    2022-12-26      2023-01-18
1.1     1       2022-12-26      2022-12-26
1.2     6       2022-12-27      2023-01-04
1.3     10      2022-12-27      2023-01-11
1.4     0       2023-01-12      2023-01-12
1.5     4       2023-01-13      2023-01-18
2       None    2023-01-19      2023-02-02
2.1     1       2023-01-19      2023-01-19
2.2     8       2023-01-20      2023-01-31
2.3     3       2023-01-20      2023-01-24
2.4     6       2023-01-20      2023-01-27
2.5     2       2023-02-01      2023-02-02
3       None    2023-02-03      2023-02-24
3.1     1       2023-02-03      2023-02-03
3.2     0       2023-02-06      2023-02-06
3.3     None    2023-02-06      2023-02-24
3.3.1   5       2023-02-06      2023-02-10
3.3.2   9       2023-02-13      2023-02-24
3.3.3   9       2023-02-13      2023-02-24

↓こう書くと、↑こう表示されます

build_project.py

# -*- coding: utf-8 -*-
import datetime
import os
import sys
sys.path.append( os.path.join(os.path.dirname(__file__), './lib') )
from project import Project

def main():
    proj = Project()
    
    start_date,goal_date = proj.calc_project_period("2022-12-25")
    print( start_date,goal_date )
    for task_id, task in proj.tasks.items():

        disp_str = "%s\t%s\t%s\t%s" % (task.id,
                                       task.man_days,
                                       task.start_date.strftime('%Y-%m-%d'),
                                       task.goal_date.strftime('%Y-%m-%d'))
        print( disp_str )

if __name__ == '__main__':
    main()

lib/project.py

# -*- coding: utf-8 -*-

import datetime
import dateutil.parser
import uuid
import sys
from bizcalendar import Calendar
from task     import Task

#┌1 ────────┐┌2 ───────┐┌3 ─────────┐
#│1.1┳1.2┳1.4━1.5┝┥2.1┳2.2┳2.5 ┳┝┥3.1┳3.2 ━━━━┳ │
#│   ┗1.3┛        ││   ┣2.3┛    ┃││   ┗3.3.1┳3.3.2┫ │
#│                  ││   ┗2.4━━━┛││          ┗3.3.3┛ │
#└─────────┘└────────┘└──────────┘
default_tasks = [
    # id     pre task      parent
    ["1",    {},           ""    ],
    ["1.1",  {},           "1"   ],
    ["1.2",  {"1.1"},      "1"   ],
    ["1.3",  {"1.1"},      "1"   ],
    ["1.4",  {"1.2","1.3"},"1"   ],
    ["1.5",  {"1.4"},      "1"   ],
    ["2",    {"1"},        ""    ],
    ["2.1",  {},           "2"   ],
    ["2.2",  {"2.1"},      "2"   ],
    ["2.3",  {"2.1"},      "2"   ],
    ["2.4",  {"2.1"},      "2"   ],
    ["2.5",  {"2.2","2.3"},"2"   ],
    ["3",    {"2"},        ""    ],
    ["3.1",  {},           "3"   ],
    ["3.2",  {"3.1"},      "3"   ],
    ["3.3",  {"3.1"},      "3"   ],
    ["3.3.1",{},           "3.3" ],
    ["3.3.2",{"3.3.1"},    "3.3" ],
    ["3.3.3",{"3.3.1"},    "3.3" ],
]

max_man_days = 100       # 無限loopを避ける為

class Project():
    
    def __init__(self):
        self.id         = uuid.uuid1()
        self.calendar   = Calendar()
        self.start_date = None
        self.goal_date  = None
        self.tasks = {}

        for task_info in default_tasks:
            task = Task( task_info[0],task_info[1],task_info[2] )
            self.tasks[task.id] = task

        # 先行&後続、親&子 の関係を双方向list化
        for task_id, task in self.tasks.items():
            # 後続taskとして登録
            for pre_id in task.pre_ids:
                self.tasks[pre_id].next_ids.add( task_id )

            # root taskに親taskはありません
            if not task.parent_id:
                continue
            
            self.tasks[task.parent_id].child_ids.add( task_id )
            
        # 子taskがある場合、工数は子taskに依存
        for task_id, task in self.tasks.items():
            if len( task.child_ids ):
                task.man_days = None

    def calc_project_period(self,start_date_str):
        self.start_date = dateutil.parser.parse( start_date_str )
        root_task_ids = self.find_root_task_ids()
        start_date,goal_date = self.calc_tasks_period( root_task_ids )
        return start_date,goal_date

        
    def calc_tasks_period(self,org_task_ids):
        func_name = sys._getframe().f_code.co_name
        
        ret_start_date = None
        ret_goal_date  = None
        task_ids = self.find_first_task_ids(org_task_ids)
        
        while len(task_ids):
            next_ids = set()
            
            for task_id in task_ids:
                task = self.tasks[task_id]
                start_goal_date = self.calc_task_period(task)
                task.start_date = start_goal_date[0]
                task.goal_date  = start_goal_date[1]

                if not ret_start_date or task.start_date < ret_start_date:
                    ret_start_date = task.start_date
                if not ret_goal_date or ret_goal_date < task.goal_date:
                    ret_goal_date = task.goal_date
                    
                next_ids.update( task.next_ids )
            task_ids = self.select_next_task(next_ids)
        return ret_start_date, ret_goal_date

    def select_next_task(self,next_ids):
        task_ids = set()
        for next_id in next_ids:
            next_task = self.tasks[next_id]
            goal_dates = []
            for pre_id in next_task.pre_ids:
                pre_task = self.tasks[pre_id]
                
                if self.tasks[pre_id].goal_date:
                    goal_dates.append(pre_task.goal_date)

            if len(goal_dates) == len(next_task.pre_ids):
                task_ids.add(next_id)

            next_task.start_date = max(goal_dates) + datetime.timedelta(days=1)

        return task_ids
    
                
    def get_default_start_date(self, task):
        func_name = sys._getframe().f_code.co_name
        
        goal_dates = []
        # 先行taskがある場合
        for pre_id in task.pre_ids:
            pre_task = self.tasks[pre_id]
            
            if not pre_task.goal_date:
                return None
            goal_dates.append( pre_task.goal_date )
            
        if len( goal_dates ):
            start_date = max(goal_dates) + datetime.timedelta(days=1)
            return start_date

        # 親taskがある場合
        if task.parent_id:
            parent_task = self.tasks[task.parent_id]
            return self.get_default_start_date( parent_task )

        # root taskを参照
        return self.start_date
            
    def calc_task_period(self, task):
        func_name = sys._getframe().f_code.co_name
        
        default_start_date = self.get_default_start_date( task )

        # 子taskがある場合
        if len(task.child_ids):
            return self.calc_tasks_period(task.child_ids)
        
        start_date = self.calc_task_start_date(task, default_start_date)
        if not start_date:
            return [None,None]

        goal_date = self.calc_task_goal_date(task, start_date)
        return [start_date,goal_date]
        
    def calc_task_start_date(self, task, start_date):
        ret_date = start_date
        while (ret_date - start_date).days < max_man_days:
            if self.calendar.is_biz_day( ret_date ):
                return ret_date
            
            ret_date = ret_date + datetime.timedelta(days=1)
        return None

    def calc_task_goal_date(self, task, start_date):
        business_days = 0
        ret_date = start_date

        while (ret_date - start_date).days < max_man_days:
            if self.calendar.is_biz_day( ret_date ):
                business_days += 1

            if task.man_days <= business_days:
                return ret_date
            
            ret_date = ret_date + datetime.timedelta(days=1)

        return None
    
    def find_first_task_ids(self, task_ids):
        ret_ids = set()
        for task_id in task_ids:
            
            task = self.tasks[task_id]
            can_add = True
            
            for pre_id in task.pre_ids:
                pre_task = self.tasks[pre_id]
                
                if pre_task.parent_id == task.parent_id:
                    can_add = False
                    break
            if can_add == True:
                ret_ids.add(task_id)

        return ret_ids
                
    def find_root_task_ids(self):
        root_task_ids = []
        for id, task in self.tasks.items():
            if not task.parent_id and len(task.pre_ids) == 0:
                root_task_ids.append( id )
        return root_task_ids

lib/task.py

# -*- coding: utf-8 -*-
import random
import uuid

class Task():
    
    def __init__(self,id, pre_ids,parent_id):
        if id:
            self.id = id
        else:
            self.id = uuid.uuid1()
            
        # self.task_type         = task_type
        self.pre_ids      = pre_ids    # 前工程
        self.next_ids     = set()      # 後工程
        self.parent_id    = parent_id  # 親工程(1コ)
        self.child_ids    = set()      # 子工程
        self.start_date   = None
        self.goal_date    = None
        self.progress     = 0          # 進捗 %
        self.man_days          = random.randint(0, 10) # 標準工数 人日

lib/bizcalendar.py

# -*- coding: utf-8 -*-

import datetime
import jpholiday

class Calendar():
    
    def __init__(self):
        pass

    def is_biz_day(self,date):
        if date.weekday() >= 5 or jpholiday.is_holiday(date):
            return False
        return True