Repository: vuejs/vue
Pull request: improve Vue.set/Vue.delete API to support multi type of array index

To complement PR #5889 that fixed issue #5884 .

<!--
Please make sure to read the Pull Request Guidelines:
https://github.com/vuejs/vue/blob/dev/.github/CONTRIBUTING.md#pull-request-guidelines
-->

<!-- PULL REQUEST TEMPLATE -->
<!-- (Update "[ ]" to "[x]" to check a box) -->

**What kind of change does this PR introduce?** (check at least one)

- [ ] Bugfix
- [x] Feature
- [ ] Code style update
- [ ] Refactor
- [ ] Build-related changes
- [ ] Other, please describe:

**Does this PR introduce a breaking change?** (check one)

- [ ] Yes
- [x] No

If yes, please describe the impact and migration path for existing applications:

**The PR fulfills these requirements:**

- [x] It's submitted to the `dev` branch for v2.x (or to a previous version branch), _not_ the `master` branch
- [x] When resolving a specific issue, it's referenced in the PR's title (e.g. `fix #xxx[,#xxx]`, where "xxx" is the issue number)
- [x] All tests are passing: https://github.com/vuejs/vue/blob/dev/.github/CONTRIBUTING.md#development-setup
- [x] New/updated tests are included



Changed files:
filename: src/core/observer/index.js
patch: @@ -6,6 +6,7 @@ import {
   def,
   isObject,
   isPlainObject,
+  isValidArrayIndex,
   hasProto,
   hasOwn,
   warn,
@@ -189,7 +190,7 @@ export function defineReactive (
  * already exist.
  */
 export function set (target: Array<any> | Object, key: any, val: any): any {
-  if (Array.isArray(target) && (typeof key === 'number' || /^\d+$/.test(key))) {
+  if (Array.isArray(target) && isValidArrayIndex(key)) {
     target.length = Math.max(target.length, key)
     target.splice(key, 1, val)
     return val
@@ -219,7 +220,7 @@ export function set (target: Array<any> | Object, key: any, val: any): any {
  * Delete a property and trigger change if necessary.
  */
 export function del (target: Array<any> | Object, key: any) {
-  if (Array.isArray(target) && typeof key === 'number') {
+  if (Array.isArray(target) && isValidArrayIndex(key)) {
     target.splice(key, 1)
     return
   }

filename: src/shared/util.js
patch: @@ -17,6 +17,7 @@ export function isTrue (v: any): boolean %checks {
 export function isFalse (v: any): boolean %checks {
   return v === false
 }
+
 /**
  * Check if value is primitive
  */
@@ -47,6 +48,14 @@ export function isRegExp (v: any): boolean {
   return _toString.call(v) === '[object RegExp]'
 }
 
+/**
+ * Check if val is a valid array index.
+ */
+export function isValidArrayIndex (val: any): boolean {
+  const n = parseFloat(val)
+  return n >= 0 && Math.floor(n) === n && isFinite(val)
+}
+
 /**
  * Convert a value to a string that is actually rendered.
  */

filename: test/unit/features/global-api/set-delete.spec.js
patch: @@ -35,6 +35,16 @@ describe('Global API: set/delete', () => {
       Vue.set(vm.list, 1, 'd')
       waitForUpdate(() => {
         expect(vm.$el.innerHTML).toBe('<div>0-a</div><div>1-d</div><div>2-c</div>')
+        Vue.set(vm.list, '2', 'e')
+      }).then(() => {
+        expect(vm.$el.innerHTML).toBe('<div>0-a</div><div>1-d</div><div>2-e</div>')
+        /* eslint-disable no-new-wrappers */
+        Vue.set(vm.list, new Number(1), 'f')
+      }).then(() => {
+        expect(vm.$el.innerHTML).toBe('<div>0-a</div><div>1-f</div><div>2-e</div>')
+        Vue.set(vm.list, '3g', 'g')
+      }).then(() => {
+        expect(vm.$el.innerHTML).toBe('<div>0-a</div><div>1-f</div><div>2-e</div>')
       }).then(done)
     })
 
@@ -106,10 +116,26 @@ describe('Global API: set/delete', () => {
       Vue.delete(vm.lists, 1)
       waitForUpdate(() => {
         expect(vm.$el.innerHTML).toBe('<p>A</p><p>C</p>')
-        Vue.delete(vm.lists, 1)
+        Vue.delete(vm.lists, NaN)
+      }).then(() => {
+        expect(vm.$el.innerHTML).toBe('<p>A</p><p>C</p>')
+        Vue.delete(vm.lists, -1)
+      }).then(() => {
+        expect(vm.$el.innerHTML).toBe('<p>A</p><p>C</p>')
+        Vue.delete(vm.lists, '1.3')
+      }).then(() => {
+        expect(vm.$el.innerHTML).toBe('<p>A</p><p>C</p>')
+        Vue.delete(vm.lists, true)
+      }).then(() => {
+        expect(vm.$el.innerHTML).toBe('<p>A</p><p>C</p>')
+        Vue.delete(vm.lists, {})
+      }).then(() => {
+        expect(vm.$el.innerHTML).toBe('<p>A</p><p>C</p>')
+        Vue.delete(vm.lists, '1')
       }).then(() => {
         expect(vm.$el.innerHTML).toBe('<p>A</p>')
-        Vue.delete(vm.lists, 0)
+        /* eslint-disable no-new-wrappers */
+        Vue.delete(vm.lists, new Number(0))
       }).then(() => {
         expect(vm.$el.innerHTML).toBe('')
       }).then(done)