2016年4月25日 星期一

Java : SimpleDateFormat、TimeZone和Date -- 觀念釐清

在這篇裡主要紀錄了在Java中,對於SimpleDateFormat、TimeZone和Date的理解,釐清之前對它們有誤會及不太清楚的地方。

java.util.Date是一個早期Java中專門用來紀錄日期的一個類別,同時也是常用java.sql.Date的父類別,它並沒有儲存時區的資訊,當我們要對其進行不同時區的值(String或Date)轉換時,可以利用SimpleDateFormat及TimeZone來幫助我們。

TimeZone是個儲存時區資訊的類別,而SimpleDateFormat是一個用來進行日期格式轉換的類別,可以利用SimpleDateFormat.setTimeZone()來設定TimeZone,而常用的為以下兩個方法:


  1. String SimpleDateFormat.format(Date)
  2. Date SimpleDateFormat.parse(String)

SimpleDateFormat.format(Date)
可以送入一個Date物件,並返回一個表示日期的String。
如果SimpleDateFormat沒有設定TimeZone的話,
等同設定了TimeZone.getDefault()。
SimpleDateFormat.format(Date) 會將送入的Date參數轉成用設定的TimeZone來看時,正確的日期String。
例如:
如果 Date 是 2022-01-01T23:00:00+08
Timezone 設定 UTC-08
那 format() 出來的 String 就會是
2022-01-01T07:00:00

如果你的 SimpleDateFormat 建構子的字串有設定代表時區的 Z 符號的話,就可以印出時區,
例如:
new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ")
上例的結果會是 2022-01-01T07:00:00-08

SimpleDateFormat.parse(String) 
可以送入一個表示日期的String,並返回一個Date物件。
如果SimpleDateFormat沒有設定TimeZone的話,
等同設定了TimeZone.getDefault()。
SimpleDateFormat.parse(String) 會將送入的 String 轉成當用設定的 Timezone 來看時
正確的 Date 物件。
例如:
如果 String 是 2022-01-01T07:00:00
Timezone 設定 UTC+08
那 parse() 出來的 Date 就會是 
2022-01-01T07:00:00+08

========================
接著下面舉一個例子:

如果我們有一個表示Local TimeZone(假設程式放在台灣server上)日期的Date物件,例如:
System.our.println(dateObject_local);   //2016/4/25 16:47:59


我們想把它轉成在TimeZone為America/Los_Angeles時,看到這個日期應看到的Date物件,例如
System.our.println(dateObject_America);   //2016/4/25 01:47:59


應該要如何做呢?
首先宣告TimeZone並設定給SimpleDateFormat
TimeZone timeZone_America = TimeZone.getTimeZone("America/Los_Angeles");
TimeZone timeZone_default = TimeZone.getDefault();

SimpleDateFormat simpleDateFormat_America = new SimpleDateFormat();
simpleDateFormat_America.setTimeZone(timeZone_America);

SimpleDateFormat simpleDateFormat_Taiwan = new SimpleDateFormat();
simpleDateFormat_Taiwan.setTimeZone(timeZone_default);

Date dateObject_local = new Date();
System.out.println("dateObject_local:" + dateObject_local);  //dateObject_local:Mon Apr 25 18:11:27 CST 2016

接著我們用simpleDateFormat_America去format上面那個Date,
String dateString_America = simpleDateFormat_America.format(dateObject_local);
System.out.println("dateString_America:" + dateString_America); //dateString_America:2016/4/25 上午 3:11

會看到時間被減了15小時,這正是美國比台灣慢15小時的時差。
如果我們想得到輸出是時間 3:11的Date物件,下面這樣是錯的:
Date Wrong_dateObject = simpleDateFormat_America.parse(dateString_America);
System.out.println("Wrong_dateObject:" + Wrong_dateObject); //Wrong_dateObject:Mon Apr 25 18:16:00 CST 2016

原因是當用simpleDateFormat_America去parse上面那個String, dateString_America時,它會用simpleDateFormat_America設定的TimeZone去看,也就是說,它認為這是美國時間3:11,所以要parse回local time的Date時,它會將時間加回15小時,所以得到非我們預期的結果。

正確的做法是下面這樣:
Date Correct_dateObject = simpleDateFormat_Taiwan.parse(dateString_America);
System.out.println("Correct_dateObject:" + Correct_dateObject); //Correct_dateObject:Mon Apr 25 03:16:00 CST 2016

我們用simpleDateFormat_Taiwan去parse,讓它以為這是本地時間3:11,再parse成local time的Date時,就不會進行時差的動作了(因為時差為0)。

只要記得,Date本身沒有時區的資訊,基本上都用Local TimeZone去對待得到一個無時區概念的毫秒數,不管是被format還是由parse得到。

而String則會用SimpleDateFormat設定的TimeZone去對待,不管是由format得到還是被parse。

2016年4月18日 星期一

ng-repeat-start和ng-repeat-end -- AngularJS

在AngularJS中,我們常會用ng-repeat這個directive來將一串資料顯示出來,不過ng-repeat的功能是有局限性的,例如如果我們有以下的data,

datas = [{name : 1 , phone:111},{name : 2 , phone:222},{name : 3 , phone: 333}];

並想讓以下重覆顯示的話,ng-repeat就不夠用了。

<h1>Name : {{data.name}}</h1>
<p style="border-bottom:thick solid #ff0000;">Phone : {{data.phone}}</p>

這時我們可以使用ng-repeat-start及ng-repeat-end,

在ng-repeat-start標示的tag及ng-repeat-end標示的tag之間(包括被ng-repeat-start和ng-repeat-end標示的tag),能夠存取到同一個串列資料中的各元素,下面就是一個使用的簡單例子:


我們先定義了ng-app、ng-controller及其中的datas。

var myapp = angular.module('myapp',[]);
myapp.controller('Ctrl',[function(){
 var self = this;
  self.datas = [{name : 1 , phone:111},{name : 2 , phone:222},{name : 3 , phone: 333}];
}]);

接著在設計要顯示的html:

<div ng-app="myapp" ng-controller="Ctrl as ctrl">  
  <h1 ng-repeat-start = "data in ctrl.datas">Name : {{data.name}}</h1>
  <p ng-repeat-end style="border-bottom:thick solid #ff0000;">Phone : {{data.phone}}</p>  
</div>

可以看到每一次的repeat都從串列資料(datas)中取出一組資料(包括了name及phone的資訊),並顯示一組
<h1>......</h1>
<p>.......</p>

參考資料:

  1. ngRepeat

2016年4月13日 星期三

AngularJS的$compile() - 動態增加DOM

在AngularJS中,會在瀏覽器對html解析成DOM後,先對AngularJS(ng-app)的進行編譯,給定scope後才會進行數據綁定。

但是如果是用例如JQuery等方式新增了新的DOM元素後,因為沒有用AngularJS編譯的關係,所以就算裡面有AngularJS的語法也不會呈現預期的結果。

例如像以下的例子:

HTML:
<div ng-app="myApp">
  <div id='DOMWrapper' ng-controller="myCtrl">
    <button ng-click='addDOM()'>Add DOM</button>
  </div>
</div>
Javascript:
var app = angular.module('myApp', []);
app.controller('myCtrl', ['$scope', '$compile', function($scope, $compile) {
	$scope.textOfDOM = 'Text show successfully';

  $scope.addDOM = function() {
    var DOMToAdd = $('<p>{{textOfDOM}}</p>');
    
    //Wrong code -- start
    $('#DOMWrapper').append(DOMToAdd);
    //Wrong code -- end    
    
    //Right code -- start
    //var linkOfDOMToAdd = $compile(DOMToAdd);
    //var nodeOfCompiledDOM = linkOfDOMToAdd($scope);
    //$('#DOMWrapper').append(nodeOfCompiledDOM);
    //Right code -- end
  };
}]);

 從結果中可以看到當按下"Add DOM"按鈕時,出現的字是 "{{textOfDOM}}",而不是我們預期的 "Text show successfully"。

這是因為要新增的DOM沒有被AngularJS編譯的關係,以下才是正確的程式碼:
HTML:
<div ng-app="myApp">
  <div id='DOMWrapper' ng-controller="myCtrl">
    <button ng-click='addDOM()'>Add DOM</button>
  </div>
</div>
Javascript:
var app = angular.module('myApp', []);
app.controller('myCtrl', ['$scope', '$compile', function($scope, $compile) {
	$scope.textOfDOM = 'Text show successfully';

  $scope.addDOM = function() {
    var DOMToAdd = $('<p>{{textOfDOM}}</p>');
    
    //Wrong code -- start
    //$('#DOMWrapper').append(DOMToAdd);
    //Wrong code -- end
    
    //Right code -- start
    var linkOfDOMToAdd = $compile(DOMToAdd);
    var nodeOfCompiledDOM = linkOfDOMToAdd($scope);
    $('#DOMWrapper').append(nodeOfCompiledDOM);
    //Right code -- end
  };
}]);
我們可以呼叫$compile()並把要編譯的DOM送進去當參數,它會返回一個 link 函式,呼叫 link 函式並把scope當作輸入參數,就可以將scope與被編譯的DOM產生連結,所以DOM就可以得到textOfDOM的值,link 函式會返回已經跟資料連結好的新的DOM物件,接著我們再把新的DOM物件用JQuery的語法放到 #DOMWrapper裡面,就可以正確的得到我們要的 "Text show successfully"了。

參考資料:

  1. 18.5. Compile的细节
  2. $compile

2016年3月6日 星期日

[SQL] 於多筆重複資料中取得該重複群組中最新一筆資料

最近要做一件特別的SQL查詢,有個一個Table1,如下所示

Table1 :
group_iduser_id
111
1111
111111
222
2222
333
3333

我們想要選出各group_id最大的user_id,也就是說我們希望的結果要像是如下這樣:
group_iduser_id
111111
2222
3333

先來看一下ROW_NUMBER()語句的,如下所示,詳細資料可參考這裡

ROW_NUMBER ( ) OVER ( [ PARTITION BY value_expression , ... [ n ] ] order_by_clause )

它可對傳回結果集的各分割區進行編列序號,各分割區序號從 1 開始,利用這個我們就可完成我們的需求。

思路是這樣的:先把Table1以group_id進行分組(partition),各組進行以user_id的DESC排序並標上編號(例如group_id=1的三組資料編號1,2,3、group_id=2的二組資料編號1,2),接著取出各組最上面那行資料,也就是各組編號為1的那行即可。

這時我們就可以使用rownumber, over, partition語法指令來幫助我們達成目標。

我們先用以下語法對Table1以group_id進行分組,並對各組以user_id做DESC排序,並為給組的排序標上編號,Sort
SELECT *, ROW_NUMBER() OVER (PARTITION BY group_id Order By user_id DESC) As Sort 
FROM Table1

這樣就會得到以下結果:
group_iduser_idSort
1111111
11112
1113
22221
2222
33331
3331

接著在跟Table1結合並選出Sort=1但不會select Sort欄位就行了,可以使用如下語句:
SELECT group_id, user_id FROM (
         SELECT *, ROW_NUMBER() OVER (PARTITION BY group_id Order By user_id DESC) As Sort
         FROM Table1
) SortTable
WHERE SortTable.Sort = 1
這樣就大功告成了。

參考資料:
  1. [SQL] 於多筆重複資料中取得該重複群組中最新一筆資料

Parse ISO 8601 duration 的方法

ISO 8601 Duration是ISO 8601的Duration標準,是用來表示持續時間的字串,例如
"P3Y6M4DT12H30M5S" 就表示 "3年6個月4天12小時又30分5秒",
像Youtube A{I回傳的影片長度就是用這個標準的字串來表示,
在Java中,除了自己想辨法解析外,也可以使用以下的方法:
  1. 在Java 1.8之後,只要使用java.time.Duration這個Class就可以輕易地解析由ISO 8601 Duration表示的持續時間,範例程式如下:
  2. import java.time.Duration;
    import java.text.NumberFormat;
    import java.text.DecimalFormat;
    
    public class HelloWorld
    {
      public static void main(String[] args)
      {
        Duration duration = Duration.parse("PT58S");
        NumberFormat formatter = new DecimalFormat("00");
    
        String hours = formatter.format(duration.toHours());
        String minutes = formatter.format(duration.toMinutes());
        String seconds = formatter.format(duration.getSeconds());
    
        System.out.print(hours + ":" + minutes + ":" + seconds); //00:00:58
      }
    }

  3. 而在Java 1.8之前,因為沒有java.time.Duration,所以可以借助第三方的Class,在這邊介紹一個很好用的第三方JAR,joda-time,到官網下載引用後,就可以使用它的Period, PeriodFormatter, ISOPeriodFormater等Class來幫助我們解析ISO 8601 Duration,範例程式如下:
  4. import java.text.DecimalFormat;
    import java.text.NumberFormat;
    import org.joda.time.Period;
    import org.joda.time.format.ISOPeriodFormat;
    import org.joda.time.format.PeriodFormatter;
    
    public class DurationTest {
    
     public DurationTest() {
      // TODO Auto-generated constructor stub
     }
    
     public static void main(String[] args) {
      PeriodFormatter periodformatter = ISOPeriodFormat.standard();
      Period p = periodformatter.parsePeriod("PT58S");
      NumberFormat numberformatter = new DecimalFormat("00");  
    
      String hours = numberformatter.format(p.getHours());
      String minutes = numberformatter.format(p.getMinutes());
      String seconds = numberformatter.format(p.getSeconds());
    
      System.out.println(hours + ":" + minutes + ":" + seconds); //00:00:58
     }
    }
參考資料:
  1. How to convert Youtube API V3 duration in Java

2016年2月6日 星期六

Javascript的原型

Javascript與我們熟知的Java及C++不同,它沒有Java及C++那種繼承(extends)與實作介面(implement interface)的概念,而是有自己獨特的「原型鏈」特性,對沒接觸過的人可能會有點難以理解,所以在這篇文我特地為其做了筆記。

在Javascript中,幾乎任何東西(除了Boolean, Number那些)都是類別(Obejct),而在類別中有三個有趣的屬性:constructor、prototype、_proto_。

當我們在使用「new」這個關鍵字時,就會用到以上三個屬性,例如:

var car1 = new Car();

它的行為就是:

  1. 為 car1 建立一個實例(instance)。
  2. 將 car1._proto_ 指向 Car.prototype 。
  3. 將上下文設定成 car1 (即 this 代表 car1) 來執行 Car.prototype.constructor()。
為了容易理解,可以用下圖來表示:




對應的程式碼可能如下:
var Transportation = function(){......};
var Car = function(){......};
Car.prototype = new Transportation();
var car1= new Car();

當要使用的變數在bus中找不到時,它會去car1._proto_中去尋找,如果還是找不到,就會到
bus._proto_._proto_中去尋找,以此類推,直到找盡頭(XXX._proto_ = null)為止。

利用這樣的特性,就可以模擬(不完全相同)Java中的繼承關係,例如上例就有如 Car 繼承 Transportation、 bus 繼承 Car這樣的關係。

如果想要再創一個Bus去繼承Car的話,就可以像這樣:
var Bus = function(){......};
Bus.prototype = new Car();



參考資料:

  1. JAVASCRIPT原型
  2. 基於原型的JavaScript繼承
  3. javascript 原型模式的工作原理 到 對象模式的探尋(上)
  4. 實體關係的確定
  5. JavaScript's Pseudo Classical Inheritance diagram





2016年1月3日 星期日

freemarker - Java :遍歷Hashmap

template中遍歷Java傳來的HashMap參數

因為不是Freemarker自己類型的Map,所以不能直接用下列方式取值遍歷:

<list map?keys as key>
${key} : ${map[key]}
</list>


原因是Freemarker判斷map可以用點(".")的方式得到的都為其擁有的名值對,所以會把map的所有Java api method名稱都列出來,例如put, keySet, entrySet ...

以下才是正確的遍歷方式:

Java碼:
HashMap testMap = new HashMap();
testMap.put("t1", "tt1");
testMap.put("t2", "t2");
this.context.put("testMap", testMap);

Template碼:
<#list testMap.entrySet() as entry>
${entry.getKey()} = {entry.getValue()}
</#list>


<#list testMap.keySet() as key>
${key} = ${testMap.get(key)}
</#list>