'How are FF and Chrome sort functions different from each other?

For some reason firefox and chrome don't seem to have the same sort method algorithms in their javascript engines. Is there a clear way to know why or how these are being processed differently from one another? It seems kind of strange to have to find out in testing that a simple array sort is causing bugs in other browsers when it should be a pretty straightforward comparison, no?

Here's the code I'm using: I collect an rss feed xml file, create a shallow copy to not be dealing with Symbols or whatever just in case, I just insert episodes with titles containing "Special" because I don't really care where they get inserted. Then, instead of creating date objects and doing a bunch of needless processing, since every episode title in the rss xml starts with the episode number like "Ep.25: Episode Name" I thought I could just parse the string and use the integer value between "Ep." and ":" to sort by integer instead of datetime objects.

For some reason Firefox doesn't give me the same results as Chrome and I can't understand how that would be different or wrong no matter what engine is processing the javascript.

See live example at https://krisdriver.com/fg

const EpisodeContainer = ()=>{

// random card background image if no specific index number is selected (select by map index)
const cardbg = ()=>{
    let x = [bgselect]

    return x[x.length-1]
}

// collect the rss feed info and start the async chain
async function loadingdata () {
    return await rssdata
}

let data    // collects results from async xml data collection
const epsperpage = 12          // PRODUCTION CONSIDERATION: consider allowing options for preloading, # per page, etc
const [datastate, setDataState] = useState(data)
const [epsLoaded, setEpsLoaded] = useState(false)    

useEffect( ()=>{
        loadingdata().then( res =>{ 
            // eslint-disable-next-line react-hooks/exhaustive-deps
            data = [...res]
            let sortedEpisodes_byDate = [...data[0]]  // create shallow copy, not reference
            // start sorting the episodes by their data...
            sortedEpisodes_byDate.sort( (x,y)=>{
                // sorting episodes so part 1 and 2 are always in order
                    // exclude special episodes
                if(!x.title.toUpperCase().includes('SPECIAL')){
                    // take the episode number from the title
                    let _temp = x.title.split(':')      // take the "Ep.xx:" out of the title
                    let _temp2 = _temp[0].split('.')    // take the number out of "Ep.xx"
                    let _temp3 = y.title.split(':')
                    let _temp4 = _temp3[0].split('.')
                    let [_x, _y] = [parseInt(_temp2[1]), parseInt(_temp4[1])] // compare integer values of ep numbers
                    if ( _x > _y) return 1             // sort
                    else return -1                     // sort
                }
                else return -1   // just insert the specials
            } )

            let sorted = [...sortedEpisodes_byDate]
            let _tmp = { eps: sorted.reverse(), channel: data[1] }
            
            // return the sorted final output data object, without episodes being sectioned into pages
            return _tmp 

        }).then( response=>{    // take final sorted data and start breaking it into sections for the DOM to render as pages...
            if (datastate === undefined) {
                // Organize episode info into pages
                let numEpsDisplayedPerPage = epsperpage                     // referred to as 'x' in the comments of this scope
                let numOfEps = response.eps.length
                let episodePages = []
                let pageloop = Math.ceil(numOfEps / numEpsDisplayedPerPage)
                
                // for each page of x eps...
                for(let i = 0; i < pageloop; i++){
                    let pageofepisodes = []
                    // for each of the x eps within page range...
                    for(let j = 0; j < numEpsDisplayedPerPage; j++ ){
                        // 'i*numEpsPerPage' is the index of Episode in the data array, the '+j' selects 1 through x on current page
                        pageofepisodes.push(response.eps[ (i*numEpsDisplayedPerPage) + j] )
                    }
                    episodePages.push([...pageofepisodes]) // make a shallow copy of the output array
                }
                // remove 'undefined' elements on the last page, if they exist
                let len = episodePages.length-1
                episodePages[len] = episodePages[len].filter( x => {
                    return x !== undefined
                })

                // finally, set the state to the data object which includes channel info, all episodes sorted, and paginated array
                let tmp_obj = { ...response }
                // remove time from publication date...
                tmp_obj.eps.forEach( v => {
                    v.published = removeTimeFromPubDate(v.published).trim()
                })
                let tmp_ = { ...tmp_obj, pages: [...episodePages], sortselected: 1, currentPage: 0, buttonText: 'Newest First' }
                // console.log(tmp_)

                // ready, set state with data object sanitized
                setDataState({ ...tmp_ })
                // allow for useEffect to render contents
                setEpsLoaded(true)
            }   // This all only runs on the first load, before the state is loaded. Data is parsed and split into pages
        })
}, [])

// sanitize publish date to remove time and timezone from display cards next to episode titles
const removeTimeFromPubDate = (published) => published.split(':')[0].slice(0, -2)

"EPISODE" COMPONENT ABOVE THIS LINE-------- "data" imported into "Episode" component below this line-----

class Episode{
    constructor(title, link, author, description, published, enclosure, itunestags, mediatags, guid){
      this.title = title.innerHTML
      this.links = link.innerHTML
      this.author = author.innerHTML
      this.description = description.innerHTML
      this.published = published.innerHTML
      this.enc = enclosure.innerHTML
      this.itunes = itunestags
      this.media = mediatags
      this.mp3url = mediatags[1].attributes[0].value    // ACCURATE MP3 URL
      this.id = guid.innerHTML  // NOTE!!! This id is actually a text string url as a unique id in the rss document. not a good id, nor is it a good source for an accurate url string. Use "mp3url" property instead
      // for accurate link to mp3 download, use this.media[1].attributes[0].value (0: url, 1: sample rate, 2:lang)
    }
  }
  
  class Channel{
    constructor(title, youtube_channel_url, description, language, copyright, image, itunes_tags, media_tags){
      this.title = title.innerHTML
      this.url = youtube_channel_url.innerHTML
      this.description = description.innerHTML
      this.lang = language.innerHTML
      this.copyright = copyright.innerHTML
      this.image = {
        imgurl: image.children[0].innerHTML,
        imgdesc: image.children[2].innerHTML,
        width: image.children[4].innerHTML,
        height: image.children[5].innerHTML
        }
      this.itunes = {
        img: itunes_tags[0],
        categories: [itunes_tags[1].innerHTML, itunes_tags[2].innerHTML, itunes_tags[3].innerHTML],
        explicit: itunes_tags[4].innerHTML,
        title: itunes_tags[5].innerHTML,
        subtitle: itunes_tags[6].innerHTML,
        summary: itunes_tags[7].innerHTML,
        keywords: itunes_tags[8].innerHTML,
        author: itunes_tags[9].innerHTML,
        owner: itunes_tags[10].innerHTML,
        link: itunes_tags[11].innerHTML
        }
      this.media = media_tags
    }
  }

    // Make array of episodes, each with templated object notation data
    let episodes = []

async function whenDataLoaded(){
  // FOR PRODUCTION - COMMENT OUT FOR DEVELOPMENT MODE
    let data = await fetch('https://krisdriver.com/feed/rssfeed.xml').then( res => {  
      // "data" is imported for development mode
      // console.log(res)
      return res.text()
    })
  // FOR PRODUCTION - COMMENT OUT FOR DEVELOPMENT MODE

  
    // parse xml data and store
    const parser = new DOMParser()
    const xml = await parser.parseFromString(data, 'application/xml')
    const getelements = await xml.getElementsByTagName('rss')
    const channel = await getelements[0].getElementsByTagName('channel')[0]
    
    // Create containers for episode data and channel data to create class objects out of
    let channelData = await getChannelInfo(channel) // returns array of <channel> children until an <item> tag is traversed
    let episodeData = await getEpisodeData(channel) // returns all item tags as html collection
    
    for(let i of episodeData) {
        episodes.push(new Episode(i.children[0], i.children[1], i.children[2], i.children[3], i.children[4], i.children[5], [i.children[6], i.children[7], i.children[8], i.children[9]], [i.children[10], i.children[11]], i.children[12]))
    }
    
    // Create an object with channel details in object legible notation
    let i = await channelData   // for less typing
    const channelDetails = await new Channel(i[0], i[1], i[2], i[3], i[4], i[5], [ i[6], i[7], i[8], i[9], i[10], i[11], i[12], i[13], i[14], i[15], i[16], i[17] ], [ i[18], i[19], i[20], i[21], i[22], i[23], i[24], i[25], i[26], i[27], i[28], i[29], i[30], i[31], i[32], i[33] ] )
  return [episodes, channelDetails]
}

function getEpisodeData(xml){
    return xml.getElementsByTagName('item')
}

function getChannelInfo(xmlstream){
    // console.log(xmlstream)
    let kids = xmlstream.children
    let result = []
    for(let i = 0; i<kids.length-1; i++){
        if(kids[i].tagName === 'item') return result  // stop looping once we get to the first item tag, return completed list
        else if(kids[i].nodeType === 1){
            result.push(kids[i])  //  add channel child elements which are not <item> tags to make a channel object with
        }
    }
}

let result = async function(){ 
    let x = await whenDataLoaded(); 
    return x
}

export default result()

https://krisdriver.com/fg/

THE RSS XML BEING PARSED BELOW------------

        <item>
    <title>Ep.61: Public Good</title>
    <link>https://www.youtube.com/channel/UCb3cCrFqaHFBp2s7jgtJvFg</link>
    <author>[email protected] (Kristopher Driver and Jordan Roy)</author>
    <description>Today Kris makes a case for government sponsored programs. This is not a narrow topic as allocation of resources is always a complicated mess of interests. Universities, corporations, the media, the general public, and those finding themselves at the bottom, all take part. Join us for this debate as we discuss what might work to lift up the less fortunate in our society so that we may all benefit. QUOTE: “The 25% of Spaniards who are presently without work simply don’t (by Milton’s presumption) want to work at the prevailing wage and are on vacation.” - Mark Blyth, Austerity: The History of a Dangerous Idea #FrivolousGravitas Links: Channel: https://www.youtube.com/channel/UCb3cCrFqaHFBp2s7jgtJvFg FB: https://www.facebook.com/Frivolous-Gravitas-Podcast-109356198202987/ Twitter: https://twitter.com/FrivolousGravi1 RSS: https://krisdriver.com/feed/rssfeed.xml If you like this kind of content and want to see more, be sure to like, share and subscribe to help keep this channel going. Use RSS url to subscribe to Frivolous Gravitas using your favourite podcast player to catch up on past episodes and listen any time you want. No commercials, no sponsors, no ads, just straight talk on demand and on the go. Original Music by Epistra </description>
    <pubDate>Feb 18 2022 05:30:00 CST</pubDate>
    <enclosure url="https://www.krisdriver.com/feed/podcast/ep61.mp3" type="audio/mpeg" length="187312271"/>
    <itunes:author>Kristopher Driver and Jordan Roy</itunes:author>
    <itunes:episodeType>full</itunes:episodeType>
    <itunes:episode>61</itunes:episode>
    <itunes:image>
    <url>https://www.krisdriver.com/feed/rssitunesb.jpg</url>
    <title>Frivolous Gravitas</title>
    <link>https://www.youtube.com/channel/UCb3cCrFqaHFBp2s7jgtJvFg</link>
    </itunes:image>
    <media:title>Ep.61: Public Good</media:title>
    <media:content url="https://www.krisdriver.com/feed/podcast/ep61.mp3" samplingrate="44.1" lang="en" type="audio/mpeg" expression="full"/>
    <guid>http://www.krisdriver.com/feed/podcast/ep61.mp3</guid>
    </item>
    <item>
    <title>Ep.60: Charity</title>
    <link>https://www.youtube.com/channel/UCb3cCrFqaHFBp2s7jgtJvFg</link>
    <author>[email protected] (Kristopher Driver and Jordan Roy)</author>
    <description>It is hard to argue that charity is anything but good, but it is not always evident that our offerings are indeed worthwhile. Many who donate don't give much thought beyond the act itself, but charity is not so simple. It can be seen as a gamble, a waste, or a necessity depending on the point of view. We hope you will join us as we look at what makes a good, and bad, act of charity. QUOTE: "Whoever closes his ear to the cry of the poor will himself call out and not be answered." - Proverbs 21:13 #FrivolousGravitas Links: Channel: https://www.youtube.com/channel/UCb3cCrFqaHFBp2s7jgtJvFg FB: https://www.facebook.com/Frivolous-Gravitas-Podcast-109356198202987/ Twitter: https://twitter.com/FrivolousGravi1 RSS: https://krisdriver.com/feed/rssfeed.xml If you like this kind of content and want to see more, be sure to like, share and subscribe to help keep this channel going. Use RSS url to subscribe to Frivolous Gravitas using your favourite podcast player to catch up on past episodes and listen any time you want. No commercials, no sponsors, no ads, just straight talk on demand and on the go. Original Music by Epistra </description>
    <pubDate>Feb 11 2022 12:30:00 CST</pubDate>
    <enclosure url="https://www.krisdriver.com/feed/podcast/ep60.mp3" type="audio/mpeg" length="133276531"/>
    <itunes:author>Kristopher Driver and Jordan Roy</itunes:author>
    <itunes:episodeType>full</itunes:episodeType>
    <itunes:episode>60</itunes:episode>
    <itunes:image>
    <url>https://www.krisdriver.com/feed/rssitunesb.jpg</url>
    <title>Frivolous Gravitas</title>
    <link>https://www.youtube.com/channel/UCb3cCrFqaHFBp2s7jgtJvFg</link>
    </itunes:image>
    <media:title>Ep.60: Charity</media:title>
    <media:content url="https://www.krisdriver.com/feed/podcast/ep60.mp3" samplingrate="44.1" lang="en" type="audio/mpeg" expression="full"/>
    <guid>http://www.krisdriver.com/feed/podcast/ep60.mp3</guid>
    </item>


Solution 1:[1]

A custom sort function, when passed any two array values, must adhere to the Reversal property of inequalities. In other words...

  • mySort( a, b ) == -mySort( b, a )

...otherwise inconsistent sort results will occur. Per my earlier comment, this appears to be the case with the sortedEpisodes_byDate.sort( (x,y)... algorithm, which does not adhere to the above property for every combination of array values. To exemplify...

a = [ 'def-SPECIAL', 'abc', 'xyz-SPECIAL' ];

function mySort( a, b ) {
  if ( a.includes( 'SPECIAL' ) ) {
    return -1;
  } else {
    return a.localeCompare( b );
  }
};


console.log( 'Initial Array:' );
console.log( a );

console.log( '\nFirst time sorting...');
console.log( a.sort( mySort ).join( ',' ) );

console.log( '\nSecond time sorting...');
console.log( a.sort( mySort ).join( ',' ) );

console.log( '\nThird time sorting...');
console.log( a.sort( mySort ).join( ',' ) );

Note that mySort, not adhering to the Reversal property, returns inconsistent array sorts. This is because mySort only checks if a.includes( 'SPECIAL' ) and not b, in a similar fashion as the sortedEpisodes_byDate sort is currently coded. Case in point...

  • mySort( 'abc', 'xyz-SPECIAL' ) returns -1 because 'abc' < 'xyz'
  • mySort( 'xyz-SPECIAL', 'abc' ) returns -1 because 'xyz-SPECIAL'.includes( 'SPECIAL' )

So now it is a crapshoot as to whether the underlying browser Javascript engine is going to treat 'abc' or 'xyz-SPECIAL' as being the lesser of the other...

Bottom line, I believe the sortedEpisodes_byDate.sort( (x,y)... needs fixing...

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1