[TypeScript] Sort formatted texts
Tags: typescript
Published: 2021-09-26 00:59:34

## Intro
I will try sorting a specific formatted string values.

#### Before
```
'SAMPLE1-1-4-A',
'SAMPLE1-1-4-3',
'SAMPLE1-1-2-B',
'SAMPLE1-2-1',
'SAMPLE1-2-12-2',
'SAMPLE1-2-2-F',
'SAMPLE1-2-5-A',
'SAMPLE1-3-5',
'SAMPLE2-3-10',
'SAMPLE2-4-2-10',
'SAMPLE2-4-2-1',
'SAMPLE2-1-1-14',
'SAMPLE2-14-2-Z',
'SAMPLE2-1-10-C',
'SAMPLE2-1-4-1',
'SAMPLE2-1-4',
'SAMPLE2-2-4-A',
'SAMPLE2-23-2',
'SAMPLE2-4-2-2',
'SAMPLE2-10-2'
```
#### After
```
"SAMPLE1-1-2-B"
"SAMPLE1-1-4-3"
"SAMPLE1-1-4-A"
"SAMPLE1-2-1"
"SAMPLE1-2-2-F"
"SAMPLE1-2-5-A"
"SAMPLE1-2-12-2"
"SAMPLE2-1-4"
"SAMPLE2-1-4-1"
"SAMPLE2-1-10-C"
"SAMPLE2-2-4-A"
"SAMPLE2-3-10"
"SAMPLE2-4-2-1"
"SAMPLE2-4-2-2"
"SAMPLE2-4-2-10"
"SAMPLE2-4-B"
"SAMPLE2-10-2"
"SAMPLE2-14-2-Z"
"SAMPLE2-D-1-14"
"SAMPLE10-3-5"
```

## localeCompare (Failed)
I sorted them with "localeCompare".

* [String.prototype.localeCompare() - JavaScript | MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/localeCompare)

#### sortSample.ts
```ts
export class SortSample {
    private originalTexts: readonly string[] = [
        'SAMPLE1-1-4-A',
        'SAMPLE1-1-4-3',
        'SAMPLE1-1-2-B',
        'SAMPLE1-2-1',
        'SAMPLE1-2-12-2',
        'SAMPLE1-2-2-F',
        'SAMPLE1-2-5-A',
        'SAMPLE10-3-5',
        'SAMPLE2-3-10',
        'SAMPLE2-4-2-10',
        'SAMPLE2-4-2-1',
        'SAMPLE2-D-1-14',
        'SAMPLE2-14-2-Z',
        'SAMPLE2-1-10-C',
        'SAMPLE2-1-4-1',
        'SAMPLE2-1-4',
        'SAMPLE2-2-4-A',
        'SAMPLE2-4-B',
        'SAMPLE2-4-2-2',
        'SAMPLE2-10-2'
    ];
    public sort(): readonly string[] {
        const copiedTexts = [...this.originalTexts];
        return copiedTexts.sort((a, b) => a.localeCompare(b));
    }
}
```
#### Result
```
0: "SAMPLE1-1-2-B"
1: "SAMPLE1-1-4-3"
2: "SAMPLE1-1-4-A"
3: "SAMPLE1-2-1"
4: "SAMPLE1-2-12-2"
5: "SAMPLE1-2-2-F"
6: "SAMPLE1-2-5-A"
7: "SAMPLE1-3-5"
8: "SAMPLE2-1-1-14"
9: "SAMPLE2-1-10-C"
10: "SAMPLE2-1-4"
11: "SAMPLE2-1-4-1"
12: "SAMPLE2-10-2"
13: "SAMPLE2-14-2-Z"
14: "SAMPLE2-2-4-A"
15: "SAMPLE2-23-2"
16: "SAMPLE2-3-10"
17: "SAMPLE2-4-2-1"
18: "SAMPLE2-4-2-10"
19: "SAMPLE2-4-2-2"
```

They were orderd like "SAMPLE2-4-2-1" -> "SAMPLE2-4-2-10" -> "SAMPLE2-4-2-2".
Numbers such as "4", "2", and "1" should be treated as "number" type instead of "string" type.

## Split and parse to numbers
I Split and parse them to numbers.

#### sortSample.ts
```ts
export class SortSample {
    private originalTexts: readonly string[] = [
...
    ];
    public sort(): readonly string[] {
        console.log(this.compareWithSplit);
        
        const copiedTexts = [...this.originalTexts];
        //return copiedTexts.sort((a, b) => a.localeCompare(b));
        return copiedTexts.sort((a, b) => this.compareWithSplit(a, b))
    }    
    private compareWithSplit(a: string|null|undefined, b: string|null|undefined): number {
        if(a == null && b == null) {
            return 0;
        }
        if(a == null) {
            return 1;
        }
        if(b == null) {
            return -1;
        }
        if(a.startsWith('SAMPLE') === false) {
            return a.localeCompare(b);
        }
        const splittedA = a.replace('SAMPLE', '').split('-');
        const splittedB = b.replace('SAMPLE', '').split('-');
        let index = 0;
        for(const sa of splittedA) {
            if(index >= splittedB.length) {
                return 1;
            }
            // to ignore compile error because the type is treated as string|undefined
            const sb = splittedB[index] ?? '';
            const na = parseInt(sa);
            const nb = parseInt(sb);
            if(isNaN(na)) {
                // sa: [A-Z]
                if(isNaN(nb)) {
                    // sb: [A-Z]
                    return sa.localeCompare(sb);
                }
                return 1;
            }
            if(isNaN(nb)) {
                // sa: [0-9] sb: [A-Z]
                return -1;
            }
            if((na - nb) !== 0) {
                return (na - nb);
            }
            // if na === nb, move next values.
            index += 1;
        }
        return (splittedA.length - splittedB.length);
    }
}
```

#### Result
```
0: "SAMPLE1-1-2-B"
1: "SAMPLE1-1-4-3"
2: "SAMPLE1-1-4-A"
3: "SAMPLE1-2-1"
4: "SAMPLE1-2-2-F"
5: "SAMPLE1-2-5-A"
6: "SAMPLE1-2-12-2"
7: "SAMPLE2-1-4"
8: "SAMPLE2-1-4-1"
9: "SAMPLE2-1-10-C"
10: "SAMPLE2-2-4-A"
11: "SAMPLE2-3-10"
12: "SAMPLE2-4-2-1"
13: "SAMPLE2-4-2-2"
14: "SAMPLE2-4-2-10"
15: "SAMPLE2-4-B"
16: "SAMPLE2-10-2"
17: "SAMPLE2-14-2-Z"
18: "SAMPLE2-D-1-14"
19: "SAMPLE10-3-5"
```

## Is there more simple way?
I find this code verbose, so I'm looking for a simpler way. 

For example, because these texts are like "SAMPLE[0-9]-[0-9|A-Z]-[0-9|A-Z]" or "SAMPLE[0-9]-[0-9|A-Z]-[0-9|A-Z]-[0-9|A-Z]", so I can split them with regex.

```ts
...
private compareWithSplit(a: string|null|undefined, b: string|null|undefined): number {
...
        const splittedA = a.match(/(([0-9]+)|([0-9|A-Z]+$))/g);
        const splittedB = b.match(/(([0-9]+)|([0-9|A-Z]+$))/g);
        if((splittedA == null || splittedA.length <= 0) ||
                (splittedB == null || splittedB.length <= 0)) {
            return a.localeCompare(b);
        }
        let index = 0;
        for(const sa of splittedA) {
            if(index >= splittedB.length) {
                return 1;
            }
            // to ignore compile error because the type is treated as string|undefined
            const sb = splittedB[index] ?? '';
            const na = parseInt(sa);
            const nb = parseInt(sb);
            if(isNaN(na)) {
                // sa: [A-Z]
                if(isNaN(nb)) {
                    // sb: [A-Z]
                    return sa.localeCompare(sb);
                }
                return 1;
            }
            if(isNaN(nb)) {
                // sa: [0-9] sb: [A-Z]
                return -1;
            }
            if((na - nb) !== 0) {
                return (na - nb);
            }
            // if na === nb, move next values.
            index += 1;
        }
        return (splittedA.length - splittedB.length);
    }
...
```
But I think it's not so efficient.
If I found more better way, I will write in this post.