顯示具有 ls 標籤的文章。 顯示所有文章
顯示具有 ls 標籤的文章。 顯示所有文章

2016-07-24

Shell&JS: 簡單的集合運算

簡單的集合運算

結論:

// A 與 B 目錄下的檔案
> ls A B
// A:
// apple  book  can  cup  chair
// 
// B:
// book  cup  dog  hat  table
//

// 交集: A&B
> (ls A;ls B) | sort | uniq -d 
// book
// cup

// 聯集: A|B
> (ls A;ls B) | sort | uniq 
// apple
// book
// can
// cup
// chair
// dog
// hat
// table

//差集: A-B , 注意 `ls B` 重複兩次, 不是筆誤.
> (ls B;ls B;ls A) | sort | uniq -u
// apple
// can
// chair

//對稱差: A^B = (A-B)|(B-A), A^B = (A|B)-(A&B)
> (ls A;ls B) | sort | uniq -u
// apple
// can
// chair
// dog
// hat
// table

廢話:

集合在數學中占有很大的分量, 但基礎是簡單的. 在程式中也經常需要用到. 使用程式語言時, 只要使用 for 跟 if, 應該就能簡單的達成交集, 聯集和差集的運算. 但有時候特地去寫程式之類, 還是算了吧.
在 shell 中利用 sort 跟 uniq 也能達成簡單的集合運算. 以下分別舉例來說明如何達成集合最基本的運算.

正文:

  • 交集: 兩個集合中相同的元素. 使用 JS 陣列時, 可以如下處理,
var A = ['apple', 'book', 'can', 'cup', 'chair'];
var B = ['book', 'cup', 'dog', 'hat', 'table'];
var S = [];

for (var i = 0; i < A.length; i++){
    var p = A[i];
    for (var j = 0; j < B.length; j++){
        var q = B[j];
        if (p == q) {           
            S.push(q);
            break;
        }
    }
}
console.log(S);
// [ 'book', 'cup' ]
同樣的假設 A 和 B 目錄下各有數個檔案. 如下, A 有 ‘apple’, ‘book’, ‘can’, ‘cup’, ‘chair’ B 有 ‘book’, ‘cup’, ‘dog’, ‘hat’, ‘table’ 使用 shell 指令來處理,
> (ls A;ls B) | sort | uniq -d
// book
// cup
使用 sort 是因為 uniq 的輸入必須是已經被排序的. 如果給 uniq 一個不是排序的資料, 將會得到錯誤的結果. 且 uniq -d 是列出重複的資料. 整個命令的意思是說: 列出A目錄跟B目錄下的檔案, 把他們放在一起, 丟給sort排序, 然後再列出重複的資料.
利用同樣的思路, 改用 JS 來處理, 如下
var A = ['apple', 'book', 'can', 'cup', 'chair'];
var B = ['book', 'cup', 'dog', 'hat', 'table'];
var X = A.concat(B);
var S = {};

for (var i=0; i< X.length; i++){
    S[X[i]] = (S[X[i]] || 0) + 1;    
}
// 上面的 JS 以計算重複次數代替判斷重複, 需要遍歷整個陣列 X,
// 所以有無排序不影響結果.

for (obj in S){
    if (S[obj] > 1) {
        console.log(obj);
    }
}
// book
// cup
  • 聯集: 有出現在兩個集合中元素.
var X = A.concat(B);
var S = {};

X.forEach(function(obj){
    S[obj] = (S[obj] || 0) + 1;
});
// 改用 Array 的 `forEach` 方法來計算重複次數

for (obj in S){
    console.log(obj);    
}
// 因在計算重複次數時, 已將相同的元素合併計算次數, 
// 所以只要將 S 的所有元素印出, 即可.
// apple
// book
// can
// cup
// chair
// dog
// hat
// table
而使用 shell 指令來處理, 就更簡單, 只要下 (ls A;ls B) | sort | uniq 就行了.
  • 差集: 有出現在其中一個集合並不出現在另一個集合的元素. 下面是可用的 JS 程式碼,
var S = [];

for (var i = 0; i < A.length; i++){
    var p = A[i];
    S.push(p);
    for (var j = 0; j < B.length; j++){
        var q = B[j];
        if (p == q) {                       
            S.pop();
            break;
        }
    }
}
console.log(S);
// [ 'apple', 'can', 'chair' ]
實作上程式的邏輯有許多方式, 這裡採用先將 A 陣列的元素放入一個容器後, 再用迴圈判斷是否存在 B 陣列中, 如果存在就再將他移出的. 好處是可以省去使用判斷是否存在 B 陣列旗標.
在 Shell 中, 要達成差集必須要有一點巧思. 我們知道差集是 A 集合減去 B 集合剩下來的元素, 所以該差集 不會 包含 B 集合的元素. 而利用uniq -u 可以列出不重複的資料, 故 (ls B;ls B) | sort | uniq -u 應該不會傳回任何資料, 因為 B 目錄下的檔案全部被重複了兩遍. 同理, 當使用(ls B;ls B;ls A) | sort | uniq -u 時, 傳回的就會是 A 目錄中不與 B 重複的檔案. 因為 A 跟 B 重複的元素, 必定也是 B 的元素, 而已經重複兩次 B 元素再加 A 重複 B 的元素還是重複, 都會被 uniq -u 消除掉. 所以在這裡 ls B 要重複兩次. 如果只是單純的(ls B;ls A) | sort | uniq -u 則會列出 A 和 B 目錄下的不重複的檔案. 也就是 A 目錄下的檔案加上 B 目錄的下的檔案再減去 A 和 B 目錄下都有的檔案, 也就是 A 跟 B 的對稱差.
根據這個思路, 改寫 JS 如下,
var X = A.concat(B).concat(B);
var S = {};

X.forEach(function(obj){
    S[obj] = (S[obj] || 0) + 1;
});

for (obj in S){
    if (S[obj]==1) {
        console.log(obj);    
    }
}
// 印出只出現一次的元素
// apple
// can
// chair
其實, 集合的運算是可以類比如位元的運算. 重新整理上面的案例, 將資料整理, 賦予每一個元素一個數值, 如下,
// apple=1, book=2, can=4, cup=8, chair=16, dog=32, hat=64, table=128;
// A = ['apple', 'book', 'can', 'cup', 'chair'] => [1, 2, 4, 8, 16]
// B = ['book', 'cup', 'dog', 'hat', 'table'] => [2, 8, 32, 64, 128]

//將 A, B 改用數值表示
  var A = 1 + 2 + 4 + 8 + 16;
// A, 十進位:  31,  二進位: 1 1111

  var B = 2 + 8 + 32+ 64+ 128;
// B, 十進位: 234,  二進位: 1110 1010

// 交集: A&B
  var x = A & B;
// 10  
// 十進位:  10,  二進位: 1010
// 10 => 2 + 8 => book, cup

// 聯集: A|B
  var x = A | B;
// 255
// 十進位:  255,  二進位: 1111 1111
// 255 => 1 + 2 + 4 + 8 + 16 + 32 + 64 + 128 
// => apple, book, can, cup, chair, dog, hat, table

// 差集: A-B?
  var x = (A|B) - B;  
// 21
// 十進位:  21,  二進位: 0001 0101
// 21 => 1 + 4 + 16 => apple, can, chair
// A集合去掉B集合的元素可表示為 A 與 B 的聯集(A|B)再去掉 B 集合, 
// 這樣就只會剩下沒有 B 集合元素的 A 集合元素.
// 這裡沒辦法直接 `A-B` 是因為若有一 B 集合元素不在 A 集合中, 
// 則會變成空集合減扣掉某一元素, 在這邊就沒辦法正確表示.

// 對稱差: A ^ B
  var x = A ^ B;
// 245 
// 十進位:  245,  二進位: 1111 0101 
// 245 = 1 + 4 + 16 + 32 + 64 + 128 =>apple, can, chair, dog, hat, table
不過 JS 的位元運算並不像 C 語言一樣有較高的效率. 雖說在運算上簡單不需要迴圈, 但要將結果轉回對映的元素, 仍要進行一串二進位運算. 總的來說, 程式碼也不算特別簡潔. 不過, 在一些特殊的運用場合仍有其用途.
所以集合運算可對應位元運算如下,
  • 交集: A&B, and 運算.
  • 聯集: A|B, or 運算.
  • 差集: (A|B)-B, 特殊條件下的減法運算.
  • 對稱差: A^B, xor 運算.

2016-06-22

Linux: 如何在上使用 ISO 時間格式表示 UTC 時間 ?

如何在 Linux 上使用 ISO 時間格式表示 UTC 時間 ?

這是一個關於時間, 時區的問題. 並作為第一篇網誌向其網誌名稱出處致敬.   (當然, 我不會告訴你 :P)
而如同網誌子標題所暗示的, 這是一系列記錄程式設計上的問題解答.
  • 首先, 為了節省大家的時間, “結論” 會放在前面.
  • 再者, 為了浪費大家的時間, 中間會加入 “廢話”.
  • 然後, 為了避內容農場嫌疑, 附上冗長的 “正文”.

結論:

使用 ls 查詢檔案時,
> TZ=utc ls -l --time-style=+%Y-%m-%dT%H:%M:%S%z
使用 date 查詢時間時,
> date -uIsec

廢話:

好, 正文之前先說明一下兩個詞彙.
  • UTC
  • ISO 8601
當然你一定知道這是甚麼, 不然你也不太可能看到這篇文章. 但請容許我為不小心誤入的讀者說明.
UTC 就是所謂的"世界協調時間", 又稱世界標準時間.  (只看中文還以為這世界一直在吵架. 不過, 也不算錯, 因為這名稱就是吵完架的結果 :P) 使用格里曆也就是西元紀年, 以英國格林尼治皇家天文台的當地 平太陽時 為準, 午夜零時為一天的起點. 以當地為本初子午線(0度經線), 往東為東經, 往西為西經. 東經 7.5 度到西經 7.5 度為 零時區 , 由零時區開始, 每隔 15 度劃為同一個時區(原則上). 向東每跨 1 個時區加 1 小時, 反之向西每跨 1 時區減 1 小時. 全球共分為 24個時區. 例如台灣就是 UTC+8 . 而鄰近東經或西經 180 度處畫有一條 國際換日線 . 由東經跨越國際換日線到西經, 必須將日期減掉 1 天; 反之, 由西經跨到東經去, 必須在時間上加 24 個小時.
ISO 就是"國際標準化組織", 他們訂的一連串標準, 就是所謂的 ISO 標準. 而 ISO 8601 就是"日期和時間的表示方法"的標準. 日期以 4 位的西元年, 2位月跟日, 中間以-連接, 時間以 2 位數的時分秒表示, 其中時須以 24 小時制表示, 中間以:連接. 日期與時間以T連接. 如果是 UTC 時間可以大寫字母 Z 結尾, 或+0表示. 其他時區則以當地時間加時差表示. 例: 2016-06-21T16:42:51+0800.
ISO 在計算周數上有特別的規定. 以大寫字母W後加上 2 位數表示一年內的第幾周. 以 1 位數表示一周內的第幾天. 第一天為星期一, 星期日為最後一天. 中間以-連接. 而一年的第一周指的是一年中第一個 星期四 所在的那一周, 表示為 W01. 或說, 若一年的最後一天若不是在一周的前4天中, 下一年的前幾天要計入該年的最後一周. 而下一年的第一周由當年 1 月 4 日所在的那一周開始起算. 也就是說, 若一周中有不同年份, 該周則歸屬於在一周中佔的天數多的那一年.

正文:

在 linux 文字模式中工作時, 每當想知道時間就偶而會令人煩躁.
但如果你總是在視窗環境下作業, 對這個說法應會感到困惑. 畢竟, 滑鼠一點, 就有一堆精美的日曆或時鐘.
而在 linux 中要知道時間, 需要鍵入date. 例如,
> date
//但這時候, 他會回應出甚麼卻要看你的環境設定了. 他也可能是
//Tue Jun 21 16:42:51 CST 2016
// 或
//Tue Jun 21 08:42:51 UTC 2016
// 或
//二  6月 21 16:42:51 CST 2016
也就是在不同的主機上, 同樣鍵入date, 很有可能會得到完全不同日期時間格式的字串.
這在要比較時間時, 你必須腦補成一樣的格式. 這或許不是難事, 但很煩. 尤其在寫程式判斷時.
然而, 託 linux 的設計理念是隨心所欲的福. 幸好, 不管 date 或 ls 都允許定義輸出的日期時間格式. 所以, 我們只要這樣做就好了. 但另一個問題是, 要輸出甚麼樣子的格式? 既然要統一格式, 當然要選一個全世界所有人都不會誤解的. 這也不用我們煩惱, ISO 已經幫我們做完這件事了, 就是 ISO 8601.
首先是 date, 根據說明,
date [OPTION]... [+FORMAT]
只要在其後加上+號, 再給出格式, 即可. 但
  %%   文字的 %
  %a   本地化簡短的周名稱 (e.g., Sun)
  %A   本地化完整的周名稱 (e.g., Sunday)
  %b   本地化簡短的月份名稱 (e.g., Jan)
  %B   本地化完整的月份名稱 (e.g., January)
  %c   本地化日期和時間 (e.g., Thu Mar  3 23:05:25 2005)
  %C   世紀; 像 %Y, 省略最後兩位數 (e.g., 20)
  %d   月份中的日 (e.g., 01)
  %D   日期; 等效於 %m/%d/%y
  %e   月份中的日, 填充空白; 等效於 %_d
  %F   完整的日期; 等效於 %Y-%m-%d
  %g   以 ISO 周數為準的年份的最後兩位數,  (見 %G)
  %G   以 ISO 周數為準年份, (見 %V); 通常跟著 %V 使用
  %h   等效於 %b
  %H   時 (00..23)
  %I   時 (01..12)
  %j   day of year (001..366)
  %k   時, 填充空白 ( 0..23); 等效於 %_H
  %l   時, 填充空白 ( 1..12); 等效於 %_I
  %m   月份 (01..12)
  %M   分 (00..59)
  %n   換行
  %N   奈秒 (000000000..999999999)
  %p   等效的本地化 AM 或 PM; 未知則空白
  %P   像 %p, 但小寫
  %r   本地化 12-小時制 時鐘的時間 (e.g., 11:11:04 PM)
  %R   24-小時制的 時 和 分; 等效於 %H:%M
  %s   從 1970-01-01 00:00:00 UTC 的秒數
  %S   秒 (00..60)
  %t   跳格
  %T   時間; 等效於 %H:%M:%S
  %u   一周中的日數 (1..7); 1 是星期一
  %U   一年中中的周數, 以星期天為一周的第一天 (00..53)
  %V   ISO 的周數, 以星期一為一周的第一天 (01..53)
  %w   一周中的日數 (0..6); 0 是星期天
  %W   一年中中的周數, 星期一是一周的第一天 (00..53)
  %x   本地化的日期表示法 (e.g., 12/31/99)
  %X   本地化的時間表示法 (e.g., 23:13:48)
  %y   年份的最後兩位數 (00..99)
  %Y   年
  %z   以 +hhmm 數字化的時區 (e.g., -0400)
  %:z  以 +hh:mm 數字化的時區 (e.g., -04:00)
  %::z  以 +hh:mm:ss 數字化的時區 (e.g., -04:00:00)
  %:::z  以必要的精準度表示的數字化時區 (e.g., -04, +05:30)
  %Z   縮寫的文字化時區 (e.g., EDT)
可以使用的格式太多, 簡言之
> date +%Y-%m-%dT%H:%M:%S%z
// 或, 可用
> date -uIsec
-u 是使用 UTC 時間
-I 是使用 ISO 8601 格式, sec 是修飾 -I 指示時間要精準到秒
再來是常用的ls, 使用 --time-style 修飾 -l, 而格式可參考 date.
> ls -l --time-style=+%Y-%m-%dT%H:%M:%S%z
但這樣出現的是本地時間. 為了要讓位在世界各地的伺服器表示的時間基準都一樣. 而不用自己腦補時區的加減, 這必須將環境變數 TZ, 也就是 TIMEZONE 設成 utc. 當然, 每次下 ls 就要去改一次環境變數也太麻煩了. 合併指令如下,
> TZ=utc ls -l --time-style=+%Y-%m-%dT%H:%M:%S%z
// 2016-06-21T16:42:51+8000
// 如果, 不堅持要完全符合 ISO 8601, 則可簡化用
> ls --full-time
// 或, 可用
> ls -l --time-style=full-iso
// 2016-06-21 16:42:51.955353135 +8000
而每次要下 ls 就要打這麼多字, 也是很累. 將他設成指令別名吧.
> vi ~/.profile
// 修改 ~/.profile , 在最下面增加如下一行
alias ll='TZ=utc ls -l --time-style=+%Y-%m-%dT%H:%M:%S%z'
之後, 只要打 ll 即可.
對我而言, 在下 ls 時用標準的 ISO 格式, 會因為日期跟時間連在一起, 反而, 不容易看. 改用
alias ll='TZ=utc ls -l --time-style="+%Y-%m-%d %H:%M:%S %:::z"'
// 2016-06-21 08:42:51 +00
// 因為格式中有空白, 所以 --time-style 的參數值要用 `"` 括起來