diff --git a/src/constant.js b/src/constant.js
--- a/src/constant.js
+++ b/src/constant.js
@@ -27,4 +27,4 @@ export const INVALID_DATE_STRING = 'Invalid Date'
 
 // regex
 export const REGEX_PARSE = /^(\d{4})[-/]?(\d{1,2})?[-/]?(\d{0,2})[^0-9]*(\d{1,2})?:?(\d{1,2})?:?(\d{1,2})?.?(\d+)?$/
-export const REGEX_FORMAT = /\[([^\]]+)]|Y{2,4}|M{1,4}|D{1,2}|d{1,4}|H{1,2}|h{1,2}|a|A|m{1,2}|s{1,2}|Z{1,2}|SSS/g
+export const REGEX_FORMAT = /\[([^\]]+)]|Y{1,4}|M{1,4}|D{1,2}|d{1,4}|H{1,2}|h{1,2}|a|A|m{1,2}|s{1,2}|Z{1,2}|SSS/g
diff --git a/src/index.js b/src/index.js
--- a/src/index.js
+++ b/src/index.js
@@ -387,7 +387,10 @@ dayjs.prototype = proto;
 })
 
 dayjs.extend = (plugin, option) => {
-  plugin(option, Dayjs, dayjs)
+  if (!plugin.$i) { // install plugin only once
+    plugin(option, Dayjs, dayjs)
+    plugin.$i = true
+  }
   return dayjs
 }
 
diff --git a/src/plugin/customParseFormat/index.js b/src/plugin/customParseFormat/index.js
--- a/src/plugin/customParseFormat/index.js
+++ b/src/plugin/customParseFormat/index.js
@@ -8,14 +8,15 @@ const match3 = /\d{3}/ // 000 - 999
 const match4 = /\d{4}/ // 0000 - 9999
 const match1to2 = /\d\d?/ // 0 - 99
 const matchSigned = /[+-]?\d+/ // -inf - inf
-const matchOffset = /[+-]\d\d:?\d\d/ // +00:00 -00:00 +0000 or -0000
+const matchOffset = /[+-]\d\d:?(\d\d)?/ // +00:00 -00:00 +0000 or -0000 +00
 const matchWord = /\d*[^\s\d-:/()]+/ // Word
 
 let locale
 
 function offsetFromString(string) {
+  if (!string) return 0
   const parts = string.match(/([+-]|\d\d)/g)
-  const minutes = +(parts[1] * 60) + +parts[2]
+  const minutes = +(parts[1] * 60) + (+parts[2] || 0)
   return minutes === 0 ? 0 : parts[0] === '+' ? -minutes : minutes // eslint-disable-line no-nested-ternary
 }
 
diff --git a/src/plugin/duration/index.js b/src/plugin/duration/index.js
--- a/src/plugin/duration/index.js
+++ b/src/plugin/duration/index.js
@@ -1,4 +1,4 @@
-import { MILLISECONDS_A_DAY, MILLISECONDS_A_HOUR, MILLISECONDS_A_MINUTE, MILLISECONDS_A_SECOND, MILLISECONDS_A_WEEK } from '../../constant'
+import { MILLISECONDS_A_DAY, MILLISECONDS_A_HOUR, MILLISECONDS_A_MINUTE, MILLISECONDS_A_SECOND, MILLISECONDS_A_WEEK, REGEX_FORMAT } from '../../constant'
 
 const MILLISECONDS_A_YEAR = MILLISECONDS_A_DAY * 365
 const MILLISECONDS_A_MONTH = MILLISECONDS_A_DAY * 30
@@ -105,6 +105,27 @@ class Duration {
     return this.toISOString()
   }
 
+  format(formatStr) {
+    const str = formatStr || 'YYYY-MM-DDTHH:mm:ss'
+    const matches = {
+      Y: this.$d.years,
+      YY: $u.s(this.$d.years, 2, '0'),
+      YYYY: $u.s(this.$d.years, 4, '0'),
+      M: this.$d.months,
+      MM: $u.s(this.$d.months, 2, '0'),
+      D: this.$d.days,
+      DD: $u.s(this.$d.days, 2, '0'),
+      H: this.$d.hours,
+      HH: $u.s(this.$d.hours, 2, '0'),
+      m: this.$d.minutes,
+      mm: $u.s(this.$d.minutes, 2, '0'),
+      s: this.$d.seconds,
+      ss: $u.s(this.$d.seconds, 2, '0'),
+      SSS: $u.s(this.$d.milliseconds, 3, '0')
+    }
+    return str.replace(REGEX_FORMAT, (match, $1) => $1 || String(matches[match]))
+  }
+
   as(unit) {
     return this.$ms / (unitToMS[prettyUnit(unit)])
   }
diff --git a/src/plugin/relativeTime/index.js b/src/plugin/relativeTime/index.js
--- a/src/plugin/relativeTime/index.js
+++ b/src/plugin/relativeTime/index.js
@@ -60,7 +60,11 @@ export default (o, c, d) => {
       }
     }
     if (withoutSuffix) return out
-    return (isFuture ? loc.future : loc.past).replace('%s', out)
+    const pastOrFuture = isFuture ? loc.future : loc.past
+    if (typeof pastOrFuture === 'function') {
+      return pastOrFuture(out)
+    }
+    return pastOrFuture.replace('%s', out)
   }
   proto.to = function (input, withoutSuffix) {
     return fromTo(input, withoutSuffix, this, true)
diff --git a/src/plugin/timezone/index.js b/src/plugin/timezone/index.js
--- a/src/plugin/timezone/index.js
+++ b/src/plugin/timezone/index.js
@@ -114,6 +114,17 @@ export default (o, c, d) => {
     return result && result.value
   }
 
+  const oldStartOf = proto.startOf
+  proto.startOf = function (units, startOf) {
+    if (!this.$x || !this.$x.$timezone) {
+      return oldStartOf.call(this, units, startOf)
+    }
+
+    const withoutTz = d(this.format('YYYY-MM-DD HH:mm:ss:SSS'))
+    const startOfWithoutTz = oldStartOf.call(withoutTz, units, startOf)
+    return startOfWithoutTz.tz(this.$x.$timezone, true)
+  }
+
   d.tz = function (input, arg1, arg2) {
     const parseFormat = arg2 && arg1
     const timezone = arg2 || arg1 || defaultTimezone
diff --git a/src/plugin/utc/index.js b/src/plugin/utc/index.js
--- a/src/plugin/utc/index.js
+++ b/src/plugin/utc/index.js
@@ -112,7 +112,7 @@ export default (option, Dayjs, dayjs) => {
   }
   const oldDiff = proto.diff
   proto.diff = function (input, units, float) {
-    if (this.$u === input.$u) {
+    if (input && this.$u === input.$u) {
       return oldDiff.call(this, input, units, float)
     }
     const localThis = this.local()
diff --git a/types/plugin/duration.d.ts b/types/plugin/duration.d.ts
--- a/types/plugin/duration.d.ts
+++ b/types/plugin/duration.d.ts
@@ -51,6 +51,8 @@ declare namespace plugin {
 
     toISOString(): string
 
+    format(formatStr?: string): string
+
     locale(locale: string): Duration
   }
 }
diff --git a/types/plugin/timezone.d.ts b/types/plugin/timezone.d.ts
--- a/types/plugin/timezone.d.ts
+++ b/types/plugin/timezone.d.ts
@@ -6,6 +6,7 @@ export = plugin
 declare module 'dayjs' {
   interface Dayjs {
     tz(timezone?: string, keepLocalTime?: boolean): Dayjs
+    offsetName(type?: 'short' | 'long'): string | undefined
   }
 
   interface DayjsTimezone {

diff --git a/test/display.test.js b/test/display.test.js
--- a/test/display.test.js
+++ b/test/display.test.js
@@ -215,6 +215,10 @@ describe('Difference', () => {
     expect(dayjs('2018-08-08').diff(dayjs('2018-09-08'), 'month')).toEqual(-1)
     expect(dayjs('2018-01-01').diff(dayjs('2018-01-01'), 'month')).toEqual(0)
   })
+
+  it('undefined edge case', () => {
+    expect(dayjs().diff(undefined, 'seconds')).toBeDefined()
+  })
 })
 
 it('Unix Timestamp (milliseconds)', () => {
diff --git a/test/plugin/customParseFormat.test.js b/test/plugin/customParseFormat.test.js
--- a/test/plugin/customParseFormat.test.js
+++ b/test/plugin/customParseFormat.test.js
@@ -101,6 +101,22 @@ it('timezone with no hour', () => {
   expect(dayjs(input, format).valueOf()).toBe(moment(input, format).valueOf())
 })
 
+describe('Timezone Offset', () => {
+  it('timezone with 2-digit offset', () => {
+    const input = '2020-12-01T20:00:00+09'
+    const format = 'YYYY-MM-DD[T]HH:mm:ssZZ'
+    const result = dayjs(input, format)
+    expect(result.valueOf()).toBe(moment(input, format).valueOf())
+    expect(result.valueOf()).toBe(1606820400000)
+  })
+  it('no timezone format token should parse in local time', () => {
+    const input = '2020-12-01T20:00:00+01:00'
+    const format = 'YYYY-MM-DD[T]HH:mm:ss'
+    const result = dayjs(input, format)
+    expect(result.valueOf()).toBe(moment(input, format).valueOf())
+  })
+})
+
 it('parse hh:mm', () => {
   const input = '12:00'
   const format = 'hh:mm'
diff --git a/test/plugin/duration.test.js b/test/plugin/duration.test.js
--- a/test/plugin/duration.test.js
+++ b/test/plugin/duration.test.js
@@ -208,3 +208,27 @@ describe('prettyUnit', () => {
     m: 12
   }).toISOString()).toBe('P12MT12M')
 })
+
+describe('Format', () => {
+  test('no formatStr', () => {
+    const d = dayjs.duration(15, 'seconds')
+      .add(13, 'hours')
+      .add(35, 'minutes')
+      .add(16, 'days')
+      .add(10, 'months')
+      .add(22, 'years')
+    expect(d.format()).toBe('0022-10-16T13:35:15')
+  })
+
+  test('with formatStr for all tokens', () => {
+    const d = dayjs.duration(1, 'seconds')
+      .add(8, 'hours')
+      .add(5, 'minutes')
+      .add(6, 'days')
+      .add(9, 'months')
+      .add(2, 'years')
+      .add(10, 'milliseconds')
+    expect(d.format('Y/YY.YYYYTESTM:MM:D:DD:H:HH:m:mm:s:ss:SSS'))
+      .toBe('2/02.0002TEST9:09:6:06:8:08:5:05:1:01:010')
+  })
+})
diff --git a/test/plugin/relativeTime.test.js b/test/plugin/relativeTime.test.js
--- a/test/plugin/relativeTime.test.js
+++ b/test/plugin/relativeTime.test.js
@@ -3,6 +3,7 @@ import moment from 'moment'
 import dayjs from '../../src'
 import * as C from '../../src/constant'
 import relativeTime from '../../src/plugin/relativeTime'
+import updateLocale from '../../src/plugin/updateLocale'
 import utc from '../../src/plugin/utc'
 import '../../src/locale/ru'
 
@@ -119,6 +120,7 @@ it('Time from now with UTC', () => {
 
 it('Custom thresholds and rounding support', () => {
   expect(dayjs().subtract(45, 'm').fromNow()).toBe('an hour ago')
+  delete relativeTime.$i // this allow plugin to be installed again
   dayjs.extend(relativeTime, {
     rounding: Math.floor,
     thresholds: [
@@ -143,3 +145,30 @@ it('Locale without relativeTime config fallback', () => {
     name: 'test-locale'
   }).fromNow()).toEqual(expect.any(String))
 })
+
+it('Past and Future keys should support function for additional processing', () => {
+  dayjs.extend(updateLocale)
+  dayjs.updateLocale('en', {
+    relativeTime: {
+      future: input => `${input} modified`,
+      past: input => `${input} modified`,
+      s: 'just now',
+      m: ' 1 min',
+      mm: '%d min',
+      h: '1 hr',
+      hh: '%d hrs',
+      d: 'a day',
+      dd: '%d days',
+      M: 'a month',
+      MM: '%d months',
+      y: 'a year',
+      yy: '%d years'
+    }
+  })
+
+
+  const past = Date.now() - 1000
+  expect(dayjs(past).fromNow()).toEqual(' 1 min modified')
+  const future = Date.now() + 1000
+  expect(dayjs(future).fromNow()).toEqual(' 1 min modified')
+})
diff --git a/test/plugin/timezone.test.js b/test/plugin/timezone.test.js
--- a/test/plugin/timezone.test.js
+++ b/test/plugin/timezone.test.js
@@ -290,3 +290,17 @@ describe('CustomPraseFormat', () => {
     expect(dayjs.tz('10/15/2020 12:30', 'MM/DD/YYYY HH:mm', DEN).unix()).toBe(result)
   })
 })
+
+describe('startOf and endOf', () => {
+  it('corrects for timezone offset in startOf', () => {
+    const originalDay = dayjs.tz('2010-01-01 00:00:00', NY)
+    const startOfDay = originalDay.startOf('day')
+    expect(startOfDay.valueOf()).toEqual(originalDay.valueOf())
+  })
+
+  it('corrects for timezone offset in endOf', () => {
+    const originalDay = dayjs.tz('2009-12-31 23:59:59.999', NY)
+    const endOfDay = originalDay.endOf('day')
+    expect(endOfDay.valueOf()).toEqual(originalDay.valueOf())
+  })
+})
diff --git a/test/plugin/utc.test.js b/test/plugin/utc.test.js
--- a/test/plugin/utc.test.js
+++ b/test/plugin/utc.test.js
@@ -237,6 +237,9 @@ describe('Diff', () => {
       expect(_.utc(d1).diff(_.utc(d2), 'm')).toBe(1440)
     })
   })
+  it('default diff', () => {
+    expect(dayjs().diff()).toBeDefined()
+  })
   it('local.diff(utc)', () => {
     expect(dayjs(d1).diff(dayjs.utc(d2), 'days'))
       .toBe(moment(d1).diff(moment.utc(d2), 'days'))
@@ -266,3 +269,7 @@ it('utc keepLocalTime', () => {
   expect(dd).toEqual(dm)
   expect(vd).toEqual(vm)
 })
+
+it('utc diff undefined edge case', () => {
+  expect(dayjs().diff(undefined, 'seconds')).toBeDefined()
+})
diff --git a/test/timezone.test.js b/test/timezone.test.js
--- a/test/timezone.test.js
+++ b/test/timezone.test.js
@@ -1,9 +1,11 @@
 import MockDate from 'mockdate'
 import moment from 'moment'
 import dayjs from '../src'
+import timezone from '../src/plugin/timezone'
 import utc from '../src/plugin/utc'
 
 dayjs.extend(utc)
+dayjs.extend(timezone)
 
 beforeEach(() => {
   MockDate.set(new Date())

Day.js doesn't parse ISO 8601 timezone offset +NN
**Describe the bug**
PostgreSQL is returning ```time with time zone``` fields in this ISO 8601 format: ```2020-12-01T20:00:00+00```. Note that it's using a 2-digit offset which is part of the ISO specification, along with the 4-digit colon-separated format (which Day.js parses fine).

Day.js doesn't parse this 2-digit format, it ignores the timezone if parsed normally, i.e. ```dayjs('2020-12-01T20:00:00+00')```, and if given a custom format like ```dayjs('2020-12-01T20:00:00+00', 'YYYY-MM-DD[T]HH:mm:ssZZ');``` it will return an ```Invalid date```.

Sandbox w/ test and control strings: https://codesandbox.io/s/festive-glitter-mh7bx

**Expected behavior**
I expect this format to be parsed correctly like the 4-digit, colon separated ISO 8601 timezone offset format ```+NN:NN```.

**Information**
 - Day.js Version: 1.9.6
 - OS: macOS
 - Browser: Chrome, Safari, Firefox
 - Time zone: GMT+1


test/timezone.test.js(1) test/timezone.test.js(2) test/timezone.test.js(3) test/timezone.test.js(4) test/timezone.test.js(5) test/plugin/timezone.test.js test/display.test.js
test/plugin/customParseFormat.test.js test/plugin/utc.test.js test/plugin/duration.test.js test/plugin/relativeTime.test.js