Java 量度單位 (JSR 363 Units of Measurement API)

在日常生活中,我們都會用到不同的量度單位。例如重量有時會用公斤 (kg),有時會用磅 (lb),有時又會用斤之類。如果在 Java 上表示這些數值,用 intfloatdouble 的話有時會令人理解錯誤,就好像 UNIX timestamp 般一時會用秒一時會用毫秒。為防止人們誤解這個 variable 或 method 的時間單位,通常都要在名稱加上 millis 之類的後綴。知名例子有 System.currentTimeMillis。如果處理時間的話,現成的有 TimeUnit。不過如果去到之前的重量單位的話,JDK 就沒有現成的 class。

如果是 Android 的話可以用 android.icu.util.Measure,但要 API level 24 (Android 7.0) 或以上才可使用,而且不包括單位轉換功能。

如果想在 Android API level 24 以前的裝置用到的話,可以考慮用 JSR 363 (Units of Measurement API)。它提供了一套 API 來表示量度單位和數值,而且包含單位轉換和格式化。

如果要用這個 library 的話,是需要這兩個 artifact:

1
2
implementation "tech.units:indriya:2.0.2"
implementation "systems.uom:systems-common:2.0.2"

tech.units:indriya 是 JSR 363 的 reference implementation。它其實是用了 javax.measure:unit-api 所定義的 interface 的一個實作。而 systems.uom:systems-common 就提供了一些主流的單位的定義,例如 SI(國際單位制)、Imperial(英制)和 USCustomary(美制)。

下面是一個由公里轉成哩的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import org.junit.Assert.assertEquals
import org.junit.Test
import si.uom.SI
import systems.uom.common.USCustomary
import tech.units.indriya.quantity.Quantities
import javax.measure.MetricPrefix
import javax.measure.Quantity
import javax.measure.quantity.Length

class MeasureTest {
@Test
fun kilometerToMile() {
val distanceKm: Quantity<Length> = Quantities.getQuantity(12, MetricPrefix.KILO(SI.METRE))
val distanceMile: Quantity<Length> = distanceKm.to(USCustomary.MILE)
assertEquals(7.45645431, distanceMile.value.toDouble(), 0.00000001)
}
}

用了這個 library 就是要把數值包裝成 Quantity<Length>,然後就可以押後處理數值的實際單位,只需知道它是長度單位就可以了,直到需要輸出時才考慮單位的問題。如果需要為單位加上詞頭 (prefix) 的話,可以套上 MetricPrefixKILOCENTINANO 之類的 decorator

如果需要輸出顯示的話,可以用它提供的 NumberDelimiterQuantityFormat,不過我覺得不太好用,在 Android 的話可以用 string resource 處理。

談到文章開首提及過的斤,systems.uom:systems-common 當然是沒有提供,但可以自己定義新的單位。根據香港法例第 68 章《度量衡條例》的定義,1 斤 = 0.60478982 公斤。我們可以用 si.uom.SI.KILOGRAM 做參照來衍生出斤這個單位:

1
2
3
4
5
6
7
8
fun cattyToKilogram() {
val catty = SI.KILOGRAM.multiply(60478982).divide(100000000)
SimpleUnitFormat.getInstance().label(catty, "斤")

val twoCatty = Quantities.getQuantity(2.0, catty)
val kg = twoCatty.to(SI.KILOGRAM)
assertEquals(1.209580, kg.value.toDouble(), 0.000001)
}

值得一提,斤在不同地方有不同定義。中國的一斤是等於 0.5 公斤;台灣的一斤是等於 0.6 公斤。

參考

Kotlin Annotation Processor

如果有做過 Android 開發的話應該都有用過 annotation processor(又稱 codegen),即是在 build.gradle 入面要用 annotationProcessor 或者 kapt 的那些 dependency。用法大概是在 code 上加上一些 @ 開頭的 annotation,然後 build 出來就會自動幫你生成相關的 class。簡單來說 annotation processor 就是用 code 來讓 Java compiler 生成 code。通常都是用來生成一些內容重覆的 code 來代替自己人手寫。

自己做一個功能不多的 annotation processor 其實都不太難。難的地方是 debug 時不能像平常般加 breakpoint,加上我們要生成 Kotlin class 所以跟平常找到的教學會有輕微出入(因為大部分教學都是討論生成 Java class)。

Read More

Firebase Crashlytics 的 CrashlyticsOrgIdException 解決方法

最近為自己的 app 加入 Firebase Crashlytics SDK beta(即是使用 Google 的 Firebase Crashlytics Gradle plugin 而不是用 Fabric 那個),但在 build release APK 時出現下面的錯誤:

1
2
3
4
java.io.IOException: com.google.firebase.crashlytics.buildtools.exception.CrashlyticsOrgIdException: Could not fetch Crashlytics Org Id
> com.google.firebase.crashlytics.buildtools.exception.CrashlyticsOrgIdException: Could not fetch Crashlytics Org Id
> Could not fetch Crashlytics Org Id
> Unable to fetch Crashlytics Org Id using app id 1:731121766578:android:4e799392a2e62811

我的 app 是在 Firebase 開了兩個 project,一個是開發時用的,另一個是 production 用的。但只有 production 才有這個錯誤。如果把 release build 設為不上載 ProGuard/R8 mapping file 的話就不會有這個錯誤。

1
2
3
firebaseCrashlytics {
mappingFileUploadEnabled false
}

起初以為是那個 google-services.json 放錯位置,但我檢查過位置是正確。之後我找了 Firebase 支援,他們最後回覆說原因是我沒有在 production 的 Firebase project 交過一次 crash report,所以 production Firebase project 的 Crashlytics Org Id 就沒有產生出來,導致交不了 ProGuard/R8 mapping file。只不過他們的文檔沒有寫明一定要 crash 一次才算正式完成安裝 Crashlytics。

Firebase Cloud Messaging

最近工作需要做 Firebase Cloud Messaging (FCM) 整合,發現了向 Firebase API 直接送出 push 的 HTTP request 都可以生成不同種類的 message。

如果要整合到 Android 的話,需要建立一個新的 Service class 並繼承自 FirebaseMessagingService。這個 Service 有一個叫 onMessageReceived 的 callback method 來接收來自 FCM 的 push 和它的 payload。但原來不是所有的 push 都能被那個 callback 接到,要視乎 push 的種類和你的 app 當時在甚麼情況而定。

Read More

Android App Icon 規格

一個 app 的第一印象應該是它的 app icon (launcher icon)。在 Android,在不同的時期前後出了好幾個 app icon 規格。但是 Android 介紹不同種類的 app icon 的文件放得非常分散,如果平時沒有一直留意的話都幾乎肯定會做錯或者做漏。以下是全部 Android app 都會用到 app icon:

  • Launcher icon
  • Launcher icon(圓形)
  • Adaptive launcher icon
  • Google Play icon

如果想簡單地做出全部 icon 的話,可以用 Android Studio 內置的 Image Asset Studio。但如果想追求完美的話,還是自行準備圖片比較好。這篇文章整理了不同 icon 的基本規格,寫的時候盡量考慮到設計師。如果有需要的話可以分享給設計師同事參考。

Read More

OpenRefine GREL 筆記

OpenRefine 是一個開源的工具,用作檢視資料、加工處理後作其他用途。簡單的例子有一堆街名,部分街名用了全寫、部分用了縮寫,想將它們全部統一用全寫。它的定位是介乎 Excel 和自己寫程式之間。有時資料用 Excel 不太方便處理,但如果自己寫程式處理又因為程式只會用一次,感覺太麻煩。OpenRefine 相信可以解決到你的需要。

OpenRefine 內置了一種程式語言,名為 General Refine Expression Language (GREL),和 Excel 可以用公式差不多。我們用 OpenRefine 就是透過這種語言來把資料批量轉換成自己想要的東西。

值得一提的是 OpenRefine 以前由 Google 負責維護,所以介面會有以前 Google 產品的影子。

Read More

用 Google Apps Script 建立 Google Calendar event

Google Apps Script 是一套以 JavaScript 造的 API,可以讓你寫程式控制 Google Apps 內 Docs、Spreadsheet、Gmail、Drive 等等的功能。這次介紹如何用 Google Apps Script 建立 Google Calendar 的 event。我們會把部分建立 event 時所需要的資料放入 spreadsheet 內(會以 2019 年香港公眾假期作例子),然後用 Google Apps Script 讀取 spreadsheet 的內容再建立 event,效果就像 mail merge 般。

Read More

八達通新方向

最近,八達通終於做應該做的事了:商戶可以用八達通提供的商用版 app 經 NFC 向實體卡扣錢,小商戶就毋須租拍卡機就能接受八達通付款,亦都毋須使用 O! ePay 的 QR code 功能。

八達通當初就是一張單純的儲值卡。八達通開拓流動支付應該要數到在 Android 2.3 (Gingerbread) 支援 NFC 的時候見到有其他 Android 開發者推出查閱餘額 app 就學人推出一個查閱交易記錄 app(因為卡內的交易紀錄被加密,所以其他 app 只可以查閱餘額)。其後亦推出了八達通 SIM 卡,但現在應該終止推廣了。

Read More