如何在递归函数中使用 Promise

huangapple go评论62阅读模式
英文:

How to use promises in recursive function

问题

I understand that you'd like a translation of the provided code. Here's the code translation without any additional information:

const fs = require("fs");

class FileTypeInformation {
  constructor(fileExtension, fileCount) {
    this.fileExtension = fileExtension;
    this.fileCount = fileCount;
  }
}

function analyze(folderPath, callback) {
  let result = {
    totalFiles: 0,
    totalSubFolders: 0,
    fileTypesInformation: [],
  };

  function readDirectory(path) {
    fs.readdir(path, (err, files) => {
      if (err) {
        throw err;
      }

      let fileCount = 0;
      let subFolderCount = 0;
      let fileTypeInformation = {};

      files.forEach((name) => {
        const filePath = `${path}/${name}`;
        const stats = fs.statSync(filePath);

        if (stats.isDirectory()) {
          subFolderCount++;
          readDirectory(filePath);
        } else {
          fileCount++;
          const fileExtension = `.${name.split(".").pop().toLowerCase()}`;
          fileTypeInformation[fileExtension] =
            (fileTypeInformation[fileExtension] || 0) + 1;
        }
      });

      result.totalFiles += fileCount;
      result.totalSubFolders += subFolderCount;

      Object.entries(fileTypeInformation).forEach(([fileExtension, fileCount]) => {
        const fileType = new FileTypeInformation(fileExtension, fileCount);
        result.fileTypesInformation.push(fileType);
      });
    });
  }

  readDirectory(folderPath);

  setTimeout(() => {
    for (let i = 0; i < result.fileTypesInformation.length; i++) {
      for (let j = 0; j < result.fileTypesInformation.length; j++) {
        if (
          result.fileTypesInformation[i].fileExtension ===
            result.fileTypesInformation[j].fileExtension &&
          i !== j
        ) {
          result.fileTypesInformation[i].fileCount +=
            result.fileTypesInformation[j].fileCount;
          result.fileTypesInformation.splice(j, 1);
        }
      }
    }
    callback(null, result);
  }, 4000);
}

module.exports = analyze;

Please note that the code you provided has a timeout issue. You mentioned that you wanted to implement promises to solve this issue, but the code you provided doesn't use promises. If you'd like assistance with implementing promises in this code, please let me know, and I can provide guidance on how to do that.

英文:

I'm doing a task in which I have to implement analyze function.
The analyze function should gather information about a folder and its content. The function should return an object that will have the following format.

{
totalFiles: number; // Contains total count of files in the folder and all its sub-folders.
totalSubFolders: number; // Contains total count of subfolders in the folder and all its sub-folders.
fileTypesInformation: FileTypeInformation[]; // Contains an array of FileTypeInformation objects that are described below.
}

The FileTypeInformation object type has the following format.

{
fileExtension: string; // Contains file extension. This should be unique in the whole fileTypesInformation array.
fileCount: number; // Contains total count of files with such extension in the folder and all its sub-folders.
}

> Create a file analyze.js that will export a function that implements
> expected behavior. The callback function passed to the analyze
> function should be called with the result object that has the format
> described in the beginning of the task. The first parameter of the
> callback will be an error object in case any occurred, the second
> parameter will be result. Keep in mind the node.js callback pattern.

This is code that checks this task:

const path = require(&#39;path&#39;);
const analyze = require(&#39;../src/analyze&#39;);
const { expect } = require(&#39;chai&#39;);
const sinon = require(&#39;sinon&#39;);
const testAssetsPath = path.resolve(__dirname, &#39;assets&#39;)
describe(&#39;analyze(folderPath, callback)&#39;, () =&gt; {
it(&#39;should return an error in case a path to invalid folder was provided&#39;, async () =&gt; {
const callbackStub = sinon.stub();
const promise = new Promise(resolve =&gt; {
callbackStub.callsFake(() =&gt; resolve());
});
analyze(&#39;/unexistedfolderblab-blab&#39;, callbackStub);
await promise;
expect(callbackStub.calledOnce).to.be.true;
const callArguments = callbackStub.getCall(0).args;
expect(callArguments[0]).to.be.instanceOf(Error);
expect(callArguments[1]).to.be.equal(undefined);
});
let result;
const callbackStub = sinon.stub();
before(async () =&gt; {
result = await new Promise((resolve) =&gt; {
callbackStub.callsFake(() =&gt; {
resolve();
})
analyze(testAssetsPath, callbackStub);
});
})
it(&#39;should call callback function once&#39;, () =&gt; {
expect(callbackStub.calledOnce).to.be.true;
})
it(&#39;should pass null as an error in case of a success&#39;, () =&gt; {
expect(callbackStub.getCall(0).args[0]).to.be.equal(null);
});
it(&#39;should pass an object that has totalFiles number property&#39;, () =&gt; {
expect(callbackStub.getCall(0).args[1]).to.has.property(&#39;totalFiles&#39;);
expect(typeof callbackStub.getCall(0).args[1].totalFiles).to.be.equal(&#39;number&#39;);
});
it(&#39;should pass an object that has totalSubFolders number property&#39;, () =&gt; {
expect(callbackStub.getCall(0).args[1]).to.has.property(&#39;totalSubFolders&#39;);
expect(callbackStub.getCall(0).args[1].totalSubFolders).to.be.an(&#39;number&#39;);
});
it(&#39;should pass an object that has fileTypesInformation array property&#39;, () =&gt; {
expect(callbackStub.getCall(0).args[1]).to.has.property(&#39;fileTypesInformation&#39;);
expect(callbackStub.getCall(0).args[1].fileTypesInformation).to.be.instanceOf(Array);
});
it(&#39;should pass correct format of objects for fileTypesInformation array&#39;, () =&gt; {
for (const fileTypeInfo of callbackStub.getCall(0).args[1].fileTypesInformation) {
expect(fileTypeInfo).to.has.all.keys(&#39;fileExtension&#39;, &#39;fileCount&#39;);
expect(fileTypeInfo.fileExtension).to.be.an(&#39;string&#39;);
expect(fileTypeInfo.fileCount).to.be.an(&#39;number&#39;);
}
});
it(&#39;should pass correct totalFiles value&#39;, () =&gt; {
expect(callbackStub.getCall(0).args[1].totalFiles).to.be.equal(10);
});
it(&#39;should pass correct totalSubFolders value&#39;, () =&gt; {
expect(callbackStub.getCall(0).args[1].totalSubFolders).to.be.equal(4);
});
it(&#39;should pass fileTypesInformation array that has correct information about extension&#39;, () =&gt; {
expect(callbackStub.getCall(0).args[1].fileTypesInformation).to.have.deep.members([
{
fileExtension: &#39;.logs&#39;,
fileCount: 1,
},
{
fileExtension: &#39;.errors&#39;,
fileCount: 1,
},
{
fileExtension: &#39;.html&#39;,
fileCount: 3,
},
{
fileExtension: &#39;.json&#39;,
fileCount: 1,
},
{
fileExtension: &#39;.jpeg&#39;,
fileCount: 2,
},
{
fileExtension: &#39;.css&#39;,
fileCount: 2,
}
],
&#39;It should has the following information about files: .logs - 1, .errors - 1, .html - 3, .json - 1, .jpeg - 2, .css - 2&#39;
);
})
});

This was my implementation:

/**
* @param folderPath: {String}
* @param callback: {Function}
*/
const fs = require(&quot;fs&quot;);
class FileTypeInformation {
constructor(fileExtension, fileCount) {
this.fileExtension = fileExtension;
this.fileCount = fileCount;
}
}
function analyze(folderPath, callback) {
let result = {
totalFiles: 0,
totalSubFolders: 0,
fileTypesInformation: []
};
function readDirectory(path) {
fs.readdir(path, (err, files) =&gt; {
if (err) {
throw err;
}
let fileCount = 0;
let subFolderCount = 0;
let fileTypeInformation = {};
files.forEach((name) =&gt; {
const filePath = `${path}/${name}`;
const stats = fs.statSync(filePath);
if (stats.isDirectory()) {
subFolderCount++;
readDirectory(filePath);
} else {
fileCount++;
const fileExtension = `.${name.split(&quot;.&quot;).pop().toLowerCase()}`;
fileTypeInformation[fileExtension] =
(fileTypeInformation[fileExtension] || 0) + 1;
}
});
result.totalFiles += fileCount;
result.totalSubFolders += subFolderCount;
Object.entries(fileTypeInformation).forEach(([fileExtension, fileCount]) =&gt; {
const fileType = new FileTypeInformation(fileExtension, fileCount);
result.fileTypesInformation.push(fileType);
});
});
}
readDirectory(folderPath);
setTimeout(() =&gt; {
for (let i = 0; i &lt; result.fileTypesInformation.length; i++){
for (let j = 0; j &lt; result.fileTypesInformation.length; j++) {
if(result.fileTypesInformation[i].fileExtension === result.fileTypesInformation[j].fileExtension &amp;&amp; i !== j){
result.fileTypesInformation[i].fileCount += result.fileTypesInformation[j].fileCount;
result.fileTypesInformation.splice(j,1);
}
}
}
callback(null,result);
}, 4000);
}
module.exports = analyze;

Failure message:
> Timeout of 2000ms exceeded. For async tests and hooks, ensure "done()"
> is called; if returning a Promise, ensure it resolves.
> (/tmp/builds/autocode_11661/test/analyze.js)

I think It should be solved with promises but I am not sure how to implement promises in recursive functions, I can show my implementation with promises also but it does not work as intended.

答案1

得分: 1

由于测试必须在2秒内完成,并且在调用回调之前实现了4秒的延迟,所以不应该感到意外,你会收到超时错误。

相反,你应该在结果准备好后立即调用回调。可能最简单的方法是使用一个支持Promise的库。

fs库可以使用支持Promise的等价方式

import * as fs from 'node:fs/promises';

然后,readdir返回一个Promise。你的函数可以改为async函数,使用awaitPromise.all

async function readDirectory(path) {
  const files = await fs.readdir(path);
  let fileCount = 0;
  let subFolderCount = 0;
  let fileTypeInformation = {};
  await Promise.all(files.map(async (name) => {
    const filePath = `${path}/${name}`;
    const stats = await fs.stat(filePath);
    if (stats.isDirectory()) {
      subFolderCount++;
      await readDirectory(filePath);
    } else {
      fileCount++;
      const fileExtension = `.${name.split(".").pop().toLowerCase()}`;
      fileTypeInformation[fileExtension] =
        (fileTypeInformation[fileExtension] || 0) + 1;
    }
  }));

  result.totalFiles += fileCount;
  result.totalSubFolders += subFolderCount;

  Object.entries(fileTypeInformation).forEach(([fileExtension, fileCount]) => {
    const fileType = new FileTypeInformation(fileExtension, fileCount);
    result.fileTypesInformation.push(fileType);
  });
}

readDirectory(folderPath).then(() => callback(null, result));

可以移除整个setTimeout调用。那些for循环是不需要的。它们假设收集到的文件类型可能具有重复的文件扩展名,但这是不可能的,因为它们是一个对象的键(fileTypeInformation)。此外,在迭代数组时进行splice操作会引发问题。

英文:

Since the tests must complete in 2 seconds and you have implemented a delay 4 seconds before calling the callback, it should come as no surprise that you get a timeout error.

Instead you should call the callback as soon as you have the result ready. It is probably easiest to use a promise-enabled library.

The fs library which use use has promise enabled equivalents:

import * as fs from &#39;node:fs/promises&#39;;

Then readdir returns a promise. Your function can then change to an async function, using await and Promise.all

  async function readDirectory(path) {
const files = await fs.readdir(path);
let fileCount = 0;
let subFolderCount = 0;
let fileTypeInformation = {};
await Promise.all(files.map(async (name) =&gt; {
const filePath = `${path}/${name}`;
const stats = fs.statSync(filePath);
if (stats.isDirectory()) {
subFolderCount++;
await readDirectory(filePath);
} else {
fileCount++;
const fileExtension = `.${name.split(&quot;.&quot;).pop().toLowerCase()}`;
fileTypeInformation[fileExtension] =
(fileTypeInformation[fileExtension] || 0) + 1;
}
}));
result.totalFiles += fileCount;
result.totalSubFolders += subFolderCount;
Object.entries(fileTypeInformation).forEach(([fileExtension, fileCount]) =&gt; {
const fileType = new FileTypeInformation(fileExtension, fileCount);
result.fileTypesInformation.push(fileType);
});
}
readDirectory(folderPath).then(() =&gt; callback(null,result));

That entire setTimeout call can be removed. Those for loops are not needed. They assume that the collected file types could have duplicate file extensions, but that is not possible, because they were keys of an object (fileTypeInformation). Furthermore, doing a splice on an array you are iterating is asking for trouble.

huangapple
  • 本文由 发表于 2023年6月26日 17:17:06
  • 转载请务必保留本文链接:https://go.coder-hub.com/76555276.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定