fix: sub-second precisions need to be rounded at the seconds field
Fixes #2376

Rounds the seconds field after adding milliseconds which covers the floating point rounding issues.
Problem: originally submitted in #2366 - I didn't completely fix this apparently. I mistakenly assumed I could round the milliseconds value alone, and then add it to the integer of seconds. However Javascript will encounter floating-point rounding errors occasionally and the same precision issues are seen.

Expected behavior

Example:

Input: duration(2812)
Expected: PT2.812S
Actual: PT2.8120000000000003S
Information

Day.js Version [e.g. dev]
OS: All
Browser All
Time zone: N/A
`Duration.toISOString()` fractional seconds still have precision issues with certain values
diff --git a/src/plugin/duration/index.js b/src/plugin/duration/index.js
index 000047065..0917719bc 100644
--- a/src/plugin/duration/index.js
+++ b/src/plugin/duration/index.js
@@ -139,7 +139,8 @@ class Duration {
 
     let seconds = this.$d.seconds || 0
     if (this.$d.milliseconds) {
-      seconds += Math.round(this.$d.milliseconds) / 1000
+      seconds += this.$d.milliseconds / 1000
+      seconds = Math.round(seconds * 1000) / 1000
     }
 
     const S = getNumberUnitFormat(seconds, 'S')

diff --git a/test/plugin/duration.test.js b/test/plugin/duration.test.js
index 7c6c6998a..7f06739e6 100644
--- a/test/plugin/duration.test.js
+++ b/test/plugin/duration.test.js
@@ -77,6 +77,15 @@ describe('Creating', () => {
     expect(dayjs.duration(1000.5).toISOString()).toBe('PT1.001S')
     expect(dayjs.duration(-1000.5).toISOString()).toBe('-PT1S')
   })
+  it('should handle floating point rounding errors', () => {
+    // An example of this is when adding 2 to 0.812 seconds, which is how
+    // the seconds component is calculated in .toISOString().
+    // > 2 + 0.812
+    // 2.8120000000000003
+    expect(dayjs.duration(-2812).toISOString()).toBe('-PT2.812S') // was -PT2.8120000000000003S
+    expect(dayjs.duration(3121632.27382247).toISOString()).toBe('PT52M1.632S') // was PT52M1.6320000000000001S
+    expect(dayjs.duration(7647826.525774224).toISOString()).toBe('PT2H7M27.827S') // was PT2H7M27.826999999999998S
+  })
 })
 
 describe('Parse ISO string', () => {

{"test/plugin/duration.test.js": {"run": "PASS", "test": "FAIL", "fix": "PASS"}}
{"test/timezone.test.js:Add Time days (DST)": {"run": "PASS", "test": "PASS", "fix": "PASS"}, "test/plugin.test.js": {"run": "PASS", "test": "PASS", "fix": "PASS"}, "test/locale/de.test.js": {"run": "PASS", "test": "PASS", "fix": "PASS"}, "test/locale/is.test.js": {"run": "PASS", "test": "PASS", "fix": "PASS"}, "test/locale/sk.test.js": {"run": "PASS", "test": "PASS", "fix": "PASS"}, "test/plugin/advancedFormat.test.js": {"run": "PASS", "test": "PASS", "fix": "PASS"}, "test/plugin/weekYear.test.js": {"run": "PASS", "test": "PASS", "fix": "PASS"}, "test/timezone.test.js": {"run": "PASS", "test": "PASS", "fix": "PASS"}, "test/plugin/isSameOrBefore.test.js": {"run": "PASS", "test": "PASS", "fix": "PASS"}, "test/plugin/pluralGetSet.test.js": {"run": "PASS", "test": "PASS", "fix": "PASS"}, "test/timezone.test.js:Diff (DST)": {"run": "PASS", "test": "PASS", "fix": "PASS"}, "test/plugin/bigIntSupport.test.js": {"run": "PASS", "test": "PASS", "fix": "PASS"}, "test/locale/keys.test.js": {"run": "PASS", "test": "PASS", "fix": "PASS"}, "test/locale/zh-tw.test.js": {"run": "PASS", "test": "PASS", "fix": "PASS"}, "test/utils.test.js": {"run": "PASS", "test": "PASS", "fix": "PASS"}, "test/plugin/relativeTime.test.js": {"run": "PASS", "test": "PASS", "fix": "PASS"}, "test/parse.test.js": {"run": "PASS", "test": "PASS", "fix": "PASS"}, "test/plugin/localizedFormat.test.js": {"run": "PASS", "test": "PASS", "fix": "PASS"}, "test/plugin/updateLocale.test.js": {"run": "PASS", "test": "PASS", "fix": "PASS"}, "test/constructor.test.js": {"run": "PASS", "test": "PASS", "fix": "PASS"}, "test/plugin/weekday.test.js": {"run": "PASS", "test": "PASS", "fix": "PASS"}, "test/locale/zh-hk.test.js": {"run": "PASS", "test": "PASS", "fix": "PASS"}, "test/plugin/isLeapYear.test.js": {"run": "PASS", "test": "PASS", "fix": "PASS"}, "test/query.test.js": {"run": "PASS", "test": "PASS", "fix": "PASS"}, "test/plugin/localeData.test.js": {"run": "PASS", "test": "PASS", "fix": "PASS"}, "test/timezone.test.js:UTC diff in DST": {"run": "PASS", "test": "PASS", "fix": "PASS"}, "test/locale/zh.test.js": {"run": "PASS", "test": "PASS", "fix": "PASS"}, "test/locale/ja.test.js": {"run": "PASS", "test": "PASS", "fix": "PASS"}, "test/plugin/declarations.test.js": {"run": "PASS", "test": "PASS", "fix": "PASS"}, "test/locale/ar-kw.test.js": {"run": "PASS", "test": "PASS", "fix": "PASS"}, "test/locale/ar-dz.test.js": {"run": "PASS", "test": "PASS", "fix": "PASS"}, "test/plugin/timezone.test.js": {"run": "PASS", "test": "PASS", "fix": "PASS"}, "test/locale/ru.test.js": {"run": "PASS", "test": "PASS", "fix": "PASS"}, "test/locale/bn.test.js": {"run": "PASS", "test": "PASS", "fix": "PASS"}, "test/plugin/dayOfYear.test.js": {"run": "PASS", "test": "PASS", "fix": "PASS"}, "test/timezone.test.js:UTC add day in DST": {"run": "PASS", "test": "PASS", "fix": "PASS"}, "test/manipulate.test.js": {"run": "PASS", "test": "PASS", "fix": "PASS"}, "test/issues/issue2027.swapped-order.test.js": {"run": "PASS", "test": "PASS", "fix": "PASS"}, "test/locale/cs.test.js": {"run": "PASS", "test": "PASS", "fix": "PASS"}, "test/locale/en.test.js": {"run": "PASS", "test": "PASS", "fix": "PASS"}, "test/get-set.test.js": {"run": "PASS", "test": "PASS", "fix": "PASS"}, "test/locale/he.test.js": {"run": "PASS", "test": "PASS", "fix": "PASS"}, "test/plugin/utc.test.js": {"run": "PASS", "test": "PASS", "fix": "PASS"}, "test/locale/bn-bd.test.js": {"run": "PASS", "test": "PASS", "fix": "PASS"}, "test/plugin/quarterOfYear.test.js": {"run": "PASS", "test": "PASS", "fix": "PASS"}, "test/plugin/objectSupport.test.js": {"run": "PASS", "test": "PASS", "fix": "PASS"}, "test/locale/fi.test.js": {"run": "PASS", "test": "PASS", "fix": "PASS"}, "test/locale/ar-tn.test.js": {"run": "PASS", "test": "PASS", "fix": "PASS"}, "test/locale/ar-ma.test.js": {"run": "PASS", "test": "PASS", "fix": "PASS"}, "test/locale/sv.test.js": {"run": "PASS", "test": "PASS", "fix": "PASS"}, "test/plugin/toArray.test.js": {"run": "PASS", "test": "PASS", "fix": "PASS"}, "test/timezone.test.js:Utc Offset": {"run": "PASS", "test": "PASS", "fix": "PASS"}, "test/locale/ar-sa.test.js": {"run": "PASS", "test": "PASS", "fix": "PASS"}, "test/locale/bg.test.js": {"run": "PASS", "test": "PASS", "fix": "PASS"}, "test/issues/issue2027.correct-order.test.js": {"run": "PASS", "test": "PASS", "fix": "PASS"}, "test/plugin/utc-utcOffset.test.js": {"run": "PASS", "test": "PASS", "fix": "PASS"}, "test/locale/sr-cyrl.test.js": {"run": "PASS", "test": "PASS", "fix": "PASS"}, "test/locale/br.test.js": {"run": "PASS", "test": "PASS", "fix": "PASS"}, "test/plugin/isMoment.test.js": {"run": "PASS", "test": "PASS", "fix": "PASS"}, "test/plugin/isYesterday.test.js": {"run": "PASS", "test": "PASS", "fix": "PASS"}, "test/locale/pl.test.js": {"run": "PASS", "test": "PASS", "fix": "PASS"}, "test/plugin/toObject.test.js": {"run": "PASS", "test": "PASS", "fix": "PASS"}, "test/locale/sr.test.js": {"run": "PASS", "test": "PASS", "fix": "PASS"}, "test/locale/it-ch.test.js": {"run": "PASS", "test": "PASS", "fix": "PASS"}, "test/plugin/preParsePostFormat.test.js": {"run": "PASS", "test": "PASS", "fix": "PASS"}, "test/plugin/isSameOrAfter.test.js": {"run": "PASS", "test": "PASS", "fix": "PASS"}, "test/plugin/weekOfYear.test.js": {"run": "PASS", "test": "PASS", "fix": "PASS"}, "test/comparison.test.js": {"run": "PASS", "test": "PASS", "fix": "PASS"}, "test/plugin/calendar.test.js": {"run": "PASS", "test": "PASS", "fix": "PASS"}, "test/plugin/isTomorrow.test.js": {"run": "PASS", "test": "PASS", "fix": "PASS"}, "test/locale/hu.test.js": {"run": "PASS", "test": "PASS", "fix": "PASS"}, "test/locale/et.test.js": {"run": "PASS", "test": "PASS", "fix": "PASS"}, "test/timezone.test.js:UTC and utcOffset": {"run": "PASS", "test": "PASS", "fix": "PASS"}, "test/plugin/arraySupport.test.js": {"run": "PASS", "test": "PASS", "fix": "PASS"}, "test/plugin/isToday.test.js": {"run": "PASS", "test": "PASS", "fix": "PASS"}, "test/locale/hr.test.js": {"run": "PASS", "test": "PASS", "fix": "PASS"}, "test/locale/ar-ly.test.js": {"run": "PASS", "test": "PASS", "fix": "PASS"}, "test/locale/ku.test.js": {"run": "PASS", "test": "PASS", "fix": "PASS"}, "test/locale.test.js": {"run": "PASS", "test": "PASS", "fix": "PASS"}, "test/plugin/badMutable.test.js": {"run": "PASS", "test": "PASS", "fix": "PASS"}, "test/locale/sv-fi.test.js": {"run": "PASS", "test": "PASS", "fix": "PASS"}, "test/plugin/customParseFormat.test.js": {"run": "PASS", "test": "PASS", "fix": "PASS"}, "test/plugin/isoWeek.test.js": {"run": "PASS", "test": "PASS", "fix": "PASS"}, "test/locale/ar-iq.test.js": {"run": "PASS", "test": "PASS", "fix": "PASS"}, "test/plugin/minMax.test.js": {"run": "PASS", "test": "PASS", "fix": "PASS"}, "test/plugin/isoWeeksInYear.test.js": {"run": "PASS", "test": "PASS", "fix": "PASS"}, "test/locale/uk.test.js": {"run": "PASS", "test": "PASS", "fix": "PASS"}, "test/plugin/buddhistEra.test.js": {"run": "PASS", "test": "PASS", "fix": "PASS"}, "test/display.test.js": {"run": "PASS", "test": "PASS", "fix": "PASS"}, "test/plugin/devHelper.test.js": {"run": "PASS", "test": "PASS", "fix": "PASS"}, "test/locale/lt.test.js": {"run": "PASS", "test": "PASS", "fix": "PASS"}, "test/locale/zh-cn.test.js": {"run": "PASS", "test": "PASS", "fix": "PASS"}, "test/locale/ar.test.js": {"run": "PASS", "test": "PASS", "fix": "PASS"}, "test/plugin/isBetween.test.js": {"run": "PASS", "test": "PASS", "fix": "PASS"}}
{"test/plugin/duration.test.js": {"run": "PASS", "test": "FAIL", "fix": "PASS"}}
{"passed_count": 95, "failed_count": 0, "skipped_count": 0, "passed_tests": ["test/timezone.test.js:Add Time days (DST)", "test/locale/sk.test.js", "test/plugin/weekYear.test.js", "test/plugin/isSameOrBefore.test.js", "test/plugin/pluralGetSet.test.js", "test/timezone.test.js:Diff (DST)", "test/plugin/bigIntSupport.test.js", "test/locale/keys.test.js", "test/locale/zh-tw.test.js", "test/parse.test.js", "test/plugin/weekday.test.js", "test/locale/zh-hk.test.js", "test/plugin/isLeapYear.test.js", "test/query.test.js", "test/plugin/localeData.test.js", "test/timezone.test.js:UTC diff in DST", "test/locale/zh.test.js", "test/plugin/declarations.test.js", "test/locale/bn.test.js", "test/plugin/dayOfYear.test.js", "test/locale/en.test.js", "test/get-set.test.js", "test/locale/he.test.js", "test/locale/bn-bd.test.js", "test/plugin/quarterOfYear.test.js", "test/plugin/objectSupport.test.js", "test/locale/fi.test.js", "test/locale/ar-sa.test.js", "test/issues/issue2027.correct-order.test.js", "test/plugin/utc-utcOffset.test.js", "test/locale/br.test.js", "test/locale/pl.test.js", "test/plugin/toObject.test.js", "test/plugin/isSameOrAfter.test.js", "test/plugin/weekOfYear.test.js", "test/plugin/calendar.test.js", "test/plugin/arraySupport.test.js", "test/plugin/isToday.test.js", "test/locale/hr.test.js", "test/locale/ar-ly.test.js", "test/locale.test.js", "test/plugin/customParseFormat.test.js", "test/plugin/duration.test.js", "test/plugin/isoWeeksInYear.test.js", "test/locale/uk.test.js", "test/locale/zh-cn.test.js", "test/plugin/isBetween.test.js", "test/plugin.test.js", "test/locale/de.test.js", "test/locale/is.test.js", "test/plugin/advancedFormat.test.js", "test/timezone.test.js", "test/utils.test.js", "test/plugin/relativeTime.test.js", "test/plugin/localizedFormat.test.js", "test/plugin/updateLocale.test.js", "test/constructor.test.js", "test/locale/ja.test.js", "test/locale/ar-kw.test.js", "test/locale/ar-dz.test.js", "test/plugin/timezone.test.js", "test/locale/ru.test.js", "test/timezone.test.js:UTC add day in DST", "test/manipulate.test.js", "test/issues/issue2027.swapped-order.test.js", "test/locale/cs.test.js", "test/plugin/utc.test.js", "test/locale/ar-tn.test.js", "test/locale/ar-ma.test.js", "test/locale/sv.test.js", "test/plugin/toArray.test.js", "test/timezone.test.js:Utc Offset", "test/locale/bg.test.js", "test/locale/sr-cyrl.test.js", "test/plugin/isMoment.test.js", "test/plugin/isYesterday.test.js", "test/locale/sr.test.js", "test/locale/it-ch.test.js", "test/plugin/preParsePostFormat.test.js", "test/comparison.test.js", "test/plugin/isTomorrow.test.js", "test/locale/hu.test.js", "test/locale/et.test.js", "test/timezone.test.js:UTC and utcOffset", "test/locale/ku.test.js", "test/plugin/badMutable.test.js", "test/locale/sv-fi.test.js", "test/plugin/isoWeek.test.js", "test/locale/ar-iq.test.js", "test/plugin/minMax.test.js", "test/plugin/buddhistEra.test.js", "test/display.test.js", "test/plugin/devHelper.test.js", "test/locale/lt.test.js", "test/locale/ar.test.js"], "failed_tests": [], "skipped_tests": []}
{"passed_count": 94, "failed_count": 1, "skipped_count": 0, "passed_tests": ["test/timezone.test.js:Add Time days (DST)", "test/locale/sk.test.js", "test/plugin/weekYear.test.js", "test/plugin/isSameOrBefore.test.js", "test/plugin/pluralGetSet.test.js", "test/timezone.test.js:Diff (DST)", "test/plugin/bigIntSupport.test.js", "test/locale/keys.test.js", "test/locale/zh-tw.test.js", "test/parse.test.js", "test/plugin/weekday.test.js", "test/locale/zh-hk.test.js", "test/plugin/isLeapYear.test.js", "test/query.test.js", "test/plugin/localeData.test.js", "test/timezone.test.js:UTC diff in DST", "test/locale/zh.test.js", "test/plugin/declarations.test.js", "test/locale/bn.test.js", "test/plugin/dayOfYear.test.js", "test/locale/en.test.js", "test/get-set.test.js", "test/locale/he.test.js", "test/locale/bn-bd.test.js", "test/plugin/quarterOfYear.test.js", "test/plugin/objectSupport.test.js", "test/locale/fi.test.js", "test/locale/ar-sa.test.js", "test/issues/issue2027.correct-order.test.js", "test/plugin/utc-utcOffset.test.js", "test/locale/br.test.js", "test/locale/pl.test.js", "test/plugin/toObject.test.js", "test/plugin/isSameOrAfter.test.js", "test/plugin/weekOfYear.test.js", "test/plugin/calendar.test.js", "test/plugin/arraySupport.test.js", "test/plugin/isToday.test.js", "test/locale/hr.test.js", "test/locale/ar-ly.test.js", "test/locale.test.js", "test/plugin/customParseFormat.test.js", "test/plugin/isoWeeksInYear.test.js", "test/locale/uk.test.js", "test/locale/zh-cn.test.js", "test/plugin/isBetween.test.js", "test/plugin.test.js", "test/locale/de.test.js", "test/locale/is.test.js", "test/plugin/advancedFormat.test.js", "test/timezone.test.js", "test/utils.test.js", "test/plugin/relativeTime.test.js", "test/plugin/localizedFormat.test.js", "test/plugin/updateLocale.test.js", "test/constructor.test.js", "test/locale/ja.test.js", "test/locale/ar-kw.test.js", "test/locale/ar-dz.test.js", "test/plugin/timezone.test.js", "test/locale/ru.test.js", "test/timezone.test.js:UTC add day in DST", "test/manipulate.test.js", "test/issues/issue2027.swapped-order.test.js", "test/locale/cs.test.js", "test/plugin/utc.test.js", "test/locale/ar-tn.test.js", "test/locale/ar-ma.test.js", "test/locale/sv.test.js", "test/plugin/toArray.test.js", "test/timezone.test.js:Utc Offset", "test/locale/bg.test.js", "test/locale/sr-cyrl.test.js", "test/plugin/isMoment.test.js", "test/plugin/isYesterday.test.js", "test/locale/sr.test.js", "test/locale/it-ch.test.js", "test/plugin/preParsePostFormat.test.js", "test/comparison.test.js", "test/locale/et.test.js", "test/locale/hu.test.js", "test/plugin/isTomorrow.test.js", "test/timezone.test.js:UTC and utcOffset", "test/locale/ku.test.js", "test/plugin/badMutable.test.js", "test/locale/sv-fi.test.js", "test/plugin/isoWeek.test.js", "test/locale/ar-iq.test.js", "test/plugin/minMax.test.js", "test/plugin/buddhistEra.test.js", "test/display.test.js", "test/plugin/devHelper.test.js", "test/locale/lt.test.js", "test/locale/ar.test.js"], "failed_tests": ["test/plugin/duration.test.js"], "skipped_tests": []}
{"passed_count": 95, "failed_count": 0, "skipped_count": 0, "passed_tests": ["test/timezone.test.js:Add Time days (DST)", "test/locale/sk.test.js", "test/plugin/weekYear.test.js", "test/plugin/isSameOrBefore.test.js", "test/plugin/pluralGetSet.test.js", "test/timezone.test.js:Diff (DST)", "test/plugin/bigIntSupport.test.js", "test/locale/keys.test.js", "test/locale/zh-tw.test.js", "test/parse.test.js", "test/plugin/weekday.test.js", "test/locale/zh-hk.test.js", "test/plugin/isLeapYear.test.js", "test/query.test.js", "test/plugin/localeData.test.js", "test/timezone.test.js:UTC diff in DST", "test/locale/zh.test.js", "test/plugin/declarations.test.js", "test/locale/bn.test.js", "test/plugin/dayOfYear.test.js", "test/locale/en.test.js", "test/get-set.test.js", "test/locale/he.test.js", "test/locale/bn-bd.test.js", "test/plugin/quarterOfYear.test.js", "test/plugin/objectSupport.test.js", "test/locale/fi.test.js", "test/locale/ar-sa.test.js", "test/issues/issue2027.correct-order.test.js", "test/plugin/utc-utcOffset.test.js", "test/locale/br.test.js", "test/locale/pl.test.js", "test/plugin/toObject.test.js", "test/plugin/isSameOrAfter.test.js", "test/plugin/weekOfYear.test.js", "test/plugin/calendar.test.js", "test/plugin/arraySupport.test.js", "test/plugin/isToday.test.js", "test/locale/hr.test.js", "test/locale/ar-ly.test.js", "test/locale.test.js", "test/plugin/customParseFormat.test.js", "test/plugin/duration.test.js", "test/plugin/isoWeeksInYear.test.js", "test/locale/uk.test.js", "test/locale/zh-cn.test.js", "test/plugin/isBetween.test.js", "test/plugin.test.js", "test/locale/de.test.js", "test/locale/is.test.js", "test/plugin/advancedFormat.test.js", "test/timezone.test.js", "test/utils.test.js", "test/plugin/relativeTime.test.js", "test/plugin/localizedFormat.test.js", "test/plugin/updateLocale.test.js", "test/constructor.test.js", "test/locale/ja.test.js", "test/locale/ar-kw.test.js", "test/locale/ar-dz.test.js", "test/plugin/timezone.test.js", "test/locale/ru.test.js", "test/timezone.test.js:UTC add day in DST", "test/manipulate.test.js", "test/issues/issue2027.swapped-order.test.js", "test/locale/cs.test.js", "test/plugin/utc.test.js", "test/locale/ar-tn.test.js", "test/locale/ar-ma.test.js", "test/locale/sv.test.js", "test/plugin/toArray.test.js", "test/timezone.test.js:Utc Offset", "test/locale/bg.test.js", "test/locale/sr-cyrl.test.js", "test/plugin/isMoment.test.js", "test/plugin/isYesterday.test.js", "test/locale/sr.test.js", "test/locale/it-ch.test.js", "test/plugin/preParsePostFormat.test.js", "test/comparison.test.js", "test/plugin/isTomorrow.test.js", "test/locale/hu.test.js", "test/locale/et.test.js", "test/timezone.test.js:UTC and utcOffset", "test/locale/ku.test.js", "test/plugin/badMutable.test.js", "test/locale/sv-fi.test.js", "test/plugin/isoWeek.test.js", "test/locale/ar-iq.test.js", "test/plugin/minMax.test.js", "test/plugin/buddhistEra.test.js", "test/display.test.js", "test/plugin/devHelper.test.js", "test/locale/lt.test.js", "test/locale/ar.test.js"], "failed_tests": [], "skipped_tests": []}