1

I have an array of objects:

const arr = [
{
  name: Exhibit A
}, 
{
  name: Exhibit A1
}, 
{
  name: Exhibit A2
}, 
{
  name: Exhibit B
}, 
{
  name: Exhibit C
}, 
{
  name: Exhibit C1
},  
{
  name: Exhibit C2
}, 
{
  name: Exhibit C3
}, 
]

I need to sort it reverse but Exhibits without numbers should first be in its group.

Smth like that:

const result = [
{
  name: Exhibit C
}, 
{
  name: Exhibit C3
}, 
}, 
{
  name: Exhibit C2
}, 
}, 
{
  name: Exhibit C1
},
{
  name: Exhibit B
}, 
{
  name: Exhibit A
}, 
{
  name: Exhibit A2
}, 
{
  name: Exhibit A1
}
]

const result = [...arr].sort((a, b) => {???})

Simple localeCompare didn't work as I want, so I need to find another approach.

Any ideas?

UPD: I've tried smth like this:

arr.sort((a, b) => {
                  const [fullA, exhibitA, numberA] = a.name.match(/Exhibit\s([^0-9]+)(\d?)/)
                  const [fullB, exhibitB, numberB] = b.name.match(/Exhibit\s([^0-9]+)(\d?)/)
                  if (exhibitA === exhibitB) {
                    if (!numberB || !numberA) {
                      return 1;
                    }

                    if (numberA && numberB) {
                      if (numberB > numberA) {
                        return 1;
                      }
                      if (numberB > numberA) {
                        return -1;
                      }
                    }

                    return 0;
                  }
                  return b.name.localeCompare(a.name);
                })

It's almost what I needed, but Exhibits with the same letter has straight order, not reversed

0

5 Answers 5

1

You can create a custom comparator which suits your needs. See Here for how it looks like.

In your case the comparator would probably constist of a Split of the comparator name and then compare letters first and numbers second. With "no letter" being treated as "better" than any number.

Sign up to request clarification or add additional context in comments.

Comments

1

const arr = [
   { name: "Exhibit A"  }, 
   { name: "Exhibit A1" }, 
   { name: "Exhibit A2" }, 
   { name: "Exhibit B"  }, 
   { name: "Exhibit C"  }, 
   { name: "Exhibit C1" },  
   { name: "Exhibit C2" }, 
   { name: "Exhibit C3" }, 
];

function cmp( a, b ) { return a < b ? -1 : a > b ? 1 : 0; }

// Array of [ 0:orig, 1:base_name, 2:has_number, 3:number ]
const extended = arr.map(
   _ => {
      const matches = _.name.match( /^Exhibit (\D+)(\d*)$/ );
      return [ _, matches[1], matches[2] === "", parseInt( matches[2] ) ];
   }
);

extended.sort(
   ( a, b ) =>
      (  cmp( b[1], a[1] )  // By descending base name, or
      || cmp( b[2], a[2] )  // by descending has digits, or
      || cmp( b[3], a[3] )  // by descending digits.
      )
);

const sorted = extended.map( _ => _[0] );

console.log( sorted );

Extending the array avoids making expensive calculations (regex matches) multiple times for the same inputs. This approach is called Schwartzian transform or decorate-sort-undecorate.

1 Comment

It's a solid approach that can be easily extended / modified based on needs.
0

You could add some Z and sort descending.

const
    format = (s, l) => {
        while (s.length < l) s += 'Z';
        return s;
    }
    asc = (a, b) => a.localeCompare(b),
    desc = (a, b) => {
        const l = Math.max(a.length, b.length);
        return format(b, l).localeCompare(format(a, l))
    },
    array = [{ name: 'Exhibit A9' }, { name: 'Exhibit A' }, { name: 'Exhibit A1' }, { name: 'Exhibit A2' }, { name: 'Exhibit B' }, { name: 'Exhibit C' }, { name: 'Exhibit C1' }, { name: 'Exhibit C2' }, { name: 'Exhibit C3' }];

array.sort((a, b) => desc(a.name, b.name));
console.log(array);

array.sort((a, b) => asc(a.name, b.name));
console.log(array);
.as-console-wrapper { max-height: 100% !important; top: 0; }

Comments

0

This works and is infinitely more readable than the other "Schwartzian transform"

I am using a collator

var collator = new Intl.Collator([], { numeric: true }); // create a collator
const newArr = arr.slice(0)  // take a copy - this is in case array objects are nested, and in general a good idea
  .map(item => item.name
    .replace(/\d/g).length === item.name.length ? { "name": item.name + "ç" } : item); // add some char not in exhibits

newArr.sort((a, b) => collator.compare(b.name, a.name))  // sort using a collator
  .forEach(item => item.name = item.name.replace("ç", ""));

console.log(newArr)
<script>
  const arr = [{
      name: "Exhibit A"
    },
    {
      name: "Exhibit A2"
    },
    {
      name: "Exhibit B"
    },
    {
      name: "Exhibit A1"
    },
    {
      name: "Exhibit C"
    },
    {
      name: "Exhibit C1"
    },
    {
      name: "Exhibit C2"
    },
    {
      name: "Exhibit C3"
    },
  ]
</script>

Comments

0

You need to write a custom compare function. The basic idea is to get the string part and the number part. Then first compare strings and if that is not sufficient compare the optional number part.

// regex which gets a the suffix string and an optional number
const suffixNumberRegex = /([^\d]*)(\d+)?$/;

function specialCompare(a, b) {
  const [, aString, aNumber] = suffixNumberRegex.exec(a.name);
  const [, bString, bNumber] = suffixNumberRegex.exec(b.name);

  // compare the string
  const local = -1 * aString.localeCompare(bString);

  // if it is not equal we use it
  if (local !== 0) {
    return local;
  }

  // else we have to sort by numbers but only if both names contain one

  if (aNumber && bNumber) {
    return (Number.parseInt(aNumber) - Number.parseInt(bNumber)) * -1;
  }

  // if only one has a number the one without one should show up front

  if (aNumber) {
    return 1;
  }

  if (bNumber) {
    return -1;
  }

  return 0;
}

arr.sort(specialCompare);

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.