Converting long DOIs to short DOIs

go
Published

December 13, 2024

The shortDOI Service

When writing a paper recently, I came across the shortDOI service that creates short aliases for long DOIs. For example, instead of a long DOI like 10.1016/j.virusres.2019.197847 you can now have 10/g8r7pm It will either return an existing alias or create a new one if required.
(And it helped me reduce the paper length by half a page 💪)

Best part is they have a simple public API to create aliases. So, for the above long DOI, we simply append the DOI, and the desired format (either xml or json) to the URL as follows:

https://shortdoi.org/10.1016/j.virusres.2019.197847?format=json

And you get a response like below with the short DOI:

{
    "DOI": "10.1016/j.virusres.2019.197847",
    "ShortDOI": "10/g8r7pm",
    "IsNew": false
}

Automating with Go

I wanted to automate this for all my bib files. So I ended up writing a simple Go program to do this for me. The code is on GitHub.

This also gave me an opportunity to learn several core Go packages as part of my Go learning journey. The first lesson is to use the http package to call the API.

// Return the short doi received from shortdoi.org for long `doi`.
func GetShortDOI(doi string) string {
    doi = strings.ReplaceAll(doi, `\`, "")
    resp, err := http.Get(URL + doi + "?format=json")
    if err != nil {
        panic(err)
    }
    defer resp.Body.Close()

    var result response
    body, err := io.ReadAll(resp.Body)
    if err := json.Unmarshal(body, &result); err != nil {
        fmt.Println(doi)
        panic(err)
    }
    return result.ShortDOI
}

and then use goroutines and WaitGroups to asynchronously call the API for all long DOIs in a file.

// Get short DOIs for each DOI found in the file `f`.
// Returns a map of LongDOI -> ShortDOI.
func getShortDOIs(f *os.File) map[string]string {
    r := regexp.MustCompile(`10\.\d{4,9}/[-.\\_;()/:A-Za-z0-9]+`)

    var wg sync.WaitGroup
    lock := sync.RWMutex{}
    doiMap := make(map[string]string)

    s := bufio.NewScanner(f)
    for s.Scan() {
        line := s.Text()
        m := r.FindString(line)
        if len(m) > 0 {
            wg.Add(1)
            go func(doi string) {
                defer wg.Done()
                shortDoi := GetShortDOI(doi)

                lock.Lock()
                defer lock.Unlock()
                doiMap[doi] = shortDoi
            }(m)
        }
    }
    wg.Wait()

    return doiMap
}

Now if I run the executable as:

./short-doi -i ref.bib -o short-doi-ref.bib

all the long DOIs are replaced with their aliases.

Before After
note = {doi: {10.1016/j.cirpj.2020.02.002}},
note = {doi: {10.1016/j.cropro.2019.05.015}},
note = {doi: 10.1016/j.tree.2023.04.010},
note = {doi: {10.1007/978-1-4020-8992-3\_3}},
note = {doi: {10.3897/rio.10.e125167}},
note = {doi: {10.5751/ES-03400-150213}},
note = {doi: {10.1016/j.envsoft.2017.01.014}},
note = {doi: {10.1007/s13593-015-0327-9}},
note = {doi: {10/ghg846}},
note = {doi: {10/grxgxg}},
note = {doi: 10/nqnv},
note = {doi: {10/dvstgm}},
note = {doi: {10/g8r7pn}},
note = {doi: {10/ggr75q}},
note = {doi: {10/gfvzwx}},
note = {doi: {10/f7x4xt}},